CQRS
Command Query Responsibility Segregation separates write operations (Commands) from read operations (Queries), each with dedicated buses and handlers.
Core Concept
- Commands modify state, return nothing
- Queries return data, modify nothing
This separation enables optimized read/write paths and clearer intent in application code.
Commands
Commands represent intentions to change system state. They are immutable, named imperatively (verb + noun), and contain all data needed for execution.
$command = new CreateBookCommand($id, $title, $author, $isbn);
$this->commandBus->dispatch($command);
Handlers execute commands and return void:
public function handle(CommandInterface $command): void
{
$book = new Book(/* ... */);
$this->bookRepository->save($book);
$this->eventBus->dispatch(new BookCreatedEvent($book));
}
Queries
Queries request data without side effects. They can include filter criteria.
Handlers return QueryResponseInterface objects containing the requested data.
Handler Resolution
Handlers are resolved by naming convention:
| Message | Handler |
|---|---|
CreateBookCommand | CreateBookHandler |
ListBooksQuery | ListBooksHandler |
Requirements: same namespace, appropriate interface, registered in DI container.
Read Models
Read models are denormalized data structures optimized for query scenarios. They combine data from multiple entities into a single object, avoiding N+1 queries and complex joins at runtime.
Example: LoanWithDetails
The Loan entity references Book and User by ID only. For display purposes, a read model combines all related data:
final readonly class LoanWithDetails
{
public function __construct(
public string $loanId,
public string $userId,
public string $userEmail,
public string $bookId,
public string $bookTitle,
public string $borrowedAt,
public string $dueAt,
public ?string $returnedAt,
public string $status,
public bool $isOverdue,
) {}
}
Repository Method
Repositories provide dedicated methods returning read models with pre-joined data:
When to Use Read Models
| Scenario | Use Entity | Use Read Model |
|---|---|---|
| Single aggregate operations | ✓ | |
| List views with related data | ✓ | |
| Reports and dashboards | ✓ | |
| Write operations | ✓ | |
| Display-only joined data | ✓ |
Source Files
app/demo/Loan/Domain/ReadModel/LoanWithDetails.php
Best Practices
- Commands don't return data (use events or queries for results)
- Queries don't mutate state
- One handler per message
- Handlers orchestrate; business logic stays in domain
- Use read models for complex query scenarios with joins
When to Use CQRS vs Direct Mode
Phexium supports two architectural modes. Choose based on complexity and requirements.
Decision Framework
| Criteria | CQRS (Bus Mode) | Direct Mode (UseCase) |
|---|---|---|
| Business logic | Complex, with invariants | Simple, straightforward |
| Domain events | Needed | Not needed |
| Transactions | Required | Optional |
| Middleware | Auth, logging, validation | Minimal |
| Side effects | Multiple (events, notifications) | None or few |
| Testing | Handlers tested in isolation | UseCase tested directly |
Use CQRS (Bus Mode) When
- Complex business logic with domain events
- Operations requiring database transactions
- Write operations with side effects (notifications, audit logs)
- Operations needing middleware (authentication, authorization, logging)
- Multiple bounded contexts communicating via events
Use Direct Mode (UseCase) When
- Simple read-only operations (homepage, dashboards)
- Rapid prototyping or proof of concept
- No domain events needed
- Single responsibility with minimal dependencies
- Performance-critical paths without middleware overhead
Dual-Mode Coexistence
Both modes coexist in the same application. Choose per module based on complexity:
app/demo/
├── Homepage/ → Direct Mode (simple welcome page)
├── Library/ → CQRS (book management with events)
├── Loan/ → CQRS (borrowing with transactions)
└── User/ → CQRS (authentication with events)
This hybrid approach provides flexibility: simple modules stay simple, complex modules get full CQRS benefits.
Anti-patterns
| Anti-pattern | Problem | Solution |
|---|---|---|
| Commands returning data | Violates CQS, unclear intent | Use events or follow-up query |
| Queries modifying state | Side effects in read path | Move mutation to command |
| Business logic in handlers | Handlers become untestable | Keep logic in domain entities |
| Handlers calling handlers | Hidden dependencies, hard to trace | Use events for cross-handler communication |
| Skipping the bus | Bypasses middleware, transactions | Always dispatch through bus |
See Also
- Bus Mode - CQRS implementation with middleware
- Direct Mode - UseCase pattern for simple operations
- Decision Matrix - Choosing between architectural modes
- Commands & Handlers - Command implementation details
- Queries & Handlers - Query implementation details
- Command Bus - Command dispatching plugin
- Query Bus - Query dispatching plugin
- Event Bus - Event dispatching plugin