CQRS is a pattern that decouples read and write operations for a data store, allowing you to scale them independently.
Let’s see CQRS in action with Spring Boot and Axon. Imagine an e-commerce system where we manage Product entities.
Command Side (Writes):
When a user wants to update a product’s price, a UpdateProductPriceCommand is dispatched.
// Command
public class UpdateProductPriceCommand {
private String productId;
private BigDecimal newPrice;
// getters and setters
}
// Command Handler
@Component
public class ProductCommandHandler {
@Autowired
private EventSourcingRepository<Product> productRepository;
@CommandHandler
public void handle(UpdateProductPriceCommand command) {
Product product = productRepository.load(command.getProductId());
product.changePrice(command.getNewPrice());
productRepository.save(product);
}
}
// Aggregate (Domain Model)
@Aggregate
public class Product {
@AggregateIdentifier
private String id;
private String name;
private BigDecimal price;
@CommandHandler
public void changePrice(BigDecimal newPrice) {
if (newPrice == null || newPrice.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Price cannot be negative.");
}
apply(new PriceChangedEvent(this.id, newPrice));
}
@EventSourcingHandler
public void on(PriceChangedEvent event) {
this.price = event.getNewPrice();
}
// constructor, other handlers
}
// Event
public class PriceChangedEvent {
private String productId;
private BigDecimal newPrice;
// getters and setters
}
Here, the ProductCommandHandler receives the command, loads the Product aggregate (which might replay past events to reconstruct its state), applies the price change logic within the aggregate, and then saves the aggregate. Saving the aggregate publishes the PriceChangedEvent.
Query Side (Reads):
The PriceChangedEvent is then handled by an event processor that updates a read-optimized data store, like a relational database or a document store.
// Event Listener
@Component
public class ProductEventListener {
@Autowired
private ProductReadRepository productReadRepository; // JPA Repository or similar
@EventHandler
public void on(PriceChangedEvent event) {
ProductReadModel productModel = productReadRepository.findById(event.getProductId())
.orElseGet(() -> new ProductReadModel(event.getProductId()));
productModel.setPrice(event.getNewPrice());
productReadRepository.save(productModel);
}
}
// Read Model
@Entity
public class ProductReadModel {
@Id
private String id;
private String name;
private BigDecimal price;
// getters and setters
}
This ProductEventListener subscribes to PriceChangedEvent and updates a denormalized ProductReadModel. When a client requests product information, they query this ProductReadModel directly, which is optimized for read performance.
The core idea is that commands trigger state changes and produce events, while events are consumed asynchronously to update read models. This separation allows you to use different technologies for your write side (e.g., event sourcing with Axon) and your read side (e.g., a highly optimized SQL database for querying). You can scale the command handlers independently of the query projections. For example, if you have a high volume of price updates, you can add more command handler instances. If your product catalog is read heavily, you can optimize the read database and its querying mechanisms.
A common misconception is that CQRS requires event sourcing. While event sourcing is a powerful way to implement the command side, CQRS can be implemented with traditional databases on the write side as well. The key is the separation of concerns between the write and read models, not necessarily the underlying persistence mechanism. The state of your read model is a projection of the events that have occurred. This means that if your read model ever gets corrupted or needs to be rebuilt, you can simply replay all the events from the beginning of time to reconstruct it perfectly.
The next challenge is handling eventual consistency across your read models and ensuring that your read models are always eventually up-to-date with the latest state.