CQRS and Event Sourcing in Java: Architecture That Isn't Afraid of Complexity

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: When CRUD Stops Working

\n\n

Imagine you are writing a system for stock exchange trading. Every second — thousands of orders. Each order is not just "insert a record". It's balance verification, portfolio recalculation, auditing, notifications. Now imagine you need not only to record the current state but also to reconstruct what happened at 14:35:22.003 yesterday. Regular CRUD (Create, Read, Update, Delete) breaks here. Welcome to the world of CQRS and Event Sourcing in Java.

\n\n

These two patterns are not just buzzwords. They are tools that allow you to build systems with high performance, full auditing, and the ability to "rewind time". In this article, we will break down how to implement CQRS (Command Query Responsibility Segregation) and Event Sourcing in Java using modern frameworks and clean code. No fluff, with examples.

\n\n

What is CQRS in Simple Terms

\n\n

CQRS is the separation of responsibility between commands (write) and queries (read). In classic CRUD, you use the same model for reading and writing. In CQRS, you create two different models: one for changing data (commands), another for retrieving it (queries).

\n\n

Why is this needed?

\n
    \n
  • Performance: Reading and writing scale independently. You can have 10 replicas for reading and 2 for writing.
  • \n
  • Security: Commands can validate business rules, while queries simply return data.
  • \n
  • Flexibility: For reading, you can use denormalized views; for writing, normalized ones.
  • \n
\n\n

Code Example: Separating Commands and Queries

\n\n

Let's say we have an order management system. Instead of doing OrderService.save(order), we separate:

\n\n
// Command — an intention to change state\npublic class CreateOrderCommand {\n    private final String orderId;\n    private final String customerId;\n    private final List<OrderItem> items;\n\n    // constructor, getters...\n}\n\n// Command handler\n@Component\npublic class CreateOrderCommandHandler {\n    @Autowired\n    private OrderRepository orderRepository;\n\n    public void handle(CreateOrderCommand command) {\n        // Business rule validation\n        if (command.getItems().isEmpty()) {\n            throw new IllegalArgumentException("Order must have at least one item");\n        }\n        Order order = new Order(command.getOrderId(), command.getCustomerId(), command.getItems());\n        orderRepository.save(order);\n    }\n}\n\n// Query — retrieving data\npublic class GetOrderQuery {\n    private final String orderId;\n    // constructor, getters...\n}\n\n// Query handler\n@Component\npublic class GetOrderQueryHandler {\n    @Autowired\n    private OrderReadRepository orderReadRepository;\n\n    public OrderDTO handle(GetOrderQuery query) {\n        // Direct read from an optimized database\n        return orderReadRepository.findById(query.getOrderId());\n    }\n}
\n\n

Note: OrderRepository (write) and OrderReadRepository (read) are different interfaces. They can point to different databases or different schemas.

\n\n

Event Sourcing: Storing History, Not State

\n\n

Event Sourcing is a pattern where we save not the current state of an object, but the sequence of events that led to that state. Instead of "Order #123: status = DELIVERED", we store: "Event 1: Order created", "Event 2: Item added", "Event 3: Order paid", "Event 4: Order delivered".

\n\n

How It Works in Java

\n\n

Each event is an immutable object. We save them in an Event Store (a specialized database). The current state of an aggregate is calculated by replaying all events.

\n\n
// Base event\npublic interface Do                

Blogs

Book Recommendations