Axoniq Conference 2026 · Utrecht

Rebuilding the Airplane in Flight

From browser CRUD to Event Sourcing — without landing the plane.

Richard Bouška · CTO, ASSIST
Ondřej Halata · Principal Engineer, ASSIST
CQRS · DDD · Event Sourcing
links & slides
captain speakingLadies and gentlemen, this is your captain speaking. Welcome aboard. Please be advised that this aircraft is being rebuilt during the flight. Sit back and relax — our cabin crew is ready to ASSIST™ you. Cabin crew, arm doors and cross-check.
The usual story

The terrifying part is getting there.

No maintenance window long enough. No big-bang rewrite. No moment to stop the world.

Twelve intermediate architectures. Every one of them had to fly.

The browser was the backend.

illustrative · the browser, orchestratinglist = await get( "/appointments") for (a of list) a.customer = await get( `/sessions/${a.id}`) await post("/session") await post("/appointment") // …and if the second one fails?

The browser wasn’t really a frontend. It had become the integration platform.

the NO_TRANSACTION dragon
One business action, several CRUD calls — nothing held them together.
Phase 0 · Control the chaos
Browser-as-Backend
Starting state · before the web BFF (2021)
captain speakingLadies and gentlemen, the CRUD swamp is now being left behind. Some turbulence is to be expected — please keep your seatbelts fastened while the seatbelt sign is illuminated. Should help be needed, the crew is standing by to ASSIST™ you.

Put a backend in the middle.

Mar 2022 · an entire service methodreturn accountsConnector .postPreRegister(…);

It wasn’t the new architecture yet. It was the place where the new architecture could start.

BFF: not “best friends forever”. Backend for Frontend — a protocol stereotype service.

the NO_TRANSACTION dragon
The same CRUD calls through the new door: still no transaction, and the third can still get lost.
Phase 1 · Control the chaos
Gateway BFF
Git · Nov 2021 BFF initial commit (14fe5c3de) · Mar 2022 “BFF for WebFE” (633555af7)

CRUD starts producing events.

REST → service → JPA commit @PostPersist → CREATED @PostUpdate → MODIFIED @PostRemove → DELETED afterCommit() → SNS

First our events described what changed. Later they started describing why.

the OUT_OF_ORDER dragon
Ordering mattered within two weeks: FIFO message groups, May 2022.
Phase 2 · Control the chaos
CRUD + Change Events
Git · May 2022 · f4ada88e0 · “Send SNS notifications when an appointment is created/updated/deleted”

We built projections before we called them projections.

before fan-out at request time: config → resources → appointments → a customer session per appointment after one query: /projections/appointments/search

Read models before Event Sourcing. CQRS doesn’t require Event Sourcing — the write side was still CRUD.

the READ_MODEL dragon
Read models bring their own problems: projection deadlocks, May 2024.
Phase 3 · Local state & messaging
Read Models on the Side
Git · Dec 2022 – Apr 2023 · 0cea060ee, be704bc6e, 4b11db078
captain speakingLadies and gentlemen, the chaos has been left behind and cruising altitude is being reached. Local state and messaging can be seen on your left. Cabin crew, you are released to ASSIST™ with the in-flight service.

Stop going outside to talk inside.

Apr 2023 · BFF configurationcustomer sessions APIGEE /commerce/v1 → /customer-sessions/api/v1

Still REST. Just no longer through the front door.

API management stays the external boundary — not the internal service bus.

the DETOUR dragon
Out and back in, just to talk to a neighbour: proxy, APIGEE, ALB — AWS to GCP and back.
Phase 4 · Local state & messaging
In-Cluster Services
Git · Jul 2022 d9819ada9 “switch connector from apigee…” · Apr 2023 63304cff4

We adopted Axon before we adopted Event Sourcing.

JPA afterCommit → Axon command → aggregate → event ↘ SNS, still running each path behind its own feature flag

Yes, there were aggregates. Their replay restored exactly one field: the ID.

the FILTER_CONFIG dragon
Every release shipped new filter config; every rollback had to undo it. We never found a forward/backward-compatible way.
Phase 5 · Local state & messaging
Axon Message Bus
Git · Jul – Aug 2023 · d45e981a1, fe6355276, d67519a83 “Replace SNS/SQS with Axon”

From technical APIs to domain conversations.

before PATCH key-drop document { …fields… } worse PUT { the whole record } → diff it after SetParkingAccessCode UploadKeyDropMedia RemoveKeyDropMedia where no URL — the handlers register

We stopped making services talk HTTP to each other. We started making them speak the domain language.

the GUESS_THE_INTENT dragon
A PATCH says which fields changed. A PUT overwrites the whole record: diff old and new, then guess the intent.
Phase 6 · How the system speaks
Command & Query
Git · Mar – Nov 2024 · 8ce2a442d, 22d65d389, 96967f0de, d18d9db6a
captain speakingLadies and gentlemen, for the next stretch a new language will be spoken on board: domain conversations. Please refer to the safety card in the seat pocket in front of you; translations will be provided by ASSIST™.

Every change, told twice.

POST / PUT / PATCH / DELETE → command → domain event + CDC event, emitted explicitly JPA afterCommit → retired, service by service the same change, twice · on purpose

Every event processor moves from CDC to domain events on its own — still today.

the TOLD_TWICE dragon
150+ processing groups to migrate, one by one — still in progress.
Phase 7 · How the system speaks
Dual Stream
Git · Aug 2023 – Nov 2024 · d67519a83, f9df230d2, d18d9db6a

Intent goes in. State changes come out.

Feb 2024 · the notification pathprojection → query update → Axon subscription query → WebSocket BFF → browser

Finally out of the 1:1 request–response paradigm.

Progressively, everything moves to WebSocket.

the LONG_POLLING dragon
Long polling for notifications: long requests hurt Apdex, and it was ugly.
Phase 8 · How the system speaks
Split Front Door
Git · Dec 2023 24d559197 (scaffold) · Feb 2024 9f12aa68f, 611d3e3bd (subscriptions)

Every deployable gets an explicit job.

Feb 2024 · one image, two rolesspring profile: command spring profile: query

We stopped treating every deployment unit as a unique snowflake.

Spring profiles: not elegant — just easy.

the HALF_AND_HALF dragon
HTTP and Axon, both ways, in one service: deadlocks. Duplicated services: inconsistent code.
Phase 9 · Make it explicit
Building Blocks
Git · Feb 2024 5dc5bdc20 · Apr 2024 9aaab10f8 · Oct 2025 ace89af40 · Jul 2026 0a5a8fb82
captain speakingLadies and gentlemen, our approach to Event Sourcing has been initiated. Tray tables are to be stowed and seat backs returned to the upright position — the crew will ASSIST™ you. Cabin crew, prepare for landing.

The event stream becomes the source of truth.

Apr 2025 · CustomViewDefinitionAggregate// decide from reconstructed state if (command.getName() != null && !Objects.equals(name, command.getName())) { apply(mapper.toRenamedEvent(command)); } @EventSourcingHandler public void on(CustomViewDefinitionRenamed event) { this.id = event.getId(); this.name = event.getName(); }

Not only in new aggregates: fully event-sourced flows inside existing hybrid ones.

1 command · 2 aggregate · 3 domain event · 4 Axon Server · 5 event processor · 6 projection

the HAND_PATCH dragon
Projections patched by hand over the years no longer match their events.
Phase 10 · Make it explicit
Event-Sourced Core
Git · Apr 2025 c5932fced · Aug 2025 69606607e

You can’t replay what was never written.

projection: rows from before the events event store: starts later import batch → …Imported events // ≈ 20 million of them replay → the whole story

First we wrote the history down. Then we could replay it.

the WIPE_AND_REPLAY dragon
A replay would have quietly dropped everything older than the first event.
Phase 11 · Make it explicit
Full-History Event Store
Git · Jan 2026 · a727e8da0 · import support for pre-Axon data
The payoff

The behaviour is already there. It is the event stream.

Once meaningful state changes are first-class events, a new capability is often just a new reader.

Every event processor has its own cassette — a copy of an Axon context, at its own place. Each one replays on its own.

R: real event names from the codebase · L: their CDC twins, illustrative · the herd: illustrative
captain speakingLadies and gentlemen, our destination has been reached — but no one is permitted to disembark. Please remain seated with your seatbelt fastened: the rebuild is being continued by ASSIST™. Cabin crew, doors remain armed.
Reality check

It was never a straight line.

Different parts of the system moved at different speeds. The phases overlapped — the order is a story, not a schedule.

Not a project, a process: driven by the business, changing as new needs arise — never an ivory tower.

Each dot is a selected commit anchoring a phase · Git author dates, not deployment dates · phase 0 predates this repository
Closing

None of these was the target architecture. Every one of them was a production architecture.

We never migrated the application to Event Sourcing. We continuously evolved it into an event-sourced system.

Thank you.
Questions — and war stories — welcome.
Richard Bouška · Ondřej Halata — ASSIST
links & slidesassist-links.web.appscan it before the dragon skates off with it
captain speakingLadies and gentlemen, thank you for flying with us. This aircraft is being kept in service. Should you need anything, our crew will be happy to ASSIST™ you. Cabin crew, disarm doors and cross-check.
01 / 17v238 · c03d8dc · 2026-10-28 07:30 UTC