June 29, 2026

Axon Framework CQRS Tutorial — Step by Step (2026)

New tutorial — June 29, 2026 · Hands-on Axon Framework CQRS with Spring Boot. Microservices hub

Kindson Munonye · Software engineer & technical author
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

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=8080

Step 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

Resources

Kindson Munonye

Kindson Munonye is a software engineer and technical author specializing in Angular, Spring Boot, and microservices architecture. He publishes step-by-step tutorials with source code covering CRUD operations, reactive forms, CQRS, event sourcing, and REST API integration.GitHub · LinkedIn · About · YouTube

View all posts by Kindson Munonye →
0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted