Exploring AxonIQ: Building a Simple Application with Axon Framework

Exploring AxonIQ: Building a Simple Application with Axon Framework

Exploring AxonIQ: Building a Simple Application with Axon Framework

AxonIQ is a powerful suite designed for implementing microservices using CQRS (Command Query Responsibility Segregation) and Event Sourcing patterns.Its core components, Axon Framework and Axon Server, help developers create scalable, event-driven applications with built-in support for complex business logic.

Key Concepts of AxonIQ

  1. Axon Framework: A Java-based framework for building CQRS and Event-Sourcing applications. It provides abstractions for commands, events, and queries and seamlessly integrates with Axon Server.
  2. Axon Server: A purpose-built event store and messaging platform designed to work with Axon Framework. It supports event storage, distribution, and monitoring, simplifying complex microservices setups

Core Principles: CQRS and Event Sourcing

  • CQRS: This pattern separates the responsibility of handling commands (write operations) and queries (read operations) in an application, allowing for independent scaling
  • Event Sourcing: Instead of storing the current state of data, event sourcing logs all changes as events. The application’s state can be rebuilt by replaying these events

Example Application: Building a Simple Product Inventory System

Step 1: Set Up the Project

First, create a Spring Boot project with Axon Framework dependencies. You can use Spring Initializr or add the following dependencies to your pom.xml

<dependency>
    <groupId>org.axonframework</groupId>
    <artifactId>axon-spring-boot-starter</artifactId>
    <version>4.5</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Step 2: Define the Product Aggregate

In CQRS and event sourcing, an Aggregate is a domain-driven entity that processes commands and applies events. Here, we’ll create a ProductAggregate to handle adding products and updating stock

import static org.axonframework.modelling.command.AggregateLifecycle.apply;
import org.axonframework.modelling.command.AggregateIdentifier;
import org.axonframework.spring.stereotype.Aggregate;
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.eventsourcing.EventSourcingHandler;

@Aggregate
public class ProductAggregate {
    @AggregateIdentifier
    private String productId;
    private String name;
    private int stock;

    public ProductAggregate() {}

    @CommandHandler
    public ProductAggregate(CreateProductCommand command) {
        apply(new ProductCreatedEvent(command.getProductId(), command.getName(), command.getStock()));
    }

    @EventSourcingHandler
    public void on(ProductCreatedEvent event) {
        this.productId = event.getProductId();
        this.name = event.getName();
        this.stock = event.getStock();
    }
}

  

Step 3: Define Commands and Events

Commands represent the actions that change the state of the application, while events represent the outcome of these actions.


import org.axonframework.modelling.command.TargetAggregateIdentifier;
  
public class CreateProductCommand {
      
    @TargetAggregateIdentifier  
    private final String productId;
    private final String name;
    private final int stock;

    public CreateProductCommand(String productId, String name, int stock) {
      this.productId = productId;
      this.name = name;
      this.stock = stock;
    }

    // Getters
}

public class ProductCreatedEvent {
    private final String productId;
    private final String name;
    private final int stock;

    public ProductCreatedEvent(String productId, String name, int stock) {
      this.productId = productId;
      this.name = name;
      this.stock = stock;
    }

    // Getters
}  


import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "products")
public class Product {

    @Id
    private String productId;
    private String name;
    private int stock;

    // Default constructor for JPA
    public Product() {}

    public Product(String productId, String name, int stock) {
        this.productId = productId;
        this.name = name;
        this.stock = stock;
    }

    // Getters and Setters
    public String getProductId() { return productId; }
    public void setProductId(String productId) { this.productId = productId; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public int getStock() { return stock; }
    public void setStock(int stock) { this.stock = stock; }
}
    


import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductRepository extends JpaRepository {
    // Custom query methods (if needed) can be defined here
}

  

Step 4: Create Event and Query Handlers

Commands represent the actions that change the state of the application, while events represent the outcome of these actions.


import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.queryhandling.QueryHandler;
import org.axonframework.eventhandling.EventHandler;
import org.springframework.stereotype.Service;

@Service
public class ProductEventHandler {

    private final ProductRepository productRepository;

    public ProductEventHandler(ProductRepository productRepository){
        this.productRepository = productRepository;
    }

   

    @EventHandler
    public void on(ProductCreatedEvent event) {
        Product product = new Product(event.getProductId(), event.getName(), event.getStock());
        productRepository.save(product);
    }

  @QueryHandler
    public Product handle(ProductQuery query) {
        // Retrieve the product by productId
        return productRepository.findByProductId(query.getProductId())
                .orElseThrow(() -> new ProductNotFoundException("Product not found with id: " + query.getProductId()));
    }
}    

Step 5: Create a rest controller

Here's a sample Spring Boot controller class for the Axon-based product inventory application. This controller will handle REST API requests to add and retrieve products, allowing you to test the setup.


class CreateProductRequest {
    private String productId;
    private String name;
    private int stock;

    // Getters and setters
    public String getProductId() { return productId; }
    public void setProductId(String productId) { this.productId = productId; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getStock() { return stock; }
    public void setStock(int stock) { this.stock = stock; }
}


// Query to get a product by ID
public class ProductQuery {
    private final String productId;

    public ProductQuery(String productId) {
        this.productId = productId;
    }

    public String getProductId() { return productId; }
}


import org.axonframework.commandhandling.gateway.CommandGateway;
import org.axonframework.messaging.responsetypes.ResponseTypes;
import org.axonframework.queryhandling.QueryGateway;
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.CompletableFuture;

@RestController
@RequestMapping("/products")
public class ProductController {

    private final CommandGateway commandGateway;
    private final QueryGateway queryGateway;

    public ProductController(CommandGateway commandGateway, QueryGateway queryGateway) {
        this.commandGateway = commandGateway;
        this.queryGateway = queryGateway;
    }

    // Endpoint to add a new product
    @PostMapping
    public CompletableFuture addProduct(@RequestBody CreateProductRequest request) {
        CreateProductCommand command = new CreateProductCommand(
                request.getProductId(),
                request.getName(),
                request.getStock()
        );
        return commandGateway.send(command);
    }

    // Endpoint to retrieve a product by its ID
    @GetMapping("/{productId}")
    public CompletableFuture getProduct(@PathVariable String productId) {
        ProductQuery query = new ProductQuery(productId);
        return queryGateway.query(query, ResponseTypes.instanceOf(Product.class));
    }
}
  

Step 6: Run and Test the Application

You can now use Axon Server to run the application, which provides dashboards and monitoring tools to view the commands and events flowing through your system.

To test the setup:

  • Use a REST API client to send commands to add products to your inventory.
  • Retrieve the event logs from Axon Server or query your application to verify that products are correctly created and stored as events.

Diagram: AxonIQ Application Flow

Define command handlers for handling incoming commands and query handlers for retrieving data. Axon Framework can automatically route commands and events to the correct handlers.

┌────────────┐     ┌──────────────┐      ┌───────────────┐
│  Client    │──►──│ Command Bus  │──►──►│ Aggregates    │
│            │     │              │      │ (Product)     │
└────────────┘     └──────────────┘      └──────┬────────┘
                  │                              │
                  │                              │
┌───────────────┐ │ ┌─────────────┐     ┌────────┴───────┐
│ Event Bus     │◄──┤ Event Store │◄────│  Event Handler │
└───────────────┘   └─────────────┘     └───────────────┘

AxonIQ Application Flow

  1. Client: Initiates commands, such as creating or updating products.
  2. Command Bus: Routes commands to the appropriate aggregate.
  3. Aggregate (Product): Processes commands and applies events, which are then stored.
  4. Event Store: Stores the events generated by the aggregates, which represent the state changes.
  5. Event Bus: Broadcasts events to subscribers, including any handlers or query services.

Conclusion

Using AxonIQ’s Axon Framework and Axon Server allows developers to leverage event-driven design while building scalable applications with CQRS and event sourcing patterns. This example demonstrates how you can define commands, aggregates, and events, giving you a base to build complex microservices architectures.