I’ve been building software systems for years, and I keep running into the same challenge: how to handle complex business logic while keeping applications scalable and maintainable. The traditional approach of using a single model for both reading and writing data often leads to performance bottlenecks and tangled code. This frustration led me to explore CQRS and Event Sourcing, and I want to share how you can implement these patterns using Spring Boot and Axon Framework.
CQRS stands for Command Query Responsibility Segregation. It’s a simple but powerful idea: separate the operations that change data (commands) from those that read data (queries). Why would you do this? Because reads and writes often have very different requirements. Writes need to enforce business rules and maintain consistency, while reads need to be fast and optimized for specific queries. Have you ever noticed how your database queries slow down when there are lots of updates happening? CQRS solves this by giving each side its own dedicated model.
Event Sourcing takes this a step further. Instead of storing just the current state of your data, you store every change as an immutable event. Think about a bank account – rather than storing just the current balance, you store every deposit and withdrawal that ever happened. This gives you a complete history of changes, which is incredibly valuable for auditing, debugging, and even implementing features like “undo” operations.
When should you use these patterns? They work best in systems with complex business rules, where you need strong audit trails, or where read and write workloads scale differently. If you’re building a simple CRUD application, they might be overkill. But for domains like financial systems, e-commerce platforms, or any system where data history matters, they can be game-changing.
Let’s get our hands dirty with some code. First, we’ll set up a Spring Boot project with the necessary dependencies. Here’s what your Maven configuration might look like:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-spring-boot-starter</artifactId>
<version>4.8.3</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
And here’s a basic application configuration:
spring:
datasource:
url: jdbc:h2:mem:testdb
jpa:
hibernate:
ddl-auto: create-drop
axon:
eventhandling:
processors:
account-projection:
mode: subscribing
Now, let’s model our domain using a banking example. Commands represent actions we want to perform:
public class CreateAccountCommand {
@TargetAggregateIdentifier
private final String accountId;
private final String accountHolderName;
public CreateAccountCommand(String accountId, String accountHolderName) {
this.accountId = accountId;
this.accountHolderName = accountHolderName;
}
// getters omitted for brevity
}
Events represent things that have already happened:
public class AccountCreatedEvent {
private final String accountId;
private final String accountHolderName;
public AccountCreatedEvent(String accountId, String accountHolderName) {
this.accountId = accountId;
this.accountHolderName = accountHolderName;
}
// getters
}
But how do we connect commands to events? That’s where aggregates come in. An aggregate is responsible for handling commands and producing events. Here’s a simple account aggregate:
@Aggregate
public class BankAccount {
@AggregateIdentifier
private String accountId;
private String accountHolderName;
private BigDecimal balance;
public BankAccount() {
// Axon requires a no-arg constructor
}
@CommandHandler
public BankAccount(CreateAccountCommand command) {
apply(new AccountCreatedEvent(command.getAccountId(),
command.getAccountHolderName()));
}
@EventSourcingHandler
public void on(AccountCreatedEvent event) {
this.accountId = event.getAccountId();
this.accountHolderName = event.getAccountHolderName();
this.balance = BigDecimal.ZERO;
}
}
On the query side, we need to build projections that update read models when events occur. This is where you can optimize for specific queries without worrying about write consistency. For example, you might have a separate database table just for account summaries:
@Component
public class AccountProjection {
private final JdbcTemplate jdbcTemplate;
public AccountProjection(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@EventHandler
public void on(AccountCreatedEvent event) {
String sql = "INSERT INTO account_summary (account_id, holder_name, balance) VALUES (?, ?, 0)";
jdbcTemplate.update(sql, event.getAccountId(), event.getAccountHolderName());
}
}
What about complex business processes that span multiple aggregates? That’s where sagas come in. Sagas help you manage long-running transactions by reacting to events and sending new commands. Imagine a money transfer between accounts – it involves multiple steps and needs to handle failures gracefully.
Testing is crucial in event-sourced systems. Axon provides excellent test support:
@SpringBootTest
class BankAccountTest {
@Autowired
private CommandGateway commandGateway;
@Test
void testCreateAccount() {
String accountId = "acc123";
commandGateway.sendAndWait(new CreateAccountCommand(accountId, "John Doe"));
// Verify events were stored and projections updated
}
}
One common concern with CQRS is eventual consistency. Since reads and writes are separated, there might be a small delay before a write appears in read models. In practice, this is usually acceptable for most business scenarios. The benefits in scalability and maintainability often outweigh this temporary inconsistency.
I’ve found that starting with a simple implementation and gradually adding complexity works best. Don’t try to implement every feature at once. Begin with basic command and event handling, then add projections, and finally introduce sagas for more complex workflows.
Remember that events are immutable facts. Once stored, they should never be changed. This immutability is what gives you the audit trail and the ability to replay history. Have you considered how valuable it would be to reconstruct your system’s state from any point in time?
As you scale, you might want to use different databases for your command and query sides. The write side could use an event store optimized for append-only operations, while the read side might use a relational database or even a search engine for complex queries.
What surprised me most when I started using these patterns was how much cleaner my code became. Business logic is concentrated in the command handlers, while query logic is separated and optimized. Debugging becomes easier because you can see exactly what events led to the current state.
I hope this guide gives you a solid foundation for implementing CQRS and Event Sourcing in your projects. The combination of Spring Boot and Axon Framework makes it surprisingly approachable. Start small, experiment, and you’ll soon see the benefits in your own systems.
If this article helped you understand these concepts better, I’d love to hear about your experiences. Please share your thoughts in the comments, and if you found it valuable, consider liking and sharing it with others who might benefit. Your feedback helps me create better content for our community.