In 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 setup 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 to 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 for to test our APIs. These dependencies will help us enable Swagger for our Spring Boot application.
Below is how our POM.xml looks like:
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | <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 does 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:
1 2 3 4 5 6 7 8 9 10 11 12 | @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 @Entity annotation available with JPA. However, we will be using the Axon recommended annotation.
- @AggregateIdentifier annotation is used for the 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, the Account Held Event.
Let’s model them in our application. First off, we will create a Base Command and a Base Event.
1 2 3 4 5 6 7 8 9 | public class BaseCommand<T> { @TargetAggregateIdentifier public final T id; public BaseCommand(T id) { this.id = id; } } |
1 2 3 4 5 6 7 8 | 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
1 2 3 4 5 6 7 8 9 10 11 12 | 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
1 2 3 4 5 6 7 8 9 10 11 12 | 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
1 2 3 4 5 6 7 8 9 10 11 12 | 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.
Next step is to implements the events.
Account Created Event
1 2 3 4 5 6 7 8 9 10 11 12 | 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
1 2 3 4 5 6 7 8 9 10 11 12 | 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
1 2 3 4 5 6 7 8 9 10 11 12 | 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
1 2 3 4 5 6 7 8 9 | public class AccountActivatedEvent extends BaseEvent<String> { public final Status status; public AccountActivatedEvent(String id, Status status) { super(id); this.status = status; } } |
Account Held Event
1 2 3 4 5 6 7 8 9 | 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 implements 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.
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | @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 @CommandHandler annotation. We have three handler methods because there are three commands we want to handle.
The handler methods use 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.
1 2 3 4 5 6 7 8 9 | @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 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 the bank account and perform other operations.
However, this post has become quite long. So we will tackle that part in the next post.
4 Comments
zhangpingzuan · June 22, 2019 at 9:28 am
I am confused that where I should put my business logic (https://docs.axoniq.io/reference-guide/implementing-domain-logic/command-handling/aggregate), it seems that I should not change state in @CommandHandler annotated function if answer is @CommandHandler annotated function.
Saurabh Dashora · June 23, 2019 at 5:07 am
Hi,
As per Axon Documentation, you should put your business logic in the @CommandHandler annotated method. However, I agree that you should probably avoid it since you might choose to refactor your solution in the future where you don’t use Axon. I would suggest you put the business logic in Service layer of your application and from there you issue commands using CommandGateway. This way your business logic stays in more of the Spring-managed classes of your application.
OUNIS Mohamed · July 29, 2019 at 10:29 am
Hi, i’m trying to implement this but I’m having trouble determining if the Status type you’ve been using is a custom enumerate type or belongs to a specific library.
Saurabh Dashora · July 29, 2019 at 3:33 pm
Hi,
It is a custom enumeration class.
You can find it over here