Event-Driven Microservices with Axon Framework: Mastering CQRS and Async Persistence
A comprehensive guide to building event-driven microservices using AxonIQ Framework, implementing CQRS patterns with async JPA persistence for scalable, loosely-coupled architectures.
Table of Contents
Introduction
In the evolving landscape of distributed systems, event-driven microservices have emerged as a powerful paradigm for building scalable, loosely-coupled applications. At the heart of this architecture lies the CQRS (Command Query Responsibility Segregation) pattern, which fundamentally changes how we think about data access and state management.
This article explores a production-ready implementation using the AxonIQ Framework - a battle-tested toolkit for building event-driven applications in Java. We will demonstrate how to separate read and write models, implement async persistence, and achieve location transparency through event-based module interactions.
Key Insight: The starting point of CQRS design is differentiating between READ and WRITE models. This separation enables independent scaling and optimization of each side.
Understanding the Core Concepts
Event-Driven Architecture vs CQRS
Before diving into implementation, it is crucial to understand that Event-Driven Architecture is not CQRS, though they complement each other exceptionally well.
Event-Driven Architecture
| Concept | Description |
|---|---|
| Event-Driven | Architecture where components communicate through events |
| CQRS | Pattern that separates read and write operations into different models |
| Event Sourcing | Storing state changes as a sequence of events |
Events vs Messages
A common misconception is conflating events with messages:
| Aspect | Event | Message |
|---|---|---|
| Nature | Fact about something that happened | Container for data transfer |
| Immutability | Always immutable | May be mutable |
| Temporal | Past tense (OrderPlaced) | Imperative or neutral |
| Deletion | Never physically deleted | Can be deleted |
Important: Events are never physically deleted. Deletions become new events, enabling full audit trails and analytics capabilities.
Architecture Overview
The architecture separates concerns into distinct layers with event-based communication:
Microservices Architecture
┌─────────────────────────────────────────────────────────────────┐
│ API Gateway (REST) │
│ Swagger UI @ :8888 │
└─────────────────────────────┬───────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ Command Side │ │ Query Side │
│ (Write Model) │ │ (Read Model) │
├─────────────────────────┤ ├─────────────────────────┤
│ - Command Handlers │ │ - Query Handlers │
│ - Aggregate Roots │ │ - Projections │
│ - Domain Validation │ │ - Materialized Views │
└───────────┬─────────────┘ └─────────────┬───────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Axon Event Store │
│ (Embedded AxonIQ Database) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Async JPA Persistence Layer │
│ (EclipseLink + Derby DB) │
└─────────────────────────────────────────────────────────────────┘
Technology Stack
The implementation leverages a robust set of technologies:
| Technology | Purpose |
|---|---|
| Spring Boot 2 | Application framework and dependency injection |
| Axon Framework | CQRS and Event Sourcing infrastructure |
| EclipseLink JPA | Async persistence layer |
| Derby Database | Embedded database for storage |
| Swagger UI | API documentation and testing |
| Kotlin | Enhanced domain modeling (7.6% of codebase) |
Implementation Deep Dive
Domain Model Design
The architecture maintains clear separation between domain objects and entities:
// Command - Request for state change
public class CreateOrderCommand {
@TargetAggregateIdentifier
private final String orderId;
private final String customerId;
private final List<OrderItem> items;
public CreateOrderCommand(String orderId, String customerId,
List<OrderItem> items) {
this.orderId = orderId;
this.customerId = customerId;
this.items = items;
}
// Getters...
}
// Event - Fact about what happened
public class OrderCreatedEvent {
private final String orderId;
private final String customerId;
private final List<OrderItem> items;
private final Instant timestamp;
public OrderCreatedEvent(String orderId, String customerId,
List<OrderItem> items) {
this.orderId = orderId;
this.customerId = customerId;
this.items = items;
this.timestamp = Instant.now();
}
// Getters...
}
Aggregate Root Implementation
The Aggregate Root handles commands and produces events:
@Aggregate
public class OrderAggregate {
@AggregateIdentifier
private String orderId;
private OrderStatus status;
private List<OrderItem> items;
// Required by Axon
protected OrderAggregate() {}
@CommandHandler
public OrderAggregate(CreateOrderCommand command) {
// Validate business rules
validateItems(command.getItems());
// Apply the event (this persists and notifies)
AggregateLifecycle.apply(
new OrderCreatedEvent(
command.getOrderId(),
command.getCustomerId(),
command.getItems()
)
);
}
@EventSourcingHandler
public void on(OrderCreatedEvent event) {
// Update aggregate state from event
this.orderId = event.getOrderId();
this.status = OrderStatus.CREATED;
this.items = event.getItems();
}
@CommandHandler
public void handle(ConfirmOrderCommand command) {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException(
"Order cannot be confirmed in status: " + status);
}
AggregateLifecycle.apply(
new OrderConfirmedEvent(orderId)
);
}
@EventSourcingHandler
public void on(OrderConfirmedEvent event) {
this.status = OrderStatus.CONFIRMED;
}
private void validateItems(List<OrderItem> items) {
if (items == null || items.isEmpty()) {
throw new IllegalArgumentException(
"Order must contain at least one item");
}
}
}
Query Side Projections
Projections build optimized read models from events:
@Component
@ProcessingGroup("order-projections")
public class OrderProjection {
private final OrderViewRepository repository;
public OrderProjection(OrderViewRepository repository) {
this.repository = repository;
}
@EventHandler
public void on(OrderCreatedEvent event) {
OrderView view = new OrderView();
view.setOrderId(event.getOrderId());
view.setCustomerId(event.getCustomerId());
view.setStatus("CREATED");
view.setItemCount(event.getItems().size());
view.setCreatedAt(event.getTimestamp());
repository.save(view);
}
@EventHandler
public void on(OrderConfirmedEvent event) {
repository.findById(event.getOrderId())
.ifPresent(view -> {
view.setStatus("CONFIRMED");
view.setConfirmedAt(Instant.now());
repository.save(view);
});
}
@QueryHandler
public List<OrderView> handle(FindOrdersByCustomerQuery query) {
return repository.findByCustomerId(query.getCustomerId());
}
@QueryHandler
public Optional<OrderView> handle(FindOrderByIdQuery query) {
return repository.findById(query.getOrderId());
}
}
Async JPA Persistence Configuration
The implementation uses a custom async JPA layer with EclipseLink:
@Configuration
@EnableJpaRepositories
public class PersistenceConfig {
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
DataSource dataSource) {
LocalContainerEntityManagerFactoryBean em =
new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.gonnect.order");
em.setJpaVendorAdapter(new EclipseLinkJpaVendorAdapter());
Properties properties = new Properties();
properties.setProperty(
"eclipselink.weaving", "false");
properties.setProperty(
"eclipselink.logging.level", "FINE");
// Enable async persistence
properties.setProperty(
"eclipselink.persistence-context.flush-mode", "COMMIT");
em.setJpaProperties(properties);
return em;
}
@Bean
public PlatformTransactionManager transactionManager(
EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
}
REST API with Swagger
The application exposes REST endpoints documented via Swagger:
@RestController
@RequestMapping("/api/orders")
@Api(tags = "Order Management")
public class OrderController {
private final CommandGateway commandGateway;
private final QueryGateway queryGateway;
public OrderController(CommandGateway commandGateway,
QueryGateway queryGateway) {
this.commandGateway = commandGateway;
this.queryGateway = queryGateway;
}
@PostMapping
@ApiOperation("Create a new order")
public CompletableFuture<String> createOrder(
@RequestBody CreateOrderRequest request) {
String orderId = UUID.randomUUID().toString();
return commandGateway.send(
new CreateOrderCommand(
orderId,
request.getCustomerId(),
request.getItems()
)
);
}
@GetMapping("/{orderId}")
@ApiOperation("Get order by ID")
public CompletableFuture<OrderView> getOrder(
@PathVariable String orderId) {
return queryGateway.query(
new FindOrderByIdQuery(orderId),
ResponseTypes.optionalInstanceOf(OrderView.class)
).thenApply(opt -> opt.orElseThrow(
() -> new OrderNotFoundException(orderId)));
}
@GetMapping("/customer/{customerId}")
@ApiOperation("Get orders by customer")
public CompletableFuture<List<OrderView>> getCustomerOrders(
@PathVariable String customerId) {
return queryGateway.query(
new FindOrdersByCustomerQuery(customerId),
ResponseTypes.multipleInstancesOf(OrderView.class)
);
}
}
Running the Application
Prerequisites
- Clone and install the jpa-eclipselink dependency locally
- Java 8 or higher
- Maven 3.x
Build and Run
# Clone the repository
git clone https://github.com/mgorav/EventDrivenMicroServiceUsingAxonIQ.git
cd EventDrivenMicroServiceUsingAxonIQ
# Install the custom JPA library
cd jpa-eclipselink
mvn clean install
# Build the microservice
cd ..
mvn clean install
# Run the application
java -jar target/event-driven-microservice.jar
Access Points
| Endpoint | URL |
|---|---|
| Swagger UI | http://localhost:8888/swagger-ui.html |
| API Base | http://localhost:8888/api |
Testing the API
# Create an order
curl -X POST http://localhost:8888/api/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": "CUST-001",
"items": [
{"productId": "PROD-001", "quantity": 2, "price": 29.99}
]
}'
# Query the order
curl http://localhost:8888/api/orders/{orderId}
# Get customer orders
curl http://localhost:8888/api/orders/customer/CUST-001
Cloud Deployment
The project includes PCF (Pivotal Cloud Foundry) deployment configuration:
# manifest.yml
applications:
- name: event-driven-service
memory: 1G
instances: 1
path: target/event-driven-microservice.jar
buildpack: java_buildpack
env:
SPRING_PROFILES_ACTIVE: cloud
# Deploy to PCF
cf push
Benefits and Trade-offs
Benefits
| Benefit | Description |
|---|---|
| Scalability | Read and write sides scale independently |
| Flexibility | Multiple read models optimized for different queries |
| Audit Trail | Complete history through event sourcing |
| Loose Coupling | Modules communicate via events, not direct calls |
| Location Transparency | Services can be distributed across the network |
Trade-offs
| Challenge | Mitigation |
|---|---|
| Eventual Consistency | Design for async; use read-your-writes where critical |
| Complexity | Invest in tooling and team training |
| Event Schema Evolution | Use schema registry; design for forward compatibility |
| Debugging | Implement comprehensive logging and tracing |
Conclusion
The Axon Framework provides a robust foundation for building event-driven microservices with CQRS. By separating command and query responsibilities, implementing async persistence, and leveraging event-based communication, organizations can build systems that are:
- Scalable: Handle varying read and write loads independently
- Resilient: Recover from failures through event replay
- Evolvable: Add new read models without changing the command side
- Auditable: Maintain complete history for compliance and analytics
The EventDrivenMicroServiceUsingAxonIQ repository provides a complete reference implementation that you can use as a starting point for your own event-driven architecture journey.