Implementing CQRS and Event Sourcing in Spring Boot: A Complete Guide

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.

CQRS Event Sourcing Spring Boot

๐Ÿงฑ 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


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


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


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


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


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 kafkaTemplate;

    public OrderEventPublisher(KafkaTemplate kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void publish(OrderCreatedEvent event) {
        kafkaTemplate.send("orders-events", event);
    }
}

๐Ÿงฉ 5. Event Listener (for Projection): OrderEventListener.java


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


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


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 getAll() {
        return repository.findAll();
    }
}

๐Ÿงช Test Flow

  1. POST to /command/orders โ†’ sends command
  2. Command triggers OrderCreatedEvent
  3. Kafka publishes to topic
  4. OrderEventListener updates read model
  5. GET /orders โ†’ reads from projection

โœ… Benefits of CQRS + Event Sourcing

FeatureBenefit
SeparationBetter scalability & decoupling
Audit TrailComplete event history
PerformanceQuery models can be optimized independently
FlexibilityAdd 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.

๐Ÿ”— External References