CQRS Tutorial With Axon Framework (Step by Step FoodCart Project for Beginners)

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:

  1. The application we would build(FoodCart)
  2. Setup the project in IntelliJ
  3. Build the Core API
  4. Develop the Command Model
  5. Develop the Query Model
  6. 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 CreateFoodCartCommandSelectProductCommand, 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));
}