This is a complete tutorial on CQRS and Event Sourcing based application using Axon Framework(Food Cart). For best learning experience, this step by step guide should go along with the video explanation
We would cover the following:
- The application we would build(FoodCart)
- Setup the project in IntelliJ
- Build the Core API
- Develop the Command Model
- Develop the Query Model
- Connect to the UI
1. The application we would build
As you follow the video lesson, ensure you understand the following concepts. I would be explaining them as I go
- The Structure of the Axon Application
- What is CQRS(Command Query Responsibility Segregation)
- What are Commands, Events and Queries
- DDD (Domain Driven Design)
- Event Sourcing
- Evolutionary Microservices
The Structure of a CQRS based application is shown below. The architecture explained in this video

2. Setup the Project in IntelliJ
Create a new project in IntelliJ.
Add the dependencies:
- spring-boot-starter-data-jpa
- sprint-boot-starter-web
- h2-database (or another db)
In the pom.xml, add the axon-springboot-starter dependency.
This is shown below.
<dependency> <groupId>org.axonframework</groupId> <artifactId>axon-spring-boot-starter</artifactId> <version>4.3</version> </dependency>
The axon version should be 4.3!
Create three packages:
- command: expression of intent to perform some operation
- query: notification that some operation have occurred
- coreapi: request for data
These three are different types of messages.
Add the h2 database configuration in the application.properties file (get it from here)
3. Build the Core API (an event-driven API)
In this part, we are going to build the core api which is made up of commands, events and queries.
Commands: Create a kotlin file named commands in the commands package. Write the data class for CreateFoodCartCommand, SelectProductCommand, DeselectProductCommand and ConfirmOrderCommand
The codes for these four commands is given below:
data class CreateFoodCartCommand( @TargetAggregateIdentifier val foodCartId: UUID ) data class SelectProductCommand( @TargetAggregateIdentifier val foodCartId: UUID, val productId: UUID, val quantity: Int ) data class DeselectProductCommand( @TargetAggregateIdentifier val foodCartId: UUID, val productId: UUID, val quantity: Int ) data class ConfirmOrderCommand( @TargetAggregateIdentifier val foodCartId: UUID )
The val specification ensures that the fields are final fields.
data class provide equal and hash code and ToString() methods
Events: Create a kotlin file named events in the coreapi package.
Write the data classes for the events corresponding to each command you wrote previously. This are shown in the code below:
data class FoodCardCreatedEvent( val foodCardId: UUID ) data class ProductSelectedEvent( val foodCardId: UUID, val productId: UUID, val quantity: Int ) data class ProductDeselectedEvent( val foodCardId: UUID, val productId: UUID, val quantity: Int ) data class OrderConfirmedEvent( val foodCardId: UUID )
Note that for the events, an id is specified for the foodCardCreatedEvent. This is because, an event indicated that the foodCart has been created and therefore exists.
Queries: Create a kotlin class named queries. In it, write a data class for FindFoodCartQuery and another class for RetrieveProductOptions. This is shown below
data class FindFoodCartQuery( val foodCartId: UUID ) //Which products can be selected class RetrieveProductOptionsQuery
4. Develop the Command Model (Build the Aggregate)
Building the Aggregate: You need to create the FoodCart aggregate class in the commands package, create an empty constructor. Then annotate this class with the @Aggregate annotation.
The @Aggregate annotation serves the following purpose:
- makes the framework creates an aggregate factory for us
- make the framework know that this class would contain commandhandler that would be subscribed to
- that this class would publish events and therefore provides the repository
This aggregate should have two fields: the foodCardId and the selectedProducts
Handling Commands: Then you need to create a command handler for the constructor for FoodCart. Next, create two more command handlers for SelectProduct and DeselectProduct commands
The three commandhandlers are shown below with an empty constructor as well
public FoodCart(){ } @CommandHandler public FoodCart(CreateFoodCartCommand cmd){ apply(new FoodCartCreatedEvent(cmd.getFoodCartId())); } @CommandHandler public void handle(SelectProductCommand cmd){ apply(new ProductSelectedEvent( foodCartId, cmd.getProductId(), cmd.getQuantity() )); } @CommandHandler public void handle(DeselectProductCommand cmd) throws ProductDeselectedException { UUID productId = cmd.getProductId(); if(!selectedProducts.containsKey(productId)){ throw new ProductDeselectedException(); } apply(new ProductDeselectedEvent( foodCartId, cmd.getProductId(), cmd.getQuantity() )); }
The @CommandHandler annotation tells the framework that this is a commandhandling function. So the method is registered to the command bus as capable of handling a particular command.
Event Sourcing the Aggregate: Here, you need to write the event sourcing handlers for FoodcartCreated, ProductSelected and ProductDeselected events.
The methods are shown below
@EventSourcingHandler public void on(FoodCardCreatedEvent evt){ foodCardId = evt.getFoodCardId(); //Instantiate the selected product //once the foodcart is created selectedProducts = new HashMap<>(); } @EventSourcingHandler public void on(ProductSelectedEvent evt){ selectedProducts.merge(evt.getProductId(), evt.getQuantity(), Integer::sum); } //The merge() function is used to combine // multiple mapped values for a key // using the mapping function
Validate the DeselectProduct command
You need to modify the DeleteProduct command to make sure the product being deselected actually exists. To do this, you need to check if the hashmap actually contains the given key.
I also choose to create an exceptions kotlin file to place the ProductDeselectionException
Now the modified DeselectProductCommand is shown below:
@CommandHandler public void handle(DeselectProductCommand cmd) throws ProductDeselectedException { UUID productId = cmd.getProductId(); if(!selectedProducts.containsKey(productId)){ throw new ProductDeselectedException(); } // apply() omitted }
Note: You need to create the ProductDeletedException() class in the coreapi package. It should inherit Exception (see video)
5. Develop the Query Model
We would now build the query model, to handle events and queries.
Create a file in the query package and name it FoodCartView.
Create class with same name and annotate this class with @Entity annotation. This class should contain two fields:
foodCartId and products
Inside this class, create an interface FoodCartViewRepository that inherits JpaRepository. The code is given below:
@Entity data class FoodCartView( @Id val foodCartId: UUID, @ElementCollection val products: Map<UUID, Int> ) interface FoodCartViewRepository:JpaRepository<FoodCartView, UUID>
We need a component to handle the events and queries.
So create a java class in the query package and call it FoodCartProjector. This would be in charge of projecting the FoodCartView as well as updating the FoodCardView.
Annotate this class with @Component annotation
In this class, create the repository variable and then the constructor.
Create an EventHandler for FoodCartCreated event. This would simply create a new foodCart and persist it using the repository save() method.
Create a QueryHandler to return find a FoodCart by Id
The content of the FoodCartProjector is given below
private final FoodCartViewRepository repository; public FoodCartProjector(FoodCartViewRepository repository) { this.repository = repository; } @EventHandler public void on(FoodCartCreatedEvent evt){ FoodCartView foodCartView = new FoodCartView( evt.getFoodCartId(), Collections.emptyMap() ); } @QueryHandler public FoodCartView handle(FindFoodCartQuery query) { return repository.findById( query.getFoodCartId()) .orElse(null); }
6. Connection to the UI
Here we would introduce a component that we can invoke with regular REST operation. This UI component would us CommandGateway to dispatch command and query messages to the Framework
Create a new package called the gui.
In this FoodOrderingController in the gui package and annotate with with @RestController
The content of the FoodOrderingController is given below:
private final CommandGateway commandGateway; private final QueryGateway queryGateway; public FoodOrderingController(CommandGateway commandGateway, QueryGateway queryGateway) { this.commandGateway = commandGateway; this.queryGateway = queryGateway; } @PostMapping("/create") public void handle(){ commandGateway.send(new CreateFoodCartCommand()); } @GetMapping("/footcart/{foodCartId") public CompletableFuture<FoodCartView> handle(@PathVariable("foodCartId") String foodCartId){ return queryGateway.query(new FindFoodCartQuery(UUID.fromString(foodCartId)), ResponseTypes.instanceOf(FoodCartView.class)); }
