Product Decisions This Supports
- CQRS/Event-Driven Architecture: Enables strict separation of read (queries) and write (commands) operations, reducing side effects and improving scalability. Justifies investment in domain-driven design (DDD) by providing a standardized way to handle queries as first-class citizens.
- Modular Query Logic: Decouples query handling from controllers/services, allowing for reusable, testable, and maintainable query components. Supports the "fat model, skinny controller" principle by offloading business logic to query handlers.
- Build vs. Buy Decision: Avoids reinventing a query bus from scratch, reducing technical debt while maintaining flexibility. The MIT license allows for customization if needed, and the package’s extension of SimpleBus ensures compatibility with existing Laravel/PHP ecosystems.
- Roadmap Priorities:
- API Layer Refactoring: Replace direct repository calls in controllers with structured query buses (e.g.,
UserQueryBus->handle(new FindUserById())), improving consistency and reducing boilerplate.
- Analytics/Reporting: Offload complex read operations (e.g., "sales by region") to dedicated query handlers, enabling easier maintenance and scalability.
- Microservices Standardization: Standardize query patterns across services for consistency, especially in distributed systems where read operations may span multiple services.
- Use Cases:
- Search/Filtering: Replace ad-hoc Eloquent queries (e.g.,
Model::where()->get()) with structured query objects (e.g., SearchProductsByCriteria), improving readability and reusability.
- Caching Strategies: Implement caching at the query bus level (via middleware) to reduce database load and improve performance for repeated queries.
- Audit Logging: Log query execution details (e.g., "User fetched by ID 123") via middleware, enhancing observability and debugging capabilities.
- Validation: Add query validation logic (e.g., checking for valid search parameters) via middleware before processing, reducing errors in downstream services.
When to Consider This Package
- Adopt When:
- Your Laravel/PHP application has complex query logic (e.g., nested filters, joins, or business-logic-heavy reads) that is scattered across controllers or services.
- You are adopting CQRS or DDD and need a lightweight, standardized way to handle queries as first-class objects.
- Your team prefers explicit over implicit query design, where queries are defined as objects (e.g.,
FindActiveUsers) rather than ad-hoc SQL or Eloquent chains.
- You want to leverage middleware for queries (e.g., caching, logging, validation) without modifying query handlers directly.
- You are building a microservices architecture and need consistent query patterns across services.
- Look Elsewhere If:
- Your queries are simple CRUD operations (e.g.,
User::find($id)), where the overhead of a query bus is not justified.
- You rely heavily on Eloquent’s dynamic query builder (e.g.,
whereHas, with) and need flexible, dynamic SQL generation. Consider Laravel’s built-in query scopes or packages like spatie/laravel-query-builder.
- Your team lacks experience with PHP/DDD patterns, as this package requires understanding of message handlers, resolvers, and middleware.
- You need real-time or asynchronous query results (e.g., WebSocket updates). This package is synchronous and not designed for async workflows.
- Performance is critical for high-throughput queries. Benchmark against direct repository calls or Eloquent queries first, as the query bus may introduce slight overhead.
How to Pitch It (Stakeholders)
For Executives:
*"This package transforms how we handle database queries—turning them into reusable, structured components that behave like API calls. Instead of writing ad-hoc SQL or Eloquent queries in every controller, we’ll define queries as objects (e.g., FindUserByEmail) and let a dedicated query bus handle them. This approach reduces bugs, speeds up development, and makes it easier to add features like caching or analytics later.
Why it matters:
- Faster Development: Queries become reusable components, like functions but with middleware support for caching, logging, or validation.
- Lower Maintenance: Changes to query logic (e.g., adding a new filter) only need to be updated in one place.
- Scalability: Middleware enables features like caching or rate-limiting without touching query handlers, making the system more robust as we grow.
Example: Instead of writing User::where('email', $email)->first() in 10 different places, we define a FindUserByEmail query once and reuse it everywhere. This also makes it easier to add features like caching or audit logs later.
ROI: Reduced technical debt, faster feature delivery, and a more maintainable codebase—especially as our application scales."*
For Engineering:
*"This package extends SimpleBus to support synchronous query buses, filling a gap in Laravel’s ecosystem for structured query handling. Here’s why it’s a strong fit for our stack:
Key Benefits:
Trade-offs:
- Learning Curve: Requires understanding of message buses and resolvers, but the documentation is clear, and the package is well-structured.
- Synchronous Only: Not suitable for async workflows (use Laravel Queues or a command bus for those).
- Small Community: Only 5 stars, but the MIT license means we can fork or maintain it if needed.
Proposal:
- Pilot Phase: Implement 2–3 complex queries (e.g., dashboard metrics, search) to validate the approach.
- Standardization: Enforce query objects for all new read operations to ensure consistency.
- Integration: Add middleware for caching (e.g., Redis) and logging to demonstrate value early.
Alternatives Considered:
- Roll Our Own: Would take 2–3 dev days; this package gives us that functionality for free.
- Laravel’s Built-in: No query bus support; would require custom middleware.
- Other Packages: None combine SimpleBus’s maturity with query-specific features like this one.
Next Steps:
- Spike: Implement a single query (e.g.,
GetUserOrders) with this package and compare performance to direct Eloquent calls.
- Align with DDD/CQRS roadmap if applicable.
- Prototype middleware for caching or logging to showcase immediate benefits."*
For Developers:
*"This package lets you treat queries like commands—structured, reusable, and decoupled from your controllers. Here’s how it works in practice:
Before (Ad-hoc Queries):
// Controller
public function show(User $user) {
$orders = Order::where('user_id', $user->id)
->where('status', 'completed')
->with('items')
->get();
return view('orders', compact('orders'));
}
After (Query Bus):
// Query Object
class FindCompletedUserOrders {
public function __construct(public User $user) {}
}
// Handler
class FindCompletedUserOrdersHandler {
public function handle(FindCompletedUserOrders $query) {
return Order::where('user_id', $query->user->id)
->where('status', 'completed')
->with('items')
->get();
}
}
// Usage
$queryBus->handle(new FindCompletedUserOrders($user), $orders);
Why This Rocks:
Getting Started:
- Install the package:
composer require ajgl/simple-bus-query-bus
- Set up your query bus (see README).
- Start with a simple query and handler, then expand.
Pro Tip: Use this for queries that are used in multiple places or have complex logic. For simple find() calls, Eloquent is still fine!"*