Implementing Event Sourcing With Axon and Spring Boot (Part 2)
Learn more about event sourcing with the Axon and Spring Boot frameworks.
Join the DZone community and get the full member experience.
Join For FreeIn the previous post, we understood the concept behind event sourcing. Now, we will start implementing event sourcing with Axon and Spring Boot.
We will create a simple Spring Boot project using https://start.spring.io. If you aren't sure on how to set up a project using Spring Initializer, you can refer to this post.
The POM Dependencies
We will use Maven as our build and dependency management tool. Some of the major dependencies are as follows:
- Spring Boot Starter Web — This brings in support for web application capabilities and Spring Dispatcher Servlet. It also brings in Tomcat dependencies to run your application.
- Spring Boot Starter Data JPA — This brings in support for JPA and Hibernate. Basically, we need these two dependencies to connect to a database.
- H2 Database — This is for bringing in H2 in-memory database support.
- Spring Boot Starter Actuator — This enables Spring Boot actuator endpoints. Basically, these endpoints allow us to ask questions about our application. Also, you can get other run-time statistics.
- Axon Spring Boot Starter — This is a very important dependency for our example. It basically brings in support for Axon Framework along with all the annotations.
- Springfox Swagger2 and Springfox Swagger UI — We will be using Swagger for documenting our API end-points. Also, Swagger will provide a neat user interface to test our APIs. These dependencies will help us enable Swagger for our Spring Boot application.
Below is how our POM.xml looks like:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<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>3.2</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Configuring the Application
With Axon Spring Boot Starter, you don't need a ton of configuration. Basically, the starter packages do most of the heavy-lifting in terms of creating the necessary beans.
However, the only bare minimum configuration required would be setting up the H2 database. In other words, we need to enable the console view. In order to do so, add the below statements in the application.properties file.
#H2 settings
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
If you need more details about setting up H2 with Spring Boot, read this detailed post on the topic.
Creating the Event Sourced Entity
We will model our Accounting example in this sample app. Therefore, we will create an Account entity. Basically, this entity will act as our use-case to demonstrate Event Sourcing.
See the entity definition in Java as below:
@Aggregate
public class AccountAggregate {
@AggregateIdentifier
private String id;
private double accountBalance;
private String currency;
private String status;
}
Important things to note here are the two annotations.
-
@Aggregate
annotation tells Axon that this entity will be managed by Axon. Basically, this is similar to the@Entity
annotation available with JPA. However, we will be using the Axon recommended annotation. -
@AggregateIdentifier
annotation is used for identifying a particular instance of the Aggregate. In other words, this is similar to JPA's@Id
annotation.
Modeling the Commands and Events
Axon works on the concept of commands and events. To elaborate, commands are user-initiated actions that can change the state of your aggregate. However, events are the actual changing of that state.
Now, considering our Account aggregate, there could be many commands and events possible. However, we will try and model some important ones.
The primary commands would be Create Account Command, Credit Money Command, and Debit Money Command. Based on them, the corresponding events that can occur are Account Created Event, Money Credited Event, and Money Debited Event. However, there could be a couple of more events. For instance, one of them is the Account Activated Event. Also, there is the Account Held Event.
Let's model them in our application. First off, we will create a Base Command and a Base Event.
public class BaseCommand<T> {
@TargetAggregateIdentifier
public final T id;
public BaseCommand(T id) {
this.id = id;
}
}
public class BaseEvent<T> {
public final T id;
public BaseEvent(T id) {
this.id = id;
}
}
We have used Java Generics here. Basically, this makes our id field flexible across different classes that extend these classes.
However, the most important thing to note here is the @TargetAggregateIdentifier
annotation. Basically, this is an Axon specific requirement to identify the aggregate instance. In other words, this annotation is required for Axon to determine the instance of the Aggregate that should handle the command. The annotation can be placed on either the field or the getter method. In this example, we chose to put it on the field.
Now, we implement the other commands.
Create Account Command
public class CreateAccountCommand extends BaseCommand<String> {
public final double accountBalance;
public final String currency;
public CreateAccountCommand(String id, double accountBalance, String currency) {
super(id);
this.accountBalance = accountBalance;
this.currency = currency;
}
}
Credit Money Command
public class CreditMoneyCommand extends BaseCommand<String> {
public final double creditAmount;
public final String currency;
public CreditMoneyCommand(String id, double creditAmount, String currency) {
super(id);
this.creditAmount = creditAmount;
this.currency = currency;
}
}
Debit Money Command
public class DebitMoneyCommand extends BaseCommand<String> {
public final double debitAmount;
public final String currency;
public DebitMoneyCommand(String id, double debitAmount, String currency) {
super(id);
this.debitAmount = debitAmount;
this.currency = currency;
}
}
Note that all the above commands extend the Base Command. Moreover, they supply the Generic type for the id field as String.
The next step is to implement the events.
Account Created Event
public class AccountCreatedEvent extends BaseEvent<String> {
public final double accountBalance;
public final String currency;
public AccountCreatedEvent(String id, double accountBalance, String currency) {
super(id);
this.accountBalance = accountBalance;
this.currency = currency;
}
}
Money Credited Event
public class MoneyCreditedEvent extends BaseEvent<String> {
public final double creditAmount;
public final String currency;
public MoneyCreditedEvent(String id, double creditAmount, String currency) {
super(id);
this.creditAmount = creditAmount;
this.currency = currency;
}
}
Money Debited Event
public class MoneyDebitedEvent extends BaseEvent<String> {
public final double debitAmount;
public final String currency;
public MoneyDebitedEvent(String id, double debitAmount, String currency) {
super(id);
this.debitAmount = debitAmount;
this.currency = currency;
}
}
Account Activated Event
public class AccountActivatedEvent extends BaseEvent<String> {
public final Status status;
public AccountActivatedEvent(String id, Status status) {
super(id);
this.status = status;
}
}
Account Held Event
public class AccountHeldEvent extends BaseEvent<String> {
public final Status status;
public AccountHeldEvent(String id, Status status) {
super(id);
this.status = status;
}
}
Command Handlers and Event Handlers
Now that we successfully modeled the commands and events, we can implement handlers for them. Basically, handlers are methods on the Aggregate that should be invoked for a particular command or an event.
Due to their relation to the Aggregate, it is recommended to define the handlers in the Aggregate class itself. Also, the command handlers often need to access the state of the Aggregate.
In our case, we will define them in the AccountAggregate
class. See below for the complete AccountAggregate
class implementation.
Aggregate
public class AccountAggregate {
@AggregateIdentifier
private String id;
private double accountBalance;
private String currency;
private String status;
public AccountAggregate() {
}
@CommandHandler
public AccountAggregate(CreateAccountCommand createAccountCommand){
AggregateLifecycle.apply(new AccountCreatedEvent(createAccountCommand.id, createAccountCommand.accountBalance, createAccountCommand.currency));
}
@EventSourcingHandler
protected void on(AccountCreatedEvent accountCreatedEvent){
this.id = accountCreatedEvent.id;
this.accountBalance = accountCreatedEvent.accountBalance;
this.currency = accountCreatedEvent.currency;
this.status = String.valueOf(Status.CREATED);
AggregateLifecycle.apply(new AccountActivatedEvent(this.id, Status.ACTIVATED));
}
@EventSourcingHandler
protected void on(AccountActivatedEvent accountActivatedEvent){
this.status = String.valueOf(accountActivatedEvent.status);
}
@CommandHandler
protected void on(CreditMoneyCommand creditMoneyCommand){
AggregateLifecycle.apply(new MoneyCreditedEvent(creditMoneyCommand.id, creditMoneyCommand.creditAmount, creditMoneyCommand.currency));
}
@EventSourcingHandler
protected void on(MoneyCreditedEvent moneyCreditedEvent){
if (this.accountBalance < 0 & (this.accountBalance + moneyCreditedEvent.creditAmount) >= 0){
AggregateLifecycle.apply(new AccountActivatedEvent(this.id, Status.ACTIVATED));
}
this.accountBalance += moneyCreditedEvent.creditAmount;
}
@CommandHandler
protected void on(DebitMoneyCommand debitMoneyCommand){
AggregateLifecycle.apply(new MoneyDebitedEvent(debitMoneyCommand.id, debitMoneyCommand.debitAmount, debitMoneyCommand.currency));
}
@EventSourcingHandler
protected void on(MoneyDebitedEvent moneyDebitedEvent){
if (this.accountBalance >= 0 & (this.accountBalance - moneyDebitedEvent.debitAmount) < 0){
AggregateLifecycle.apply(new AccountHeldEvent(this.id, Status.HOLD));
}
this.accountBalance -= moneyDebitedEvent.debitAmount;
}
@EventSourcingHandler
protected void on(AccountHeldEvent accountHeldEvent){
this.status = String.valueOf(accountHeldEvent.status);
}
}
As you can see, we are handling the three commands in their own handler methods. These handler methods should be annotated with the @CommandHandler
annotation. We have three handler methods because there are three commands we want to handle.
The handler methods use the AggregateLifecyle.apply()
method to register events.
These events, in turn, are handled by methods annotated with @EventSourcingHandler
annotation. Also, it is imperative that all state changes in an event sourced aggregate should be performed in these methods.
Another important point to keep in mind is that the Aggregate Identifier must be set in the first method annotated with @EventSourcingHandler
. In other words, this will be the creation Event.
In our example, this is evident in the below method.
@EventSourcingHandler
protected void on(AccountCreatedEvent accountCreatedEvent){
this.id = accountCreatedEvent.id;
this.accountBalance = accountCreatedEvent.accountBalance;
this.currency = accountCreatedEvent.currency;
this.status = String.valueOf(Status.CREATED);
AggregateLifecycle.apply(new AccountActivatedEvent(this.id, Status.ACTIVATED));
}
Other events are handled in other methods. All of such methods are annotated with @EventSourcingHandler
.
Another important thing to point out here is the no-args default constructor. You need to declare such a constructor because the Axon framework needs it. Basically, using this constructor, Axon creates an empty instance of the aggregate. Then, it applies the events. If this constructor is not present, it will result in an exception.
The Next Step
At this point, we have implemented the bulk of the event sourcing part. However, we still don't have a solid way of testing our application. To do so, we would like to implement RESTful interfaces. Basically, these interfaces should allow us to create a bank account and perform other operations.
However, this post has become quite long. So we will tackle that part in the next post.
Published at DZone with permission of Saurabh Dashora. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments