Event Sourcing with Spring Boot and Axon.
The basic idea of event sourcing is to store every change in the state of an application as events instead of only storing the current state. The current state can be constructed by applying all past events. In this blog post I want to give an example on how to implement an event sourcing application with the Axon Framework and Spring Boot.
Axon is a framework that helps developers to create such applications by providing the most important building blocks. With Axon you manipulate domain objects (called aggregate) with commands which will lead to events. A command is an intend to change aggregates. It contains all the necessary information to execute it. Each command is handled by a Command Handler. It verifies the command and executes a method on the aggregate to change its state. A command can also be rejected. The change of the aggregate will lead to events. An event is a change of the aggregate that has already happened. You cannot change the past, therefore you should not validate or reject events. Only the events are persisted in the event store. The state of the aggregate is volatile, but can always be reconstructed from the event store.
To show it by example, let’s think of a simple banking application. It will allow the following actions: You can create a new account, withdraw or deposit money and finally close your account. To keep this example simple we will only withdraw or deposit money from one account and will not transfer money between two accounts. All actions apply to the bank account, therefore this is our aggregate.
Note: You can find the complete source code on GitHub: https://github.com/jd1/spring-boot-axon
Project setup
We will start with a new project from https://start.spring.io/ with two dependencies:
- spring-boot-starter-web
- spring-boot-starter-data-mongodb
Then add the Axon dependencies(axon-spring-boot-starter, axon-mongo) manually to the pom.xml. The pom.xml should look similar to this one:
Aggregates, Commands and Events
With these basic project setup we can start to create our aggregate, commands and events:
To allow Axon to identify the bank account as an aggregate, the class must be annotated with @Aggregate and contain a field with the @AggregateIdentifier annotation. This is the identifier of the aggregate. Our bank account aggregate will look like this:
As mentioned previously, our bank account will support four actions/commands. In general one command can result in one or more events, but to keep it simple each command will only produce one event:
Command | Event | Description |
---|---|---|
CreateAccountCommand | AccountCreatedEvent | Create a new account |
DepositMoneyCommand | MoneyDepositedEvent | Deposit money to an bank account |
WithdrawMoneyCommand | MoneyWithdrawnEvent | Withdraw money from a bank account |
CloseAccountCommand | AccountClosedEvent | Close an existing account |
Sending commands
A command must contain all the information that is necessary for a command handler to execute it, but at least contain the id of the aggregate that should be updated. The CreateAccountCommand contains the account id and the name of the account creator. The annotation @TargetAggregateIdentifier must be placed on a field or method that contains the identifier of the aggregate.
The other three commands look similar:
To make the banking application accessible to the rest of the world I created a REST interface that will create commands and publish them via the CommandGateway to the command bus.
Deposit and withdraw money is nearly the same(only with negative sign), but I separated this in two commands, because the handling will be different.
The exception handler maps a AggregateNotFoundException to an HTTP status code 404 if a command cannot be dispatched to an aggregate. If an account does not contain enough money to perform an operation, an InsufficientBalanceException will be thrown which is also handled.
Reacting to commands
The command bus distributes commands to event handlers. A command handler is a method with the annotation @CommandHandler that accepts one command as a parameter. In this method the command can be validated and an event can be publish as a reaction to the state change(via AggregateLifecycle.apply()). If the @CommandHandler annotation is placed inside an aggregate, the correct aggregate is chosen by the @TargetAggregateIdentifier of the command and the @AggregateIdentifier of the aggregate.
CreateAccountCommand
To create a new aggregate the annotation @CommandHandler can be placed on the constructor:
With this constructor an CreateAccountCommand will create an new instance of BankAccount. The constructor verifies that the command has an id and an account creator. If the command is valid, an AccountCreatedEvent will be published with the account id, owner and an initial balance of 0.
To create event sourced aggregates all changes of the aggregate are described as events and state changes are only performed in @EventSourcingHandler annotated methods. This methods will also be called when the aggregate’s state is reconstructed from the events in the event store.
To react to the AccountCreatedEvent we will add a new method to BankAccount:
DepositMoneyCommand
It is always possible to deposit money to a bank account, therefore no special validation is necessary for the DepositMoneyCommand. It is only verified that the amount of money is positive:
In the event handler the new account balance is calculated:
WithdrawMoneyCommand
It is only possible to withdraw money if there is enough money in the account. Otherwise an InsufficientBalanceException will be thrown:
And similar to the MoneyDepositedEvent the new balance is calculated:
CloseAccountCommand
The CloseAccountCommand is special because it marks the aggregate as delete in the event store. Deleted aggregates still exist, but cannot be modified.
This is the final BankAccount class:
Event Store
There is still one thing left to use our banking application: We have store the events into a database which is called event store. Axon ships with different EventStorageEngines:
- InMemoryEventStorageEngine
- JdbcEventStorageEngine
- JpaEventStorageEngine
- MongoEventStorageEngine
To configure an event store all you have to do is to create a bean of type EventStorageEngine and Axon will use it to store events. JpaEventStorageEngine is even configured automatically when a bean of type EntityManagerFactory is found.
In this example I will use MongoDB as an event store. The connection to the MongoDB is configured in the known spring way: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-nosql.html#boot-features-connecting-to-mongodb Spring provides a MongoClient which then can be used to create a MongoEventStorageEngine:
Calling the API
With the database configured we are now able to call our REST API to verify that the application works as expected. First, let’s create a new account:
The response is the account id, e.g. a9fb4b34-1852-4a9a-b81b-1f0d144c67fa.
With the new account you can deposit money:
and withdraw money:
Both requests will only return HTTP Status 202 and no body. At the moment there is no way to request the current balance of an account.
It is also possible to verify that you cannot overdraw the account:
This will result in error with the message: Insufficient balance.
And finally you can close the bank account:
Each further call for this account will be answered with HTTP status 404.
Conclusion
Finally, I can say that it is really easy and fun to create event sourcing applications with Axon. To start with a simple application you have to create aggregates, commands and events and bring them together. You can focus on the business code and Axon will handle the technical details. In combination with Spring Boot it is even easier because the Axon Spring Boot Starter configures the infrastructure and you can use it in a familiar environment with Spring Boot.
Reference:
Comments
Post a Comment