Implementing CQRS Patterns
1. Command Model Pattern
| Aspect | Detail |
|---|---|
| Purpose | Validates business rules and applies state changes |
| Schema | Normalized, optimized for invariants |
| Returns | Acknowledgment / event, not data |
| Examples | PlaceOrder, CancelOrder, ApplyDiscount |
Example: Command + Handler
public record PlaceOrderCommand(CustomerId customer, List<Item> items) {}
@Component
public class PlaceOrderHandler {
public OrderId handle(PlaceOrderCommand cmd) {
Order order = orderFactory.create(cmd.customer(), cmd.items());
orderRepo.save(order);
eventBus.publish(new OrderPlaced(order.id(), cmd.customer(), order.total()));
return order.id();
}
}
2. Query Model Pattern
| Aspect | Detail |
|---|---|
| Purpose | Optimized for reads; no business rules |
| Schema | Denormalized; one table per UI screen often |
| Returns | DTOs shaped for client |
| Storage | RDBMS, ES, Redis — whatever fits read pattern |
3. Read Models Pattern
| Type | Use Case |
|---|---|
| List View | Paginated table of orders |
| Detail View | Full order with line items, customer, shipping |
| Aggregated View | Sales by day, top products |
| Search Index | Full-text searchable view |
4. Event Sourcing with CQRS
| Component | Role |
|---|---|
| Event Store | Sole source of truth (write side) |
| Aggregate | Rebuilt from events; processes commands → new events |
| Projection | Subscribes to event stream → builds read models |
| Query API | Reads from projections only |
ES + CQRS Flow
[Command] → [Aggregate] → [Event Store] → [Projection] → [Read Model] → [Query]
↓
[Other Consumers]
5. Separate Read/Write Databases
| Side | Storage Choice |
|---|---|
| Write | PostgreSQL with strong consistency, normalization |
| Read | Elasticsearch (search), Redis (low-latency lookup), Materialized RDBMS |
| Sync | Events / CDC; eventual consistency |
| Scale | Independent scale and tech choice per side |
6. Projection Pattern
| Property | Detail |
|---|---|
| Function | (currentReadModel, event) → newReadModel |
| Idempotent | Required since events may replay |
| Offset Tracking | Persist last-processed event ID/offset |
| Rebuild | Reset offset → replay full stream |
7. Snapshot Pattern
| Aspect | Detail |
|---|---|
| Purpose | Avoid replaying entire event history on every load |
| Mechanism | Periodic snapshot of aggregate state at version N |
| Load | Load snapshot + events after N |
| Frequency | Every X events (e.g., 100) or time-based |
8. Command Handler Pattern
| Step | Action |
|---|---|
| 1. Validate | Schema, authorization, format |
| 2. Load Aggregate | From repo (or replay events) |
| 3. Apply Command | Business rules; produce events |
| 4. Persist | Save aggregate / append events |
| 5. Publish | Emit events via outbox |
9. Query Handler Pattern
| Aspect | Detail |
|---|---|
| Input | Query DTO (filter, paging, sorting) |
| Output | Read DTO directly from read store |
| No Side Effects | Pure function over read model |
| Caching | Often safe to cache aggressively |
Example: Query Handler
public record OrderListQuery(CustomerId customer, int page, int size) {}
@Component
public class OrderListHandler {
public Page<OrderSummaryDto> handle(OrderListQuery q) {
return orderViewRepo.findByCustomer(q.customer(),
PageRequest.of(q.page(), q.size(), Sort.by("placedAt").descending()));
}
}
10. Eventually Consistent Queries
| UX Strategy | Detail |
|---|---|
| Optimistic Update | Show new state in UI before server confirms |
| Indicator | "Saving..." spinner or syncing badge |
| Read-after-Write | Cache write locally for the writing client |
| Polling | Re-fetch until projection catches up |
| Versioned Reads | Pass written version; wait until read store catches up |