Modern enterprise applications require scalability, auditability, and separation of concerns. Two architectural patterns help solve these challenges efficiently:
- CQRS (Command Query Responsibility Segregation) separates read and write models.
- Event Sourcing stores changes to application state as a sequence of immutable events.
In this post, you’ll learn how to implement CQRS and Event Sourcing in Spring Boot using real examples, Kafka for event publishing, and PostgreSQL for projections โ all in the package: com.kscodes.springboot.advanced
.

๐งฑ What is CQRS?
CQRS is a pattern that separates write operations (commands) from read operations (queries):
- Command Model: Handles state changes (create/update/delete).
- Query Model: Handles read operations using a separate model, optimized for querying.
๐ What is Event Sourcing?
Instead of storing the current state, Event Sourcing stores a full history of changes as events:
- Events are immutable facts (e.g.,
OrderCreated
,ItemAdded
) - The state is reconstructed by replaying these events
๐ฆ Tools Used
- Spring Boot 3.x
- Spring Data JPA
- Apache Kafka (for event publishing)
- PostgreSQL (for projections)
- Jackson (for event serialization)
๐๏ธ Project Structure
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
com.kscodes.springboot.advanced โ โโโ command โ โโโ model โ Aggregate Root (Write model) โ โโโ service โ Command handlers โ โโโ controller โ API for commands โ โโโ event โ โโโ model โ Domain events โ โโโ store โ Event storage โ โโโ publisher โ Kafka/Log publisher โ โโโ query โ โโโ model โ Read model โ โโโ repository โ Projections โ โโโ controller โ API for queries |
๐ 1. Domain Event: OrderCreatedEvent.java
1 2 3 4 5 6 7 8 9 10 11 12 |
package com.kscodes.springboot.advanced.event.model; public class OrderCreatedEvent { private String orderId; private String customer; private double amount; // Constructor, Getters } |
๐ 2. Command Model: Order.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package com.kscodes.springboot.advanced.command.model; public class Order { private String orderId; private String customer; private double amount; public Order(String orderId, String customer, double amount) { this.orderId = orderId; this.customer = customer; this.amount = amount; } public OrderCreatedEvent toEvent() { return new OrderCreatedEvent(orderId, customer, amount); } } |
๐งฐ 3. Command Handler: OrderCommandService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package com.kscodes.springboot.advanced.command.service; import com.kscodes.springboot.advanced.command.model.Order; import com.kscodes.springboot.advanced.event.model.OrderCreatedEvent; import com.kscodes.springboot.advanced.event.publisher.OrderEventPublisher; import org.springframework.stereotype.Service; @Service public class OrderCommandService { private final OrderEventPublisher eventPublisher; public OrderCommandService(OrderEventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } public void createOrder(Order order) { OrderCreatedEvent event = order.toEvent(); eventPublisher.publish(event); } } |
๐ค 4. Kafka Publisher: OrderEventPublisher.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package com.kscodes.springboot.advanced.event.publisher; import com.kscodes.springboot.advanced.event.model.OrderCreatedEvent; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Component; @Component public class OrderEventPublisher { private final KafkaTemplate<String, OrderCreatedEvent> kafkaTemplate; public OrderEventPublisher(KafkaTemplate<String, OrderCreatedEvent> kafkaTemplate) { this.kafkaTemplate = kafkaTemplate; } public void publish(OrderCreatedEvent event) { kafkaTemplate.send("orders-events", event); } } |
๐งฉ 5. Event Listener (for Projection): OrderEventListener.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package com.kscodes.springboot.advanced.query.service; import com.kscodes.springboot.advanced.event.model.OrderCreatedEvent; import com.kscodes.springboot.advanced.query.model.OrderView; import com.kscodes.springboot.advanced.query.repository.OrderViewRepository; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Service; @Service public class OrderEventListener { private final OrderViewRepository repository; public OrderEventListener(OrderViewRepository repository) { this.repository = repository; } @KafkaListener(topics = "orders-events", groupId = "read-model") public void handle(OrderCreatedEvent event) { OrderView view = new OrderView(event.getOrderId(), event.getCustomer(), event.getAmount()); repository.save(view); } } |
๐ 6. Read Model (Projection): OrderView.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package com.kscodes.springboot.advanced.query.model; import jakarta.persistence.Entity; import jakarta.persistence.Id; @Entity public class OrderView { @Id private String orderId; private String customer; private double amount; public OrderView(String orderId, String customer, double amount) { this.orderId = orderId; this.customer = customer; this.amount = amount; } // Getters and setters } |
๐ 7. Query API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package com.kscodes.springboot.advanced.query.controller; import com.kscodes.springboot.advanced.query.model.OrderView; import com.kscodes.springboot.advanced.query.repository.OrderViewRepository; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/orders") public class OrderQueryController { private final OrderViewRepository repository; public OrderQueryController(OrderViewRepository repository) { this.repository = repository; } @GetMapping public List<OrderView> getAll() { return repository.findAll(); } } |
๐งช Test Flow
- POST to
/command/orders
โ sends command - Command triggers
OrderCreatedEvent
- Kafka publishes to topic
OrderEventListener
updates read model- GET
/orders
โ reads from projection
โ Benefits of CQRS + Event Sourcing
Feature | Benefit |
---|---|
Separation | Better scalability & decoupling |
Audit Trail | Complete event history |
Performance | Query models can be optimized independently |
Flexibility | Add new consumers without changing producers |
โ ๏ธ Challenges to Consider
- Event Versioning: Handle changes to event schemas
- Event Ordering: Preserve consistency
- Replay Handling: Support state rebuilds from event logs
- Idempotency: Avoid duplicate processing
๐ Conclusion
CQRS and Event Sourcing with Spring Boot enable you to build highly scalable, resilient, and auditable systems. By separating write logic from read logic and storing event histories instead of raw state, you gain more control, traceability, and architectural flexibility.
While CQRS and Event Sourcing introduce complexity, they shine in microservices, finance, e-commerce, and any domain where event tracking and audit logs are crucial.
Start small โ and scale as your application grows.