GitHub · LinkedIn · About · YouTube
Last updated by Kindson Munonye — June 29, 2026
Last updated: June 29, 2026 · Estimated time: ~60 minutes
This step-by-step tutorial builds a minimal CQRS application with Axon Framework and Spring Boot. If you’re new to the concepts, start with the CQRS & Event Sourcing introduction, then return here for the hands-on implementation.
What we’ll build
- Command side: CreateOrder command → OrderAggregate → OrderCreatedEvent
- Event store: Axon Server (local dev)
- Query side: OrderProjection → H2 read model → REST GET endpoint
Prerequisites
- Java 17+, Maven or Gradle, Spring Boot 3.x basics
- Understanding of REST APIs (Spring Boot CRUD tutorial)
- Axon Server downloaded from axoniq.io
Step 1 — Create the Spring Boot project
Use Spring Initializr with dependencies: Spring Web, Spring Data JPA, H2, Axon Spring Boot Starter.
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-spring-boot-starter</artifactId>
<version>4.9.3</version>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-server-connector</artifactId>
<version>4.9.3</version>
</dependency>Step 2 — Start Axon Server
# Extract and run Axon Server
./axonserver
# HTTP UI: http://localhost:8024
# gRPC: localhost:8124# application.properties
spring.application.name=order-service
axon.axonserver.servers=localhost:8124
spring.datasource.url=jdbc:h2:mem:readmodel
spring.jpa.hibernate.ddl-auto=update
server.port=8080Step 3 — Define commands and events
// CreateOrderCommand.java
public class CreateOrderCommand {
@TargetAggregateIdentifier
private final String orderId;
private final String productId;
public CreateOrderCommand(String orderId, String productId) {
this.orderId = orderId;
this.productId = productId;
}
public String getOrderId() { return orderId; }
public String getProductId() { return productId; }
}
// OrderCreatedEvent.java
public class OrderCreatedEvent {
private final String orderId;
private final String productId;
public OrderCreatedEvent(String orderId, String productId) {
this.orderId = orderId; this.productId = productId;
}
public String getOrderId() { return orderId; }
public String getProductId() { return productId; }
}Step 4 — Create the aggregate (command handler)
@Aggregate
public class OrderAggregate {
@AggregateIdentifier
private String orderId;
protected OrderAggregate() {} // required by Axon
@CommandHandler
public OrderAggregate(CreateOrderCommand cmd) {
AggregateLifecycle.apply(new OrderCreatedEvent(cmd.getOrderId(), cmd.getProductId()));
}
@EventSourcingHandler
public void on(OrderCreatedEvent event) {
this.orderId = event.getOrderId();
}
}The aggregate enforces business rules and emits events. Axon persists events to Axon Server — this is event sourcing.
Step 5 — Build the query side (projection)
@Entity
public class OrderView {
@Id private String orderId;
private String productId;
private String status;
// constructors, getters, setters
}
@Component
public class OrderProjection {
@Autowired private OrderViewRepository repo;
@EventHandler
public void on(OrderCreatedEvent event) {
repo.save(new OrderView(event.getOrderId(), event.getProductId(), "CREATED"));
}
}Step 6 — REST endpoints (command + query)
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired CommandGateway commandGateway;
@Autowired OrderViewRepository orderViewRepository;
@PostMapping
public CompletableFuture<String> create(@RequestBody CreateOrderRequest req) {
String id = UUID.randomUUID().toString();
return commandGateway.send(new CreateOrderCommand(id, req.getProductId()))
.thenApply(v -> id);
}
@GetMapping("/{id}")
public OrderView get(@PathVariable String id) {
return orderViewRepository.findById(id).orElseThrow();
}
}CQRS in action: POST dispatches a command through Axon; GET reads from the optimized query database.
Step 7 — Test the flow
# Create order (command)
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{"productId": "PROD-001"}'
# Query order (read model) — may take a moment (eventual consistency)
curl http://localhost:8080/orders/<orderId>Check Axon Server UI at http://localhost:8024 to see events stored in the event stream.
Step 8 — Unit test the aggregate
@Test
void shouldCreateOrder() {
fixture.givenNoPriorActivity()
.when(new CreateOrderCommand("order-1", "PROD-001"))
.expectEvents(new OrderCreatedEvent("order-1", "PROD-001"));
}Next steps
- Add
ShipOrderCommandandOrderShippedEventfor a second event type - Implement a Saga for multi-step workflows (payment + inventory)
- Read the CQRS concepts guide for architecture patterns
- Explore the Microservices tutorials hub
- Compare with traditional CRUD: Angular CRUD + Spring Boot guide