
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
- 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.
- 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
- Client: Initiates commands, such as creating or updating products.
- Command Bus: Routes commands to the appropriate aggregate.
- Aggregate (Product): Processes commands and applies events, which are then stored.
- Event Store: Stores the events generated by the aggregates, which represent the state changes.
- 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.