advanced~2h

Capstone: The Complete Event-Driven Bank

A synthesis topic that assembles every pattern covered so far — aggregates, event store, projections, materialized views, sagas, the transactional outbox, snapshots, and event replay — into one coherent picture of how a real event-driven system actually works end to end.

Learning objectives

  • Describe how commands, aggregates, the event store, projections, materialized views, and sagas fit together in one running system
  • Explain, in plain language, what capabilities a full event-driven architecture unlocks
  • Identify common failure modes that appear across a complete event-driven system, not just in one component
  • Judge when this architecture's complexity is worth paying for, and when a simpler approach is the better call

Step back from any single pattern and look at how a complete event-driven system actually fits together, using the bank domain built up across earlier material as the concrete example.

A client application issues a command — "approve this loan," "deposit these funds," "update this credit profile" — through a command gateway. That command routes to the right aggregate: Customer, Account, Card, or Loan, each one responsible for enforcing its own business rules and deciding whether the command is even valid. If it is, the aggregate applies one or more events, and those events are appended, permanently, to the event store — the single, authoritative record of everything that has ever happened in the system.

From there, the event store's job is to make sure every interested party finds out. Projections — per-service read models, one per bounded context — react to the events relevant to them and keep their own fast, query-optimized view up to date. A materialized view does something broader: it combines events from all of the participating services into one denormalized view built specifically for a screen or a report that needs data spanning several services at once, without forcing a query to reach across service boundaries at read time.

Multi-service processes — like a loan approval that needs money deposited and a credit profile updated — are coordinated by a saga, whether implemented as choreography or as an explicit orchestrator, complete with compensation logic for when a step fails partway through.

Underneath all of this, several supporting patterns keep the whole system trustworthy and fast without anyone having to think about them constantly. A transactional outbox guarantees that persisting state and publishing the corresponding event happen atomically, so a crash between the two can never leave the database and the event stream disagreeing about what happened. Snapshots keep long-lived, heavily used aggregates fast to load, without ever discarding their real history. Event replay means any projection or materialized view can be deleted and perfectly rebuilt from the event store at any time, which is what makes read models genuinely disposable rather than something to handle with extreme care.

Put together: commands flow in, aggregates enforce rules and emit events, the event store holds the permanent truth, projections and materialized views keep reads fast, and sagas coordinate anything that spans more than one aggregate — with the outbox, snapshots, and replay all working quietly in the background to keep the whole thing correct and performant over time.

Looking back at everything covered, the capabilities involved cluster into a few clear themes.

Understanding why this complexity is worth it in the first place. You should be able to explain, in plain language, why splitting a monolith's database across several services creates real cross-service problems that a single shared database never had — and why combining data from several services for one screen needs a deliberate answer, whether that's simple request-time composition for a low-traffic screen or a materialized view for a high-traffic one. You should also be able to explain CQRS honestly: what separating the write model from the read model actually buys you, and when that separation is genuinely not worth its added complexity for a simple CRUD-shaped feature.

Building the write side correctly. This means explaining event sourcing's core idea — that state is always calculated from history, never stored directly as a mutable row — and being able to build a real aggregate that enforces business rules and applies events correctly in response to valid commands.

Building a fast, correct read side. This covers building an independent query side with projections that stay fast regardless of how the write side is modeled, choosing correctly between different event processor strategies depending on whether ordering and delivery guarantees matter for a given projection, and knowing how to use a subscription query to push live updates to a client instead of forcing it to poll.

Coordinating multi-service processes safely. This means designing a saga with correct, explicit compensation for every step that could fail, and deliberately choosing between choreography and orchestration based on how many steps are involved and how much explicit visibility into failure handling actually matters — not defaulting to either one out of habit.

Keeping the system correct and fast as it ages. This means explaining and avoiding the dual-write problem using the transactional outbox pattern, using event replay to fix a projection bug retroactively without a painful manual data migration, and using snapshots to keep a heavily used aggregate fast without ever losing any of its real history.

Together, these aren't independent tricks — they're one coherent way of building systems that can honestly explain, at any moment, exactly how they arrived at their current state.

Looking at the complete system rather than any one pattern in isolation, a handful of failure modes tend to recur, and they're worth naming explicitly.

▲ Common mistake

Persisting state to a database and publishing the corresponding event as two separate operations, without a transactional outbox tying them together. If the process crashes between the two, the database and the event stream disagree about what happened — either an event nobody's data reflects, or data that nobody downstream ever heard about. This is the dual-write problem, and it quietly breaks the "the event store is the single source of truth" guarantee everything else in the system depends on.

Letting a choreography saga sprawl past the point where the plan is traceable. Every service in the chain is individually simple, but a long, undocumented chain of implicit listeners eventually becomes genuinely difficult to debug — "why didn't the customer get notified?" can require tracing through several unrelated codebases when nothing in any one of them shows the full picture.

Treating a read model as if it were the source of truth. A projection or materialized view that's ever mutated directly, or that a downstream system starts depending on being always perfectly correct without accounting for eventual consistency, undermines the entire premise that read models are disposable and rebuildable. If a bug is ever fixed by hand-editing a read model's rows instead of fixing the projection logic and replaying, that fix silently disappears the next time a replay happens.

Running a full event replay carelessly against a busy production system. Ignoring the load it places on the event store, or the temporary staleness of the projection being rebuilt, turns a genuinely safe operation into an incident.

Snapshotting aggregates that never needed it, or forgetting that a snapshot can go stale relative to changed aggregate code. Both waste effort without adding real value, and both are easy to avoid by reserving snapshots for aggregates that actually accumulate a large event volume, and by treating a snapshot as disposable the moment it stops matching current code.

Most of these failure modes share a common root: forgetting which part of the system is the actual source of truth, and treating something disposable — a projection, a materialized view, a snapshot — as if it weren't.

None of this — event sourcing, CQRS, sagas, projections, snapshots — is free. Every one of these patterns adds real code, real operational surface area, and real cognitive load for anyone new joining the team. Honest engineering means being able to say clearly when that cost is worth paying, and when it isn't.

It's worth it when:

  • The domain genuinely benefits from a permanent, complete audit trail of every state change — financial transactions, regulatory-sensitive workflows, anything where "how did we get here?" is a question that gets asked for real, not hypothetically.
  • Multiple services need their own independently scaled, independently modeled view of overlapping data, and keeping those views in sync through direct database access or synchronous calls has already caused real problems.
  • Read and write load are different enough in shape or scale that optimizing them separately (CQRS) produces a measurable, needed improvement — not just a theoretical one.
  • Multi-step processes spanning several services are common enough, and failure-prone enough, that having explicit, well-tested compensation logic (a saga) actually prevents real incidents.

It's overkill when:

  • A service is simple, largely CRUD-shaped, and unlikely to ever need more than "current state" — event sourcing here adds replay logic, snapshotting decisions, and projection maintenance for a benefit nobody will use.
  • A team is small, or new to these patterns, and the operational cost of running an event store, tuning event processors, and reasoning about eventual consistency outweighs whatever architectural benefit is being chased.
  • A single database and a couple of well-indexed tables would answer every query the system actually needs to answer, at the actual scale the system actually runs at.

A useful gut check: if you can't name a specific, concrete problem this architecture solves for your system — a real cross-service consistency issue you've hit, a real query pattern a normalized schema can't serve fast enough, a real audit requirement — that's usually a sign a simpler design would serve better, at least for now. These patterns are genuinely valuable tools, not a default starting point for every new service.

A few directions are worth exploring once the core patterns here feel comfortable, each addressing a real gap that a complete, production-grade system eventually runs into.

  • TCC (Try-Confirm/Cancel). A more structured alternative to free-form saga compensation, common in payment-industry systems, where every step is explicitly split into a reversible "try" phase and a separate "confirm" or "cancel" phase — worth comparing directly against the compensation patterns covered here.
  • Deadline scheduling for sagas. Real sagas need to handle the case where an expected event simply never arrives — a downstream service goes down permanently, a message gets lost. Scheduling a saga to time out and compensate automatically if a step doesn't complete within a set window closes a real gap that a saga without any timeout handling leaves open.
  • Using a distributed log like Kafka as the event bus. For very high-throughput systems, distributing events through a system built specifically for high-volume, ordered, partitioned streaming can outperform a purpose-built event store's own distribution mechanism, at the cost of additional operational complexity.
  • Distributed tracing across sagas. Correlating a saga's full multi-service journey — every command, every event, every service it touched — using a shared correlation ID makes debugging a failed or slow multi-step process dramatically easier in a system with many services.

None of these are required to build a correct, working event-driven system using everything covered so far. They're the next layer of maturity once the fundamentals — aggregates, projections, sagas, the outbox, snapshots, replay — are second nature.

Q: A customer closes their last account, and active cards need cancelling and the credit bureau needs notifying — what pattern coordinates this, and which variant would you likely reach for? A: A saga. With a small, well-defined number of steps like this, choreography is a reasonable starting point; if the process is expected to grow more steps or need a clear audit trail, orchestration is the safer long-term choice.

Q: What keeps the event store and application database from silently disagreeing after a crash? A: A transactional outbox — persisting state and the outbox record in the same local transaction, then publishing the event separately, guarantees the event eventually gets published if and only if the state change actually committed.

Q: A read model has been serving subtly wrong data for months due to a projection bug. What's the fix? A: Correct the bug in the projection's code, delete the read model, and trigger an event replay — the permanent event history, now processed by the fixed code, rebuilds a fully correct read model automatically.

Q: Which pattern keeps a heavily used, long-lived aggregate fast to load without discarding any of its history? A: Snapshots — Axon loads the most recent snapshot directly and replays only the events that happened after it, instead of replaying the aggregate's entire history every time.

Q: A screen needs live-updating data combined from four different services. What two patterns combine to serve this well? A: A materialized view to combine events from all four services into one fast, purpose-built read model, and a subscription query on top of it to push updates to the client the instant that view changes.

Q: What's the actual difference in coupling between choreography and orchestration? A: In choreography, services are only coupled to event types, not to each other directly. In orchestration, the orchestrator is explicitly coupled to every participant it coordinates, in exchange for making the whole process visible and centrally auditable.

Want a visual for this concept?

Generate a diagram tailored to “Capstone: The Complete Event-Driven Bank” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Practice interview questions on this topic →← Back to all Event-Driven Microservices chapters