Introduction: When CRUD Stops Working
\n\nImagine 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\nThese 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\nWhat is CQRS in Simple Terms
\n\nCQRS 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\nWhy 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
Code Example: Separating Commands and Queries
\n\nLet's say we have an order management system. Instead of doing OrderService.save(order), we separate:
// 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\nNote: OrderRepository (write) and OrderReadRepository (read) are different interfaces. They can point to different databases or different schemas.
Event Sourcing: Storing History, Not State
\n\nEvent 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\nHow It Works in Java
\n\nEach 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