DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • A Robust Distributed Payment Network With Enchanted Audit Functionality - Part 2: Spring Boot, Axon, and Implementation
  • How To Build Self-Hosted RSS Feed Reader Using Spring Boot and Redis
  • Leveraging Salesforce Using a Client Written In Vue.js
  • Sprinkle Some ELK on Your Spring Boot Logs

Trending

  • How to Build Local LLM RAG Apps With Ollama, DeepSeek-R1, and SingleStore
  • A Guide to Container Runtimes
  • Building Scalable and Resilient Data Pipelines With Apache Airflow
  • Java's Quiet Revolution: Thriving in the Serverless Kubernetes Era
  1. DZone
  2. Coding
  3. Frameworks
  4. Implementing Event Sourcing With Axon and Spring Boot (Part 2)

Implementing Event Sourcing With Axon and Spring Boot (Part 2)

Learn more about event sourcing with the Axon and Spring Boot frameworks.

By 
Saurabh Dashora user avatar
Saurabh Dashora
DZone Core CORE ·
Feb. 11, 19 · Tutorial
Likes (9)
Comment
Save
Tweet
Share
25.6K Views

Join the DZone community and get the full member experience.

Join For Free

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 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.

Spring Framework Spring Boot Event Command (computing)

Published at DZone with permission of Saurabh Dashora. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • A Robust Distributed Payment Network With Enchanted Audit Functionality - Part 2: Spring Boot, Axon, and Implementation
  • How To Build Self-Hosted RSS Feed Reader Using Spring Boot and Redis
  • Leveraging Salesforce Using a Client Written In Vue.js
  • Sprinkle Some ELK on Your Spring Boot Logs

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!