Skip to content

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.

$query = new ListBooksQuery();
$response = $this->queryBus->dispatch($query);

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:

public function findAllWithDetails(): array  // Returns LoanWithDetails[]

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