Skip to content

CQRS (Command Query Responsibility Segregation)


Definition

CQRS Definition - Traditional vs Separate Models


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 Benefits and Trade-offs


CQRS + Event Sourcing

CQRS with Event Sourcing Architecture


Tips & Tricks

CQRS Tips and Tricks