CQRS (Command Query Responsibility Segregation)
Definition

Commands and Queries
// COMMAND: Intent to change state (write)
// Named as imperative (do something)
public class CreateOrderCommand {
private final String customerId;
private final List<OrderItem> items;
}
public class CancelOrderCommand {
private final String orderId;
private final String reason;
}
// COMMAND HANDLER: Processes commands
@Service
class OrderCommandHandler {
@CommandHandler
public void handle(CreateOrderCommand cmd) {
// Validate
Customer customer = customerRepository.findById(cmd.getCustomerId())
.orElseThrow(() -> new CustomerNotFoundException());
// Execute business logic
Order order = Order.create(customer, cmd.getItems());
// Persist
orderRepository.save(order);
// Publish events
eventPublisher.publish(new OrderCreatedEvent(order));
}
@CommandHandler
public void handle(CancelOrderCommand cmd) {
Order order = orderRepository.findById(cmd.getOrderId())
.orElseThrow(() -> new OrderNotFoundException());
order.cancel(cmd.getReason());
orderRepository.save(order);
eventPublisher.publish(new OrderCancelledEvent(order));
}
}
// QUERY: Request for data (read)
// Named as question (get something)
public class GetOrderQuery {
private final String orderId;
}
public class GetOrdersByCustomerQuery {
private final String customerId;
private final int page;
private final int size;
}
// QUERY HANDLER: Processes queries
@Service
class OrderQueryHandler {
@QueryHandler
public OrderView handle(GetOrderQuery query) {
return orderReadRepository.findById(query.getOrderId())
.orElseThrow(() -> new OrderNotFoundException());
}
@QueryHandler
public Page<OrderSummary> handle(GetOrdersByCustomerQuery query) {
return orderReadRepository.findByCustomerId(
query.getCustomerId(),
PageRequest.of(query.getPage(), query.getSize())
);
}
}
Read Model Synchronization
// OPTIONS FOR KEEPING READ MODEL IN SYNC
// OPTION 1: Same database, different tables/views
// Simplest approach, strong consistency
@Service
class OrderService {
@Transactional
public void createOrder(CreateOrderCommand cmd) {
// Write to normalized tables
Order order = orderRepository.save(new Order(...));
// Also update denormalized read model
orderReadModelRepository.save(new OrderReadModel(order));
}
}
// OPTION 2: Event-driven synchronization
// Eventually consistent, more scalable
@Component
class OrderReadModelUpdater {
@EventHandler
void on(OrderCreatedEvent event) {
OrderReadModel readModel = new OrderReadModel();
readModel.setOrderId(event.getOrderId());
readModel.setCustomerName(event.getCustomerName());
readModel.setItems(event.getItems());
readModel.setTotal(event.getTotal());
orderReadModelRepository.save(readModel);
}
@EventHandler
void on(OrderShippedEvent event) {
OrderReadModel readModel = orderReadModelRepository
.findById(event.getOrderId()).orElseThrow();
readModel.setStatus("SHIPPED");
readModel.setShippedDate(event.getShippedDate());
readModel.setTrackingNumber(event.getTrackingNumber());
orderReadModelRepository.save(readModel);
}
}
// OPTION 3: Database replication + transformation
// Use CDC (Change Data Capture) to sync
// Write to OLTP database, read from OLAP database
Read Model Design
// READ MODELS: Optimized for queries
// Write model (normalized for consistency)
@Entity
class Order {
@Id
private String id;
private String customerId; // FK to Customer
@OneToMany
private List<OrderItem> items; // FK to items
}
@Entity
class Customer {
@Id
private String id;
private String name;
private String email;
}
// Read model (denormalized for performance)
@Document
class OrderView {
private String orderId;
private String customerId;
private String customerName; // Denormalized
private String customerEmail; // Denormalized
private List<ItemView> items; // Embedded
private BigDecimal total; // Pre-calculated
private String status;
private Instant createdAt;
}
// Different read models for different use cases
// Dashboard view (summary data)
class OrderSummary {
private String orderId;
private String customerName;
private BigDecimal total;
private String status;
}
// Search view (full-text indexed)
@Document(indexName = "orders")
class OrderSearchView {
private String orderId;
private String customerName;
private String productNames; // All product names concatenated
private List<String> tags;
}
// Analytics view (aggregated)
class DailyOrderStats {
private LocalDate date;
private int orderCount;
private BigDecimal totalRevenue;
private BigDecimal averageOrderValue;
}
Benefits & Trade-offs

CQRS + Event Sourcing

Tips & Tricks
