In the previous post, we understood the concept of Event Sourcing CQRS. Also, we made a plan for building a sample application using these concepts. We also discussed the tech stack for building such an application. Now, we need to implement Event Sourcing with Spring Boot Axon.

Below is our high-level plan for this series of posts.

In Part 1, we discuss the concepts of Event Sourcing and CQRS. Also, we talk about the example application we will be building. We also describe the tech stack we will be using for the same.

In Part 2 (this part), we implement the Account Service using Event Sourcing with Spring Boot Axon integration.

Next, in Part 3, we implement the CQRS part of our application.

In Part 4, we implement the event publisher and subscriber aspect of our application plan We also test our overall application.

1. Recap of the Application Design

Below is the illustration describing our application and the various components that will be a part of it.

event sourcing with spring boot axon

In a nutshell, we have a Customer Service, Account Service and a Statement Service.

We will now start working on the Account Service. This service will basically utilize Event Sourcing. We will be using Spring Boot and Axon Framework to build this.

2. Event Sourcing Spring Boot Axon Dependencies

Below are the dependencies for the Account Service in the POM.xml file.

<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>4.0.3</version>
			<exclusions>
				<exclusion>
					<groupId>org.axonframework</groupId>
					<artifactId>axon-server-connector</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<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>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
</dependencies>

Below is the description for these dependencies:

  • Spring Boot Starter Web – This package enables support for web applications. Basically, it brings in Spring Dispatcher Servlet as well as Tomcat to run our application.
  • Spring Boot Starter Data JPA – This package brings in support for JPA and Hibernate.
  • H2 Database – We will use H2 in-memory database as our event store in this case. Therefore, we also include that as one of the dependencies.
  • Axon Framework – As mentioned earlier, we use Axon to manage our event store and provide an easy-to-use implementation of Event Sourcing. Therefore, we also include the Axon Spring Boot Starter package. Basically, this package enables easy integration between Axon and Spring Boot. Note that we have excluded Axon Connector. Basically, this is to prevent our application from trying to connect to Axon Server. We will not be using Axon Server in our application.
  • Swagger – Lastly, we also include Swagger dependencies. If you are not aware, Swagger Spring Boot package provides automatic Swagger generation of our API end-points.

3. The Account Aggregate

The next step for us is to create the Account Aggregate. Basically, this will an entity that will be managed using Event Sourcing concepts.

@Aggregate
public class Account {

    @AggregateIdentifier
    private String id;

    private BigDecimal accountBalance;

    private String currency;

    private String status;
}

Note the annotations @Aggregate and @AggregateIdentifier. The @Aggregate annotation basically tells Axon Spring Boot Starter to manage this entity. @AggregateIdentifier specifies the key for the entity.

4. The Commands

In Axon framework, Commands are user-initiated actions. Basically, these actions initiate the process for changing the state of the entity.

Considering the Account Aggregate, there may be many commands possible. The primary commands we will implement are Create Account Command, Credit Money Command, Debit Money Command.

To have a uniformity across these commands, we first implement a high-level Base Command.

import org.axonframework.modelling.command.TargetAggregateIdentifier;

public class BaseCommand<T> {

    @TargetAggregateIdentifier
    public final T id;

    public BaseCommand(T id) {
        this.id = id;
    }
}

Here, we use the annotation @TargetAggregateIdentifier to mark the identifier. Basically, this identifier will be used by Axon to target the correct instance of the Aggregate.

Basically, all other commands will extend the BaseCommand class.

Create Account Command

import java.math.BigDecimal;

public class CreateAccountCommand extends BaseCommand<String> {

    public final BigDecimal accountBalance;

    public final String currency;

    public CreateAccountCommand(String id, BigDecimal accountBalance, String currency) {
        super(id);
        this.accountBalance = accountBalance;
        this.currency = currency;
    }
}

Credit Money Command

import java.math.BigDecimal;

public class CreditMoneyCommand extends BaseCommand<String> {

    public final BigDecimal creditAmount;

    public final String currency;

    public CreditMoneyCommand(String id, BigDecimal creditAmount, String currency) {
        super(id);
        this.creditAmount = creditAmount;
        this.currency = currency;
    }
}

Debit Money Command

import java.math.BigDecimal;

public class DebitMoneyCommand extends BaseCommand<String> {

    public final BigDecimal debitAmount;

    public final String currency;

    public DebitMoneyCommand(String id, BigDecimal debitAmount, String currency) {
        super(id);
        this.debitAmount = debitAmount;
        this.currency = currency;
    }
}

5. The Events

In Axon Framework, commands initiate the process to change the state. However, events apply the actual state changes on the correct Aggregate instance.

Some of the events we can model for the Account Aggregate are Account Created Event, Money Credited Event, Money Debited Event, Account Activated Event, Account Held Event.

Also, we will use a Base Event class to model other events.

public class BaseEvent<T> {

    public final T id;

    public BaseEvent(T id) {
        this.id = id;
    }
}

The other events are as follows:

Account Created Event

import java.math.BigDecimal;

public class AccountCreatedEvent extends BaseEvent<String> {

    public final BigDecimal accountBalance;

    public final String currency;

    public AccountCreatedEvent(String id, BigDecimal accountBalance, String currency) {
        super(id);
        this.accountBalance = accountBalance;
        this.currency = currency;
    }
}

Money Credited Event

import java.math.BigDecimal;

public class MoneyCreditedEvent extends BaseEvent<String> {

    public final BigDecimal creditAmount;

    public final String currency;

    public MoneyCreditedEvent(String id, BigDecimal creditAmount, String currency) {
        super(id);
        this.creditAmount = creditAmount;
        this.currency = currency;
    }
}

Money Debited Event

import java.math.BigDecimal;

public class MoneyDebitedEvent extends BaseEvent<String> {

    public final BigDecimal debitAmount;

    public final String currency;

    public MoneyDebitedEvent(String id, BigDecimal debitAmount, String currency) {
        super(id);
        this.debitAmount = debitAmount;
        this.currency = currency;
    }
}

Account Activated Event

public class AccountActivatedEvent extends BaseEvent<String> {

    public final String status;

    public AccountActivatedEvent(String id, String status) {
        super(id);
        this.status = status;
    }
}

Account Held Event

public class AccountHeldEvent extends BaseEvent<String> {

    public final String status;

    public AccountHeldEvent(String id, String status) {
        super(id);
        this.status = status;
    }
}

6. Command and Event Handlers

Next we need to provide handlers for these commands and events. Basically, these handlers execute stuff based on the required logic. Since these commands and events occur on the Account Aggregate, we place the handlers in the Aggregate class.

See below for the complete Account Aggregate class along with the handlers.

import com.progressivecoder.accountsservice.commands.CreateAccountCommand;
import com.progressivecoder.accountsservice.commands.CreditMoneyCommand;
import com.progressivecoder.accountsservice.commands.DebitMoneyCommand;
import com.progressivecoder.accountsservice.events.*;
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.eventsourcing.EventSourcingHandler;
import org.axonframework.modelling.command.AggregateIdentifier;
import org.axonframework.modelling.command.AggregateLifecycle;
import org.axonframework.spring.stereotype.Aggregate;

import java.math.BigDecimal;

@Aggregate
public class Account {

    @AggregateIdentifier
    private String id;

    private BigDecimal accountBalance;

    private String currency;

    private String status;

    public Account() {
    }

    @CommandHandler
    public Account(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 = "CREATED";
    }

    @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.doubleValue() < 0 & (this.accountBalance.doubleValue() + moneyCreditedEvent.creditAmount.doubleValue()) >= 0){
            AggregateLifecycle.apply(new AccountActivatedEvent(this.id, "ACTIVATED"));
        }

        this.accountBalance = BigDecimal.valueOf(this.accountBalance.doubleValue() + moneyCreditedEvent.creditAmount.doubleValue());
    }

    @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.doubleValue() >= 0 & (this.accountBalance.doubleValue() - moneyDebitedEvent.debitAmount.doubleValue()) < 0){
            AggregateLifecycle.apply(new AccountHeldEvent(this.id, "HOLD"));
        }

        this.accountBalance = BigDecimal.valueOf(this.accountBalance.doubleValue() - moneyDebitedEvent.debitAmount.doubleValue());

    }

    @EventSourcingHandler
    protected void on(AccountHeldEvent accountHeldEvent){
        this.status = String.valueOf(accountHeldEvent.status);
    }

}

As you can see in the above snippet, we use the annotations @CommandHandler and @EventSourcingHandler. Basically, the @CommandHandler annotation handles the command used as an argument and generates an Event out of it. Also, it should be noted that the command handler use AggregateLifeCycle.apply() method to generate the events.

Then, the @EventSourcingHandler annotation handles the event and changes the state of the Aggregate instance.

Another important thing to point out here is the no-args default constructor. We 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. In other words, if this constructor is not present, it will result in an exception.

7. The Service Layer

Next, we define a Service layer. Basically, this layer connects our Aggregate to the incoming requests. Following the principle of working on interfaces, we first define the interface for the Service class as follows.

import com.progressivecoder.accountsservice.dto.AccountCreateDTO;
import com.progressivecoder.accountsservice.dto.MoneyCreditDTO;
import com.progressivecoder.accountsservice.dto.MoneyDebitDTO;

import java.util.concurrent.CompletableFuture;

public interface AccountCommandService {

    public CompletableFuture<String> createAccount(AccountCreateDTO accountCreateDTO);
    public CompletableFuture<String> creditMoneyToAccount(String accountNumber, MoneyCreditDTO moneyCreditDTO);
    public CompletableFuture<String> debitMoneyFromAccount(String accountNumber, MoneyDebitDTO moneyDebitDTO);
}

Then, we provide the implementation of the service interface.

import com.progressivecoder.accountsservice.commands.CreateAccountCommand;
import com.progressivecoder.accountsservice.commands.CreditMoneyCommand;
import com.progressivecoder.accountsservice.commands.DebitMoneyCommand;
import com.progressivecoder.accountsservice.dto.AccountCreateDTO;
import com.progressivecoder.accountsservice.dto.MoneyCreditDTO;
import com.progressivecoder.accountsservice.dto.MoneyDebitDTO;
import org.axonframework.commandhandling.gateway.CommandGateway;
import org.springframework.stereotype.Service;

import java.util.UUID;
import java.util.concurrent.CompletableFuture;

@Service
public class AccountCommandServiceImpl implements AccountCommandService {

    private final CommandGateway commandGateway;

    public AccountCommandServiceImpl(CommandGateway commandGateway) {
        this.commandGateway = commandGateway;
    }

    @Override
    public CompletableFuture<String> createAccount(AccountCreateDTO accountCreateDTO) {
        return commandGateway.send(new CreateAccountCommand(UUID.randomUUID().toString(), accountCreateDTO.getStartingBalance(), accountCreateDTO.getCurrency()));
    }

    @Override
    public CompletableFuture<String> creditMoneyToAccount(String accountNumber, MoneyCreditDTO moneyCreditDTO) {
        return commandGateway.send(new CreditMoneyCommand(accountNumber, moneyCreditDTO.getCreditAmount(), moneyCreditDTO.getCurrency()));
    }

    @Override
    public CompletableFuture<String> debitMoneyFromAccount(String accountNumber, MoneyDebitDTO moneyDebitDTO) {
        return commandGateway.send(new DebitMoneyCommand(accountNumber, moneyDebitDTO.getDebitAmount(), moneyDebitDTO.getCurrency()));
    }
}

The important thing to note here is the Command Gateway. Basically, this is an interface provided by Axon and it can be used to dispatch commands.

Once the command is dispatched, we have to wait for the response and then return it back to the calling class.

8. The Data Transfer Objects

The service we implemented in previous section, receives DTO instances. Basically, DTO stands for Data Transfer Objects. In other words, each Data Transfer Object represents a particular action the user can perform on our entity.

We implement three DTO classes as follows:

Account Create DTO

import java.math.BigDecimal;

public class AccountCreateDTO {

    private BigDecimal startingBalance;

    private String currency;

    public BigDecimal getStartingBalance() {
        return startingBalance;
    }

    public void setStartingBalance(BigDecimal startingBalance) {
        this.startingBalance = startingBalance;
    }

    public String getCurrency() {
        return currency;
    }

    public void setCurrency(String currency) {
        this.currency = currency;
    }
}

Money Credit DTO

import java.math.BigDecimal;

public class MoneyCreditDTO {

    private BigDecimal creditAmount;

    private String currency;

    public BigDecimal getCreditAmount() {
        return creditAmount;
    }

    public void setCreditAmount(BigDecimal creditAmount) {
        this.creditAmount = creditAmount;
    }

    public String getCurrency() {
        return currency;
    }

    public void setCurrency(String currency) {
        this.currency = currency;
    }
}

Money Debit DTO

import java.math.BigDecimal;

public class MoneyDebitDTO {

    private BigDecimal debitAmount;

    private String currency;

    public BigDecimal getDebitAmount() {
        return debitAmount;
    }

    public void setDebitAmount(BigDecimal debitAmount) {
        this.debitAmount = debitAmount;
    }

    public String getCurrency() {
        return currency;
    }

    public void setCurrency(String currency) {
        this.currency = currency;
    }
}

9. The REST Controller

Lastly, we also implement the REST controller. Basically, this controller provides a bunch of end-points that can be used by clients to interact with the Account Service.

For our example, we implement a few common methods such as POST and PUT. In other words, the command related methods.

import com.progressivecoder.accountsservice.dto.AccountCreateDTO;
import com.progressivecoder.accountsservice.dto.MoneyCreditDTO;
import com.progressivecoder.accountsservice.dto.MoneyDebitDTO;
import com.progressivecoder.accountsservice.services.AccountCommandService;
import org.springframework.web.bind.annotation.*;

import java.util.concurrent.CompletableFuture;

@RestController
@RequestMapping(value = "/bank-accounts")
public class AccountCommandController {

    private final AccountCommandService accountCommandService;

    public AccountCommandController(AccountCommandService accountCommandService) {
        this.accountCommandService = accountCommandService;
    }

    @PostMapping
    public CompletableFuture<String> createAccount(@RequestBody AccountCreateDTO accountCreateDTO){
        return accountCommandService.createAccount(accountCreateDTO);
    }

    @PutMapping(value = "/credits/{accountNumber}")
    public CompletableFuture<String> creditMoneyToAccount(@PathVariable(value = "accountNumber") String accountNumber,
                                                          @RequestBody MoneyCreditDTO moneyCreditDTO){
        return accountCommandService.creditMoneyToAccount(accountNumber, moneyCreditDTO);
    }

    @PutMapping(value = "/debits/{accountNumber}")
    public CompletableFuture<String> debitMoneyFromAccount(@PathVariable(value = "accountNumber") String accountNumber,
                                                           @RequestBody MoneyDebitDTO moneyDebitDTO){
        return accountCommandService.debitMoneyFromAccount(accountNumber, moneyDebitDTO);
    }
}

If you see in the above snippet, we have some POST method. Basically, this method creates a new Account.

We also have two PUT methods. One credits money into an existing account. The other debits money from an existing account.

10. The Configuration

We also need a couple of configuration items to make our application work.

The first one is related to the H2 database that we are using as our event store. Basically, we want to enable the h2 console view so that we can check the tables that get created when our application runs.

To enable the same, we can simply add the below properties in the application.properties file.

#H2 settings
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

The next configuration is related to Swagger if we want to use it. Basically, to enable Swagger, we just need to add a configuration class as below.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any())
                .build();
    }
}

In other words, we are simply providing an implementation of the Docket Bean that configures Swagger for the application.

With this, our Account Service is ready with its core logic. Basically, this application is setup to perform Event Sourcing for the Account Aggregate.

However, it still cannot publish its messages to other services through a message broker. Those parts will be implemented in a later post.

At this point, the application will work as a standalone application. We can run it using the maven command ‘clean package spring-boot:run‘.

Conclusion

With this, we have successfully implemented Event Sourcing with Spring Boot Axon integration.

However, we have not tested the service as yet. We will do it as part of the complete application.

In the next post, we will be implementing the Customer Service. We will also implement the event publish logic in both the Customer Service and the Account Service using Spring Cloud Stream and RabbitMQ.


Saurabh Dashora

Saurabh is a Software Architect with over 12 years of experience. He has worked on large-scale distributed systems across various domains and organizations. He is also a passionate Technical Writer and loves sharing knowledge in the community.

27 Comments

User · October 4, 2019 at 10:57 am

Hi, great contents, can’t wait for parts 3 and 4 to be posted. 🙂

POUTIEU · October 30, 2019 at 1:44 am

Hello,

Congratulations to have such an interesting and informative articles on your blog. Lokingforward for the next part on the article.

With regards

    Saurabh Dashora · November 4, 2019 at 11:47 am

    Hi Poutieu,

    Thanks for the feedback!

Paulvy · October 30, 2019 at 1:47 pm

Nice explanation ,Thank you.Waiting for part 3

    Saurabh Dashora · November 4, 2019 at 11:44 am

    Thanks Paulvy!

    And yes, I’m still working on Part 3

Suruj · December 1, 2019 at 2:55 pm

Nice series regarding ES CQRS Saurabh!!!

When could we expect the third one?

    Saurabh Dashora · December 2, 2019 at 11:03 am

    Hi Suruj,

    Thanks for the feedback!

    The third one is in progress and I will publish it soon.

William T. · December 2, 2019 at 8:04 pm

Very nice series. Help me a lot ! Waiting for the next parts. will save my life 🙂

Shambhuling · December 26, 2019 at 9:33 am

You are simply amazing.!
post next parts soon.

    Saurabh Dashora · December 27, 2019 at 4:27 am

    Thanks for the feedback!
    WIP for the next posts. Hope to get them out soon.

hari · February 19, 2020 at 12:10 pm

Nice explanation ,Thank you.Waiting for part 3

Sreehari · March 2, 2020 at 6:46 am

Nice series regarding ES CQRS Saurabh!!!
When could we expect the third one?

Tarkik Shah · April 8, 2020 at 10:32 pm

This is a showcase of the excellent tutorial. It makes complete sense for the framework and its usage in practical.
Waiting for the next parts for the complete solution.

    Saurabh Dashora · April 13, 2020 at 3:40 am

    Thanks Tarkik!

Marcelo · April 21, 2020 at 2:42 pm

Thanks for the nice tutorial. Do you have some news about the final part?

    Saurabh Dashora · April 28, 2020 at 3:11 am

    Thanks Marcelo for your kind words. And yes, I’m working on the final part.

bamk · April 28, 2020 at 1:47 am

Hi,
Thanks a lot for this very helpful tutorial. I am waiting for the next part.

    Saurabh Dashora · April 28, 2020 at 3:04 am

    Hi,

    Thanks for the great feedback!

Billy · July 12, 2020 at 8:48 am

Hi, where i can find part 3 of this post? thanks.

    Saurabh Dashora · July 13, 2020 at 2:23 am

    Hi Billy, that’s in progress!

Rafael · July 26, 2020 at 7:38 pm

Awesome your presentation, I’m excited to know the integration with rabbitMQ. 🙂

    Saurabh Dashora · July 27, 2020 at 2:24 am

    Thanks for the great feedback Rafael!

      karthik · September 30, 2020 at 11:05 am

      Thanks Saurabh. Excellent posts. Waiting for next part.

Maka · November 29, 2020 at 8:59 pm

Hi Saurabh,

first of all, thank you for this very inspiring serie: clearly explained, to the point and motivating. I also followed your excellent serie on “Event Sourcing and CQRS with Axon and Spring Boot”.

I am new on event sourcing and cqrs and I am kind of stuck on making it work via a RabbitMQ message broker.
Is there any chance that you are going to release part 3 soon?

That would be really helpful.

Best regard,

    Saurabh Dashora · December 1, 2020 at 5:15 am

    Hi Maka,

    I’m glad that you liked the series and they were of help to you.

    Unfortunately, I don’t have fixed date of finishing the series since I got stuck with some other projects and couldn’t find the time to do it. I will keep you posted once I do finish it.

Anthony Kipkoech · October 7, 2021 at 7:32 am

Hi Saurabh, thanks for this very insightful series. I am really looking forward to the next article. One question I have, do you have an idea why my aggregates are not being persisted with the aggregate identifier? The table has all the other fields except for the identifier.

    Saurabh Dashora · October 8, 2021 at 12:12 pm

    Glad that the post was useful.

    With regards to your query, would have to look at your code to know more…please share a github link if possible…

Leave a Reply

Your email address will not be published. Required fields are marked *