CQRS Pattern in Java: Complete Guide with Spring Boot
CQRS — Command Query Responsibility Segregation — is an architectural pattern that separates the read (query) and write (command) sides of your application into distinct models. Instead of a single model handling both reads and writes, you have two: one optimized for writing data, one optimized for reading it.
This guide covers CQRS theory, practical implementation with Spring Boot, and real-world trade-offs so you can decide when it is the right choice for your system.
What is CQRS?
The term was coined by Greg Young, building on Bertrand Meyer's Command-Query Separation (CQS) principle. CQS states that a method should either change state (a command) or return data (a query) — never both. CQRS applies this at the architectural level.
In a traditional CRUD application, a single OrderService handles creating orders, updating them, cancelling them, and also fetching order lists and order details. As the system grows, the read requirements (paginated lists, aggregated reports, dashboards) and write requirements (validation, business rules, consistency) start to conflict. CQRS solves this by splitting them apart.
CQRS Architecture Overview
A CQRS system has two sides:
- Command side: Receives commands (CreateOrder, CancelOrder, UpdateAddress). Applies business rules, validates input, writes to the write store. Returns only a success acknowledgement — never data.
- Query side: Receives queries (GetOrderById, ListOrdersByCustomer). Reads from a read-optimized store. Returns DTOs shaped exactly for the UI. No business logic.
In simple CQRS, both sides share the same database but use separate models. In full CQRS with Event Sourcing, the read store is a separate database (e.g., Elasticsearch) kept in sync via domain events.
Implementing the Command Side
Define a command as a simple immutable object:
public record CreateOrderCommand(
@NotBlank String customerId,
@NotEmpty List<OrderLineItem> items,
String shippingAddress
) {}
Create a command handler that contains all business logic:
@Service
@Transactional
public class OrderCommandHandler {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
public OrderCommandHandler(OrderRepository repo, ApplicationEventPublisher pub) {
this.orderRepository = repo;
this.eventPublisher = pub;
}
public UUID handle(CreateOrderCommand command) {
if (command.items().isEmpty()) {
throw new IllegalArgumentException("Order must have at least one item");
}
Order order = new Order();
order.setCustomerId(command.customerId());
order.setShippingAddress(command.shippingAddress());
order.setStatus(OrderStatus.PENDING);
order.setItems(command.items());
Order saved = orderRepository.save(order);
// Publish event so the read side can sync
eventPublisher.publishEvent(new OrderCreatedEvent(saved.getId()));
return saved.getId();
}
}
The handler returns only the new entity ID — never the full object. That is the query side's job.
Implementing the Query Side
Create dedicated read models (DTOs) shaped for what the UI actually needs:
public record OrderSummaryDto(
UUID id,
String customerName,
int itemCount,
BigDecimal totalAmount,
String status,
LocalDateTime createdAt
) {}
Use a dedicated query service that bypasses the domain model entirely:
@Service
@Transactional(readOnly = true)
public class OrderQueryService {
private final EntityManager em;
public OrderQueryService(EntityManager em) { this.em = em; }
public List<OrderSummaryDto> findByCustomer(String customerId) {
return em.createQuery("""
SELECT new com.example.dto.OrderSummaryDto(
o.id, c.name, SIZE(o.items), o.totalAmount, o.status, o.createdAt
)
FROM Order o JOIN Customer c ON o.customerId = c.id
WHERE o.customerId = :cid ORDER BY o.createdAt DESC
""", OrderSummaryDto.class)
.setParameter("cid", customerId)
.getResultList();
}
}
REST Controller Wiring
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderCommandHandler commands;
private final OrderQueryService queries;
@PostMapping
public ResponseEntity<Map<String, UUID>> create(@RequestBody @Valid CreateOrderCommand cmd) {
UUID id = commands.handle(cmd);
return ResponseEntity.status(201).body(Map.of("id", id));
}
@GetMapping("/customer/{customerId}")
public List<OrderSummaryDto> listByCustomer(@PathVariable String customerId) {
return queries.findByCustomer(customerId);
}
}
When to Use CQRS
CQRS adds complexity. Use it when you have a genuine need:
- Read and write workloads have very different scalability requirements
- Complex domain logic on the write side conflicts with flexible query requirements
- You are implementing Event Sourcing and need a full audit log
- Different teams own read and write paths and need to evolve them independently
CQRS vs Traditional CRUD
| Aspect | CRUD | CQRS |
|---|---|---|
| Models | Single model for read + write | Separate command and query models |
| Complexity | Low | Higher — two models to maintain |
| Scalability | Scale whole service together | Scale read and write independently |
| Query flexibility | Limited by domain model shape | Query model shaped for UI needs |
| Consistency | Immediate/strong | Often eventual (if separate stores) |
| Best for | Simple apps, small teams | Complex domains, high read:write ratio |
Conclusion
CQRS is a powerful pattern for systems where read and write concerns have genuinely different requirements. By separating commands from queries, you can optimize each side independently and simplify complex domain logic. Start with simple CQRS (same database, separate models) before reaching for full event-sourced CQRS — most applications do not need it.
Related: Java Design Patterns Guide | Java Microservices Architecture