Implementing CQRS in Spring Boot with Axon Framework

Most Java teams in Sydney and Melbourne eventually hit the same wall. A monolithic service handling every read and write becomes a tangled web of conditional logic, and performance starts to wobble under peak load. Separating the two sides is a well-trodden path out, and Axon Framework offers one of the cleanest implementations for Spring Boot.

The Command Query Responsibility Segregation pattern, shortened to CQRS, pairs naturally with event sourcing. Commands change state, queries return projections, and events carry the truth between the two. Axon provides the building blocks out of the box, so you spend less time on plumbing and more time on the domain rules.

This walkthrough focuses on practical Java code rather than theoretical diagrams. Each step builds on the last, so by the end you will have a working CQRS slice running locally. From a Brisbane coworking space to a home office in Perth, the setup remains identical.

Why CQRS suits Australian Java projects

CQRS is a design choice that becomes powerful when paired with Axon. The write side handles commands that represent intent, while the read side returns denormalised views optimised for UI rendering and reporting.

In Australian fintech contexts, the read model might reflect GST-inclusive pricing instantly while the write model focuses on validating ABNs and managing compliance trails. That asymmetry is exactly where CQRS earns its keep. Axon handles event dispatch, storage, and the replay logic needed to rebuild read models whenever the schema evolves.

The pattern also lines up with how local banks approach Open Banking. Separating command and query APIs lets risk teams audit writes in isolation while customer-facing experiences stay responsive across NBN connections in suburban Perth.

Wiring Axon into a Spring Boot application

Start with Maven dependencies: axon-spring-boot-starter, axon-server-connector, and the usual Spring Boot starters for web and JPA. The starter auto-configures CommandBus, EventBus, and most of the standard beans you would otherwise hand-wire.

Configuration lives in application.yml. Point Axon at embedded H2 for local tests or at managed Postgres when pushing to staging in the AWS Sydney region. Connection details look like any other Spring datasource, so familiar Boot conventions apply.

Axon integrates with Spring Boot Actuator, exposing metrics under /actuator. The usual Grafana dashboards in Melbourne pick up command handling times without extra glue code, and existing Prometheus scrapes keep working unchanged.

Building aggregates and command handlers

Aggregates in Axon are plain classes annotated with @Aggregate. The constructor receives the creation command, and methods annotated with @CommandHandler mutate state. Every change publishes an event that becomes the read side's source of truth.

A practical example: an Order aggregate tracking line items, totals, and shipping status for a Sydney-based retailer. The aggregate enforces invariants like "an order cannot be cancelled after dispatch", returning a specific exception that the global error handler translates into an HTTP 422 response.

Unit testing uses AggregateTestFixture, asserting that a given command fires the expected events. These tests run in milliseconds, fitting the rapid feedback loop Australian dev teams expect from their build pipelines.

Projecting read models from events

The query side consumes events and updates read models called projections. Annotate a class with @ProcessingGroup, mark each handler with @EventHandler, and update a JPA entity, MongoDB document, or Elasticsearch index as needed.

Choosing the right backing store matters. The table below maps typical requirements onto suitable stores, including how each behaves under Australian latency expectations.

Read model requirement Recommended store Why it fits
Real-time dashboards Redis or Caffeine Sub-millisecond lookups
Auditable history Postgres append-only ACID guarantees and SQL tooling
Full-text search Elasticsearch Native tokenisation for product names
Aggregated reporting ClickHouse Columnar compression at scale
Mobile sync queues DynamoDB Managed throughput in ap-southeast-2

For an Australian loyalty program, the projection might maintain points balances in Redis while batch-recalculating tier status nightly in Postgres. The flexibility comes from the event stream being the single source of truth, so each projection evolves independently without touching the write side.

Sagas and process managers across aggregates

When a single command triggers work across multiple aggregates, sagas come into play. A saga listens for events, dispatches new commands, and keeps state until the flow completes. They handle compensating transactions cleanly, which matters when integrating external payment gateways.

A classic example in an Australian superannuation platform: moving funds between member accounts. The saga debits one aggregate, dispatches a transfer command to another, and waits for confirmation. On failure, a compensating event triggers a refund that the read model reflects within seconds.

Common pitfalls worth dodging

These guidelines cut the support burden at production volumes measured in tens of thousands of daily transactions.

Operational tips for going live

Before pushing to production, run Axon Server console locally and replay event streams. Verify projections rebuild correctly, then introduce a chaos test that kills the query database. The write side should keep accepting commands, and projections should catch up once the database recovers.

Document the runbook clearly. Australian operations crews appreciate plain-English explanations of what "a saga timeout" actually means for the business. Pair the framework with web-development expertise to keep the integration tidy and the dashboards meaningful.

Checklist before launch day

Tick those off and the architecture should hold up under the busiest sales periods an Australian retailer faces, from EOFY promotions through to the Boxing Day rush.