Home/Learn/Microservices/CQRS Pattern

CQRS Pattern

Advanced
Data Management

Command Query Responsibility Segregation separates the write model (commands mutate state) from the read model (queries hit optimised read stores), each scaling independently.

Overview

CQRS (Command Query Responsibility Segregation) splits a service's data access model into two independent parts: the command model handles writes (create, update, delete) against a normalised relational store optimised for consistency; the query model handles reads against one or more denormalised read stores (Elasticsearch, Redis, a flattened SQL view) optimised for query performance and scalability. The two models are kept in sync asynchronously via domain events. CQRS solves the fundamental tension between write models (3NF, referential integrity, constraints) and read models (wide, denormalised, fast). It is often paired with Event Sourcing but the two are independent — you can use CQRS without Event Sourcing and vice versa.

Command Side vs Query Side

In the command side, business operations (PlaceOrder, CancelOrder) mutate the write store through a domain model with invariant checks. After each successful mutation, a domain event (OrderPlaced, OrderCancelled) is published.

The query side subscribes to those events and updates one or more read projections. A read projection is a denormalised, query-optimised representation of the data — e.g., an OrderSummaryDocument in Elasticsearch that includes the customer name, product names, and total in a single document, so a query for "all orders for customer X" is a single index lookup with no joins.

Java — CQRS Command & Query Sides
// ── COMMAND SIDE ─────────────────────────────────────────────
// Command — represents an intent to change state
public record PlaceOrderCommand(String customerId, List<OrderItem> items) {}

// Command handler — executes business logic, publishes event
@Service
@RequiredArgsConstructor
public class OrderCommandHandler {
    private final OrderRepository orderRepo;        // write store
    private final ApplicationEventPublisher events;

    @Transactional
    public String handle(PlaceOrderCommand cmd) {
        Order order = Order.place(cmd.customerId(), cmd.items());
        orderRepo.save(order);
        events.publishEvent(new OrderPlacedEvent(order.getId(), order.getCustomerId(),
                                                  order.getTotal(), Instant.now()));
        return order.getId();
    }
}

// ── QUERY SIDE ────────────────────────────────────────────────
// Read model — denormalised projection for fast queries
@Document(indexName = "order-summaries")  // Elasticsearch document
public class OrderSummaryDocument {
    private String orderId;
    private String customerName;  // denormalised from Customer service
    private String customerEmail;
    private BigDecimal total;
    private String status;
    private Instant placedAt;
}

// Query handler — reads from the optimised read store
@Service
@RequiredArgsConstructor
public class OrderQueryHandler {
    private final OrderSummaryRepository esRepo; // Elasticsearch repo

    public Page<OrderSummaryDocument> findByCustomer(String customerId, Pageable page) {
        return esRepo.findByCustomerId(customerId, page);
    }
}

Event Projection — Keeping Read Models in Sync

The query side subscribes to domain events from the command side and updates read projections. Each event triggers a targeted update to the affected document(s). This is eventually consistent — there is a small window between a command completing and the projection updating.

Java — Event Projection
@Component
@RequiredArgsConstructor
public class OrderProjection {

    private final OrderSummaryRepository esRepo;
    private final CustomerRepository customerRepo; // to denormalise customer data

    @EventListener  // or @KafkaListener if events cross service boundaries
    @Async
    public void on(OrderPlacedEvent event) {
        // Fetch supplementary data for denormalisation
        Customer customer = customerRepo.findById(event.customerId()).orElseThrow();

        OrderSummaryDocument doc = new OrderSummaryDocument();
        doc.setOrderId(event.orderId());
        doc.setCustomerName(customer.getFullName());
        doc.setCustomerEmail(customer.getEmail());
        doc.setTotal(event.total());
        doc.setStatus("PENDING");
        doc.setPlacedAt(event.occurredAt());

        esRepo.save(doc);   // write to Elasticsearch read store
    }

    @EventListener
    @Async
    public void on(OrderCancelledEvent event) {
        esRepo.findById(event.orderId()).ifPresent(doc -> {
            doc.setStatus("CANCELLED");
            esRepo.save(doc);
        });
    }
}

CQRS Benefits, Costs, and When to Use It

CQRS is not free — it introduces eventual consistency between write and read models, two codebases to maintain, and infrastructure for event propagation. Use it when:

- Read and write loads differ dramatically (typical in e-commerce: 100 reads per write) - Different query patterns need different data structures (full-text search vs relational) - You need audit history or replay capability - You have already committed to a microservices architecture

Do NOT use CQRS for simple CRUD services — a single repository and JpaRepository is perfectly fine for most data access needs.

Java — When to Use CQRS
// CQRS is NOT needed here — simple CRUD is fine
@RestController
@RequiredArgsConstructor
public class ProductController {
    private final ProductRepository repo;

    @GetMapping("/products/{id}")
    public Product get(@PathVariable Long id) {
        return repo.findById(id).orElseThrow();
    }

    @PostMapping("/products")
    public Product create(@RequestBody Product p) {
        return repo.save(p);
    }
}

// CQRS IS worth it here:
// Write store: PostgreSQL — normalised schema, FK constraints
// Read store:  Elasticsearch — full-text search, filters, facets
// Event bus:   Kafka — async projection updates
// Scale:       Read pods × 20, Write pods × 2

Key Points to Remember

  • 1CQRS splits reads and writes into separate models: command side uses a normalised write store; query side uses a denormalised read store.
  • 2The two sides are kept in sync asynchronously via domain events — the system is eventually consistent.
  • 3A key benefit: the read store can use a completely different technology (Elasticsearch, Redis, Cassandra) optimised for query patterns.
  • 4CQRS and Event Sourcing are independent patterns — CQRS can be used with a simple SQL write store (not just event-sourced state).
  • 5The cost: eventual consistency, two codebases, and event infrastructure. Only adopt CQRS when you have genuinely different read/write needs.
  • 6Start without CQRS; introduce it when read/write requirements diverge beyond what indexes and views can solve.

Interview Questions

Sign in to ask Aria
1

What is CQRS and what problem does it solve?

EasyAmazon
2

How are the command and query sides kept in sync in a CQRS system?

MediumUber
3

What are the trade-offs of CQRS — what do you gain and what do you give up?

MediumNetflix
4

Is CQRS the same as Event Sourcing? Explain the relationship.

MediumThoughtworks
5

A user places an order (command), then immediately queries their order list (query). The order is not in the list. Why, and how do you handle this in the UI?

HardLinkedIn

Ask Aria about CQRS Pattern

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…