Event-Driven Microservices Interview Questions
CQRS, Event Sourcing, and the Saga pattern, built end to end with Axon Framework and Spring Boot -- learned by assembling one event-driven banking system, piece by piece, not by memorizing pattern names.
← Learn this topic from scratch firstEvent Processors: Subscribing vs Tracking
What is the difference between a subscribing and a tracking event processor in Axon?
intermediateA subscribing event processor receives events synchronously, on the same thread that published them, the instant they're applied — the handler runs inline, before the originating command is considered fully finished. A tracking event processor runs asynchronously on its own thread and keeps a durable token recording exactly which event it last processed, letting it catch up at its own pace. Because the command only waits for the event to be stored, not for a tracking handler to finish, tracking decouples projection speed from command speed. Tracking processors can also resume from an exact position after a restart and can replay event history on command, neither of which a subscribing processor can do. Axon defaults new processors to tracking mode precisely because these benefits apply to most real projections.
Why is it risky to perform slow operations inside a subscribing event handler?
intermediateA subscribing event handler runs synchronously, inline, on the same thread as the command that published the triggering event, and it runs before that command is considered fully handled. If the handler performs something slow — a heavy database write, a call to an external service, a large computation — that slowness is added directly to the time the original caller waits for a response. There is no queue or buffer absorbing the delay, because the handler is not decoupled from the command at all. This is exactly the opposite of what most projections want: a read-model update should ideally happen without making every write to the system feel slower. For this reason, subscribing processors are reserved for narrow cases where a handler genuinely must complete before the command returns, rather than used as a general default.
What does a tracking event processor's token allow you to do that a subscribing processor cannot?
intermediateA tracking event processor's token is a durable record of exactly which event, in which segment, it last processed. Because that position is stored durably rather than kept only in memory, two things become possible. First, if the application restarts or crashes, the processor can resume from its exact last position instead of starting over or missing events that arrived while it was down. Second, an operator can deliberately reset the token and have the processor replay the entire event history, or any portion of it, from scratch — useful for rebuilding a read model after a bug fix or adding a new column that needs to be backfilled from history. A subscribing processor has no durable position at all; it only reacts to events as they're published live, so neither restart recovery nor replay is available to it.
The Database-per-Service Problem
Why do microservices architectures typically give each service its own private database instead of sharing one database across all services?
beginnerGiving each service its own database preserves four benefits that a shared database quietly destroys: independent deployment (a team can change its schema without coordinating with every other team), independent scaling (only the databases under real load need to scale up), fault isolation (an outage in one service's database doesn't take down unrelated services), and technology freedom (each service can pick the storage engine that actually fits its data). A shared database forces every schema change, scaling decision, and outage to ripple across every service that touches it, even ones that had nothing to do with the original problem. The trade-off is that combined, cross-service data access — like showing one customer's full profile pulled from several services — stops being a single simple query and has to be handled deliberately instead.
What new problems does splitting a single shared database into one database per microservice actually introduce?
beginnerThree problems appear almost immediately once databases are split apart: cross-service queries, where a screen needs data owned by several services at once and there's no longer one database to query for all of it; data consistency across services, where a single business action that touches multiple services can partially succeed, leaving the system in an inconsistent state with no built-in mechanism to catch or undo it; and data duplication, where a service that keeps its own local copy of another service's data risks that copy silently drifting out of sync over time. None of these problems existed when everything lived in one database with one shared transaction boundary — they are the direct cost of gaining independent deployment, scaling, and fault isolation. Every one of them has established solutions, but none of them is optional to address once a system is split this way.
Is it ever acceptable for one microservice to read directly from another microservice's database tables as a shortcut?
beginnerIn general, no — this is considered a serious anti-pattern in microservices design, even though it looks like a fast, easy fix. The moment one service can reach directly into another's tables, the schema is no longer private, which means the owning team can no longer change it freely without breaking a hidden dependency they may not even know exists. It also re-couples the two services' scaling and availability: a slow query or an outage in one service's database can now degrade a completely unrelated service that only happens to share the same physical database. The correct approach is for the service that needs data it doesn't own to go through the owning service's API — accepting the cost of a network call in exchange for keeping ownership, and the benefits that come with it, genuinely intact.
Meet Axon Framework & Axon Server
What is the difference between Axon Framework and Axon Server, and why are they separate components?
intermediateAxon Framework is a Java library added as a dependency to a Spring Boot service; it provides annotations like @Aggregate, @CommandHandler, and @EventSourcingHandler that you code against directly inside your application. Axon Server is separate, standalone infrastructure that your service connects to over gRPC; it acts as both the durable event store and the message router that delivers commands, events, and queries to the correct handlers, potentially across several different microservices. They're kept separate so that the routing and storage concerns — which need to be shared and consistent across an entire system of services — aren't duplicated inside every individual service's own process. A service can be redeployed or restarted without losing any event history, because that history lives in Axon Server, not in the service's own memory or local database.
In a system with several microservices, how does an event published by one service end up reaching a handler running inside a completely different service?
intermediateEvery microservice in the system connects to the same shared Axon Server instance over gRPC, rather than connecting directly to each other. When a service publishes an event, it sends it to Axon Server, which durably stores it in the event store and then routes it outward to every handler registered to receive events of that type — regardless of which service that handler happens to be running in. The publishing service never needs to know which other services exist, where they're deployed, or who's listening; it only needs to know about Axon Server. This is what lets microservices stay decoupled from each other at the network level while still reacting to each other's events, and it's also what makes adding a brand-new consumer of an existing event stream possible without changing the publishing service at all.
Your Spring Boot service throws connection errors on startup right after you add the Axon Spring Boot starter. What is the most likely cause, and how would you confirm it?
intermediateThe most common cause by far is simple ordering: the Spring Boot service started before Axon Server itself was up and healthy, so the gRPC connection on port 8124 that Axon expects to establish at startup fails, producing alarming-looking connection and retry errors in the logs. This does not usually mean your service's own configuration is wrong. The fastest way to confirm it is to open Axon Server's web dashboard at http://localhost:8024 in a browser; if that page doesn't load or shows the server as unhealthy, the fix is to get Axon Server running and confirmed healthy first, then start or restart the Spring Boot service, rather than digging through the service's own axon.axonserver.servers configuration for a bug that likely isn't there.
Choreography Saga
How does a choreography saga coordinate a multi-step business process without any central coordinator?
intermediateIn a choreography saga, no single service holds the full sequence of steps — instead, each service listens for events published by the services before it, performs its own local unit of work, and publishes its own event once it's done. The next service in line reacts to that new event the same way, and the chain continues purely through this listen-react-publish relay until nothing further gets triggered. For example, a Loans service publishing LoanApprovedEvent causes an Accounts service to deposit funds and publish FundsDepositedEvent, which in turn causes a Customer service to update a credit profile. Because each service only needs to know about the event types it listens for, not about who published them or who's downstream, the participating services stay loosely coupled — the trade-off is that the overall plan only exists as an emergent pattern across several independent listeners, not as a single readable definition anywhere in the code.
How do you implement a compensating transaction in a choreography saga?
intermediateCompensation in a choreography saga uses exactly the same mechanism as the happy path: a service adds a plain event listener for the relevant failure event, and reacts by sending a command against its own aggregate to undo the effect of its earlier step. For instance, if Accounts Service fails to deposit funds and publishes DepositFailedEvent, Loans Service listens for that event and sends a CancelLoanCommand against its own LoanAggregate, cancelling the loan it approved earlier. No special saga-aware mode is required — the service that needs to compensate simply adds one more listener alongside whatever others it already has. Because events can be redelivered, the compensating command handler should also be idempotent, so it behaves correctly even if the failure event arrives more than once.
What is the main risk of choreography sagas as the number of participating services grows, and how would you recognize you've hit it?
intermediateAs a choreography saga's step count grows past roughly four or five services, the overall plan stops being traceable from any single place — understanding the full flow requires manually opening every participating service and tracing which listeners react to which events, since no code artifact describes the sequence directly. You've likely hit this point when a simple question like 'what happens, in order, when this business event occurs?' takes real investigative effort across multiple codebases to answer, or when onboarding a new engineer to the flow requires a verbal walkthrough rather than pointing them at one file. A related symptom is debugging incidents becoming slow specifically because tracing a failure back to its root cause means hopping between several unrelated services' listener classes. When this happens consistently, it's usually a sign to migrate that specific process to an explicit orchestrator, which centralizes the plan and its failure handling into one readable component.
Designing Commands, Events & Queries
Why can a command be rejected but an event cannot?
intermediateA command represents a request for something to happen, and requests can fail — the business rules might disallow it, so the command handler throws and nothing changes. An event, by contrast, represents something that has already, definitively happened; by the time an event object exists and is being processed, whatever it describes has already occurred and is now a permanent part of the system's history. Rejecting an event after the fact wouldn't make sense, because there's no request left to refuse. This is also why an @EventSourcingHandler method should never throw an exception for a business-rule reason: all decision-making about whether something is allowed happens earlier, in the command handler, before any event is ever applied.
What does the @TargetAggregateIdentifier annotation do on a command, and why doesn't a query need an equivalent annotation?
intermediate@TargetAggregateIdentifier marks the field on a command that tells Axon which specific aggregate instance the command should be routed to and handled by — for example, the customerId field on a command meant for one particular customer. Axon uses that field's value to find, or in a creation command's case start, the exact matching aggregate instance. Queries don't need an equivalent annotation because they aren't routed to an aggregate at all; aggregates only ever handle commands and their own events. A query is instead routed to whichever component — typically a projection reading from a fast, pre-built read model — actually knows how to answer that question, and no aggregate ever gets involved in answering a read.
Why does a strict, tense-based naming convention for commands, events, and queries matter more on a real team than it might seem?
intermediateOn a codebase with many message classes, a consistent convention — imperative names for commands, past-tense names for events, question-phrased names for queries — lets any engineer read a class name and instantly know its behavior contract without opening the file: whether it can be rejected, whether it's a permanent fact, or whether it's a side-effect-free read. Without that convention, a name like UpdateCustomer is ambiguous — it could be a request that might fail or a fact that already happened, and the only way to find out is to read the handling code and trace how it's used. On a large team where many engineers are adding new message types independently, this ambiguity compounds quickly and makes the codebase noticeably harder to navigate, while a consistently enforced convention keeps that cost from ever building up.
Reading Events Directly from the Event Store
How do you look up all events recorded for a specific aggregate in Axon Server?
intermediateAxon Server's dashboard, typically reachable at http://localhost:8024 in a local setup, includes a search section that lets you query the event store directly. Searching by aggregateIdentifier, for example aggregateIdentifier: "customer-123", returns every event stored for that one aggregate, in strict sequence order, exactly as it was persisted. The same lookup works for any aggregate type — accounts, cards, loans — since every aggregate annotated with @AggregateIdentifier gets its own independent, ordered event stream. This gives you a direct view of the source of truth, with no projection, cache, or application code standing between you and what was actually recorded.
What does the sequence number on a stored event guarantee?
intermediateEach event stored for an aggregate carries a sequence number — 0, 1, 2, and so on — that records its exact position in that aggregate's history. Axon stores and retrieves events strictly by this sequence, which guarantees that replaying an aggregate's history always processes its events in the exact order they truly happened, and therefore always rebuilds the same, correct final state. This holds even if multiple commands raced against the same aggregate concurrently, because the sequence numbers still resolve into one strict, final order at the moment events are persisted. Without this guarantee, replay would be non-deterministic, and the same event history could rebuild different final states on different runs.
Why should you check the raw event store before adding debug logging when an aggregate's state looks wrong?
intermediateThe raw event store is the single most direct, most reliable source of truth available in an event-sourced system — every other view of the data, including a read model or an application log, is a derived, secondhand account of what actually happened. Reading the raw sequence for the affected aggregate answers a key diagnostic question in one lookup: if the recorded events themselves tell a clear, correct story, the bug lives downstream, typically in a projection; if the events themselves already look wrong, the bug lives upstream, in a command handler. This turns debugging into a fast binary search instead of a slow crawl through logs or a debugger attached to a running process, and it's exactly the habit experienced Axon developers reach for first.
Cross-Service Queries & API Composition
What is the API composition pattern and when would you reach for it?
beginnerAPI composition is a pattern where one component — often built into an API gateway or a dedicated backend-for-frontend service — receives a single incoming request, calls each of the individual microservices that owns a relevant piece of data, waits for all of their responses, and merges them into one combined response for the client. It's the right first choice for any read that needs data from a small number of services (roughly two to four) and doesn't require complex cross-service sorting, filtering, or extremely low latency. It keeps every individual service simple, since none of them needs to know the others exist — all of the cross-service awareness lives in exactly one place, the composer.
Why should a composed endpoint call multiple downstream services concurrently instead of one after another?
beginnerCalling services sequentially means the total response time is the sum of every individual call's time — if four services each take 100 milliseconds, a sequential composer takes close to 400 milliseconds. Calling them concurrently, for example using Mono.zip with Spring's reactive WebClient, fires all the requests at essentially the same instant and waits only as long as the slowest single call, bringing that same scenario down to roughly 100 milliseconds. For a screen where a real user is actively waiting for a response, that difference is often the gap between an interface that feels instant and one that feels noticeably slow, even though both approaches fetch exactly the same data from exactly the same services.
What are the main limitations of the API composition pattern, and what do teams usually reach for once they hit them?
beginnerAPI composition struggles with three specific things: sorting or filtering a combined result across services, since you can't ask a database to sort data it never had in the first place, forcing that work into slow, memory-heavy application code; a response time permanently capped by the slowest single downstream dependency, no matter how well the concurrent calls are implemented; and partial failures, where one downstream service being unreachable forces an awkward decision about what an incomplete response should even look like. Once a system genuinely runs into these limits — needing fast, flexible, cross-service queries at real scale — teams typically move toward CQRS with a purpose-built, pre-aggregated read model instead of assembling the same data fresh on every single request.
Orchestration Saga
What problem does an orchestration saga solve that a choreography saga does not?
intermediateAn orchestration saga solves choreography's core weakness: the lack of a single, readable place where a multi-step process's full plan and failure handling actually live. Instead of the sequence emerging implicitly from several independent services each reacting to one event, a dedicated orchestrator component explicitly holds the entire sequence of steps and tells each participating service exactly what to do next. This makes the process auditable — a reviewer can open one class and see every step and every compensation path — at the cost of the orchestrator now being explicitly coupled to every service it coordinates. It's the right trade for longer or more complex flows, or ones where a clear audit trail genuinely matters, where choreography's scattered, implicit plan becomes hard to reason about.
What does associationProperty do on a @SagaEventHandler, and why is it necessary?
intermediateassociationProperty tells Axon which field on an incoming event to match against, so the event gets routed to the correct in-flight saga instance rather than an unrelated one. It's necessary because a real system typically has many instances of the same saga running concurrently — hundreds of loan approvals in progress at once, for example — and Axon needs a reliable way to know which specific saga instance a given event, like a FundsDepositedEvent for a particular account, actually belongs to. By declaring associationProperty = \"accountId\", the saga tells Axon to route that event to whichever saga instance is currently associated with that exact account ID. Without it, or with it set incorrectly, events can get routed to the wrong saga instance, or fail to find a matching instance at all, silently breaking the coordination the saga is supposed to provide.
How would you decide between choreography and orchestration for a new multi-service business process?
intermediateThe main factors are step count, coupling tolerance, and how much an explicit audit trail matters. For a short process — two or three loosely related steps — choreography is usually the simpler choice: it keeps participating services decoupled from each other and avoids introducing an extra orchestrating component for a plan that's still easy to hold in your head. Once a process grows past roughly four or five steps, involves multiple distinct failure paths each needing its own compensation, or needs to be clearly auditable end to end, orchestration becomes the better fit, since it makes the entire sequence and its failure handling visible in one component, in exchange for that orchestrator being explicitly coupled to every participant it coordinates. A common, pragmatic pattern in real teams is to start new processes with choreography and deliberately migrate to orchestration once the implicit plan starts becoming genuinely hard to trace.
Subscription Queries & Event Replay
How does a subscription query deliver live updates to a client without polling?
intermediateA subscription query stays open after returning its initial result and relies on the same event-handling pipeline that already keeps a projection's read model up to date. When a tracking event processor delivers a new event to the projection's existing @EventHandler and that handler updates the read model, Axon checks whether any open subscription queries are currently watching that same piece of data and, if so, pushes the freshly updated result directly to them. On the server side, this is typically exposed as a reactive stream — such as a Flux over Server-Sent Events — combining the initial result with a continuous stream of subsequent updates. Because it piggybacks entirely on the projection's existing event handling, no new event-handling code needs to be written specifically to support live updates; it's a capability that comes essentially free once the projection already exists.
Why is it safe to delete a projection's read model table and rebuild it from scratch?
intermediateIt's safe because a projection is a derived, disposable calculation over the event store, never the actual source of truth. The event store holds the complete, permanent history of everything that has ever happened, and a projection is nothing more than that history processed through the projection's event-handling logic into a query-friendly shape. Deleting the read model and resetting the relevant tracking event processor's token causes Axon to replay every event from the very beginning, rebuilding the read model exactly as it would have looked had the current projection code been running from day one. This is what makes it possible to fix a bug discovered in a projection's logic months after it went live: correct the code, delete the read model, and replay — no manual data migration required, and no risk of missing a row.
What operational risks should you consider before triggering a full event replay on a production system?
intermediateThe two main risks are load and staleness. Replaying a projection's full history means the event store has to serve every event from the beginning, on top of whatever normal live traffic it's already handling — for a large event store, that's a meaningful, sustained read load that can affect other consumers. Meanwhile, the read model being rebuilt is temporarily incomplete or stale for the duration of the replay, so anything querying it during that window may see partial or outdated results. The practical mitigation is to treat a replay as a planned operation rather than a casual one: run it during a low-traffic window, monitor event store load while it's in progress, and communicate to anything depending on that projection that results may be temporarily stale. For a very large event history, it's also worth estimating how long the replay will actually take before triggering it against a live production system.
Data Consistency & Duplication Challenges
Why can't a regular database transaction guarantee consistency for a business action that spans three separate microservices?
beginnerA regular database transaction only has authority over one database connection, and commits or rolls back changes within that single database. When a business action like a loan approval needs Loans to create a record, Accounts to deposit funds, and Customer to update a credit profile, those are three separate databases with three separate transaction boundaries, and no single COMMIT or ROLLBACK statement can span all three at once. If Loans successfully commits its part and Accounts then fails, there is no automatic mechanism to undo what Loans already did — the system is left in a partially completed state unless something was explicitly built to detect and correct it. This is exactly the gap that patterns like the saga pattern are designed to close.
Why do modern microservices architectures generally avoid Two-Phase Commit for cross-service consistency?
beginnerTwo-Phase Commit requires a coordinator to get every participating database to 'prepare' its part of a change and hold locks on the affected data until a final commit or rollback decision arrives. This forces every participant to stay synchronously available for the full duration of the process, and if the coordinator itself crashes partway through, participants can be left holding locks indefinitely with no decision ever arriving. That directly conflicts with the reasons services were split into independent databases in the first place — independent availability and independent scaling — since a service can no longer be truly independent if it might be blocked waiting on a coordinator elsewhere. Most microservices architectures instead use a sequence of local, independent transactions with an explicit compensation plan for undoing earlier steps if a later one fails, commonly known as the saga pattern.
When is it acceptable for one microservice to keep a local copy of data owned by another service?
beginnerDuplicating data across services is acceptable, and even a well-established best practice, when it's done deliberately and kept in sync through a reliable mechanism such as a stream of events — for example, an Accounts service keeping a local copy of a customer's name, updated whenever a name-change event arrives, so it doesn't need a network call to Customer service on every single request. What's genuinely risky isn't duplication itself; it's duplication nobody planned for, with no explicit synchronization mechanism keeping the copies honestly aligned over time. The trade-off to weigh consciously is speed and availability against a brief, bounded window of staleness — usually a completely acceptable trade as long as the staleness window is small and predictable.
Building the Aggregate — the Write Model
What is an aggregate responsible for in an event-sourced Axon application, and why is it described as the one authority for its domain concept?
intermediateAn aggregate is the single place in the system responsible for deciding whether a command touching a given entity — one customer, one account, one loan — is valid, and it is the only thing allowed to say a command can proceed by applying an event. All the business rules about that entity live inside its aggregate class's command handlers, and nowhere else is permitted to change that entity's state directly. This concentration matters because it gives the codebase exactly one place to look when asking under what conditions a given change is allowed, instead of that logic being scattered across controllers, services, and database constraints. Each aggregate is also scoped narrowly to one kind of entity — a CustomerAggregate does not enforce loan rules — keeping each aggregate's responsibility small and its event history focused purely on that one entity's lifecycle.
What exactly happens when a command handler calls AggregateLifecycle.apply(event) inside an aggregate?
intermediateCalling apply() does two distinct things in the same call. First, it publishes the event outward into the rest of the system — it gets durably stored and becomes available to any other component that's listening for it, such as a projection updating a read model. Second, it immediately routes that same event back to this aggregate instance's own matching @EventSourcingHandler method, updating the instance's in-memory fields right away, before the command handler method even returns. That second effect is what lets the aggregate keep its own state correct for the very next command that might arrive in quick succession, without needing to reload from anywhere in between. It also means any actual state change in an aggregate should only ever happen inside an event sourcing handler reacting to an applied event, never as a direct field assignment sitting inside the command handler itself.
A teammate writes a @CommandHandler that validates a command and then sets this.name = command.getName() directly, without applying any event. What is wrong with this, and what will actually happen at runtime?
intermediateThis breaks the event sourcing model at its foundation: in an event-sourced aggregate, state is only ever supposed to change as a result of replaying events through @EventSourcingHandler methods, never through a direct field assignment inside a command handler. At runtime, the immediate effect looks fine — the field really is set on that in-memory instance, and any command handled right afterward in the same session sees the updated value. The bug surfaces the next time this aggregate has to be rebuilt from its event history, which happens routinely whenever a fresh instance is loaded to handle a new command: since no event was ever applied for that name change, it isn't part of the aggregate's event history, and the rebuilt instance's name field reverts to whatever it was before the direct assignment, silently losing the change.
CQRS Approaches, Rolled Out Across the Bank
What are the different structural approaches to implementing CQRS, from simplest to most complete?
intermediateThere are five real structural flavors. The simplest is a single shared model on a single database, which only separates the API into write and read paths without gaining CQRS's other advantages. Next is separate command and query models sharing one database, which still commits changes atomically but lets each side use a model shaped for its own job. Third is separate models on separate databases without event sourcing, which allows independent scaling but risks events arriving at the read side out of order. Fourth is CQRS combined with event sourcing on separate databases, which fixes that ordering risk through strict per-aggregate sequencing and enables reliable replay and independent scaling, at the cost of real operational complexity. Fifth is event sourcing with the event store and read model sharing one database, trading independent scaling for stronger, immediate consistency while keeping most of event sourcing's benefits.
What problem can occur when you separate the write and read databases without using event sourcing?
intermediateWithout event sourcing's strict, sequenced event log, an event bus delivering messages between a write database and a read database has no built-in guarantee of ordering. A later event, such as a withdrawal, could in principle reach the read side before an earlier event, such as the deposit that preceded it, since ordinary message delivery doesn't enforce per-aggregate sequencing on its own. This can silently corrupt a read model — a running balance computed incrementally in the wrong order produces a wrong result, with no error surfaced anywhere. Event sourcing fixes this by assigning every event for a given aggregate a strict, increasing sequence number the moment it's stored, so a projection always processes that aggregate's events in the exact order they truly happened.
Why does a fully event-sourced CQRS implementation with separate databases justify its added complexity?
intermediateThis flavor is the only one that provides strict per-aggregate event ordering, which closes the silent out-of-order corruption risk present when separate databases are kept in sync without event sourcing. It also uniquely enables reliable replay of history to rebuild a read model from scratch, and genuinely independent scaling and technology choice for the write and read sides, since they're fully separate systems connected only by an event stream. For a system with real regulatory, auditing, or high-throughput scaling needs, such as a bank's account and loan services, these specific capabilities are worth the added operational cost of running a dedicated event store and a framework like Axon to manage it. For a system without those needs, a simpler flavor is a perfectly reasonable choice instead.
Event Sourcing Handlers & Rebuilding State
Why must an @EventSourcingHandler method never throw an exception for a business-rule reason?
intermediateBy the time an @EventSourcingHandler method runs, the event it's reacting to represents something that has already, definitively happened and, in most real flows, is already durably stored in the event store. There's no remaining decision to make — that decision was made earlier, inside the @CommandHandler that chose to apply this event. If the event sourcing handler threw an exception, Axon would have no sensible way to recover: the event can't be un-stored, but the aggregate's in-memory state also can't be made to reflect it. Worse, since this same method also runs during a full replay of the aggregate's history, a thrown exception there wouldn't just fail once — it could make the aggregate unable to load at all going forward, since every future replay would hit the identical exception. All genuine validation belongs strictly in the command handler, before any event is ever applied.
When a command arrives for an aggregate that already has prior events, how does Axon determine that aggregate's current state before handling the new command?
intermediateThere is no persisted 'current row' for the aggregate anywhere on the write side. Instead, Axon retrieves every event ever stored for that specific aggregate identifier, instantiates a brand-new, empty instance of the aggregate class using its no-argument constructor, and replays each stored event through the matching @EventSourcingHandler methods, in the exact order those events originally occurred. Only after that full replay completes does the new command handler actually run, now operating against a freshly rebuilt instance whose fields reflect the complete prior history. Axon does cache aggregate instances in memory to avoid replaying from scratch on every single command in quick succession, but the underlying model — state as the derived result of replaying history — holds true regardless of that caching optimization.
Where does AggregateLifecycle.markDeleted() get called from, and why not from the @CommandHandler that handles the delete request?
intermediateAggregateLifecycle.markDeleted() is called from inside the @EventSourcingHandler method that reacts to the deletion event (for example, on(CustomerDeletedEvent event)), not from the @CommandHandler that initially handles the delete command. This follows the same command/event separation used everywhere else in an aggregate: the command handler's only job is to decide whether the deletion is allowed and, if so, apply a deletion event; the actual state change — including marking the instance as finished — belongs in the event sourcing handler, since that's the method responsible for turning an already-decided event into an actual change on the aggregate. Calling markDeleted() from the command handler directly would bypass the pattern that keeps every real state change flowing through event sourcing handlers, which is exactly the pattern that makes an aggregate's state reproducible purely by replaying its events.
Introduction to CQRS
What does CQRS stand for and what is the core idea behind it?
beginnerCQRS stands for Command Query Responsibility Segregation. Its core idea is to separate the part of a system that handles commands — requests to change something — from the part that handles queries — requests to read something — and let each one be modeled and optimized entirely independently, rather than forcing one shared model to serve both jobs. The command side is optimized for correctness: validating business rules and deciding whether a change is allowed before applying it. The query side is optimized for speed and flexibility: answering questions fast, shaped exactly like whatever screen or report is asking. The name sounds intimidating, but the underlying idea is simply that writing and reading are different jobs, and each deserves a model built for what it actually needs to do.
Does implementing CQRS require using two physically separate databases for reads and writes?
beginnerNo. At its core, CQRS is only about separating the models and code paths used for writing versus reading — it doesn't strictly require two separate physical data stores. A simple version can run entirely on one shared database, with a distinct command-side class handling validation and writes, and a distinct query-side class handling read-optimized queries, both pointed at the same underlying tables. A stronger, more powerful version uses two genuinely separate stores, often paired with event sourcing, which unlocks fully independent scaling of reads and writes and allows a purpose-built, denormalized read model instead of one general-purpose table trying to serve every kind of query. Both are legitimate forms of CQRS — the difference is how far a team chooses to take the separation.
Why might the model used to write data in a system look very different from the model used to read it?
beginnerA write model exists to enforce correctness: it needs to validate business rules, like checking whether a loan amount is within policy or whether an account has sufficient funds, and it's usually shaped around those rules rather than around any particular screen. A read model exists to answer questions fast, and it's usually shaped exactly like whatever is consuming it — a dashboard that needs a list of overdue loans sorted by amount, or a profile screen that needs one customer's accounts, cards, and loans combined into a single object. Trying to serve both needs from one shared model tends to leave both jobs done worse than they would be done separately, which is exactly the imbalance CQRS is designed to fix by letting each model be optimized for its own actual purpose.
The Materialized View Pattern
What is a materialized view in an event-driven microservices architecture?
intermediateA materialized view is a dedicated, pre-combined read model built ahead of time by listening to events, rather than assembled on demand at request time. Instead of a request handler calling several services fresh every time it needs combined data, a listener component subscribes to the relevant events as they happen and merges them into one ready-to-serve record. A request for that data then becomes a single, instant lookup against one table, with no cross-service calls, no request-time composition, and no dependency on every underlying service being available and fast at that exact moment. The trade-off is that the view is eventually consistent, since it's updated asynchronously as events arrive rather than reflecting the absolute latest state at the instant of the read.
How does a materialized view differ from an ordinary CQRS projection?
intermediateAn ordinary CQRS projection keeps one service's own read model in sync with events published by that same service's own aggregate — an account service's balance view listening only to its own account events, for example. A materialized view deliberately combines events published by several different aggregates across several different microservices into one unified record. The distinguishing feature isn't the mechanism, since both use event handlers to update a read model, it's the scope: a projection answers a question one service can answer alone, while a materialized view answers a question that genuinely spans service boundaries, such as a customer profile screen needing data from customer, account, card, and loan services at once.
What makes it possible for a single component to build a read model from events published by several different microservices?
intermediateA shared event-streaming backbone, such as Axon Server, is what makes this possible. Every microservice already publishes the events its own aggregates produce through that same shared hub, regardless of who might be listening. Because of that, any component in any service can register event handler methods for any event type it's interested in, entirely independent of which service originally produced that event, without any direct coupling, shared code, or synchronous API call between the two services involved. A materialized view's listener simply registers handlers for several unrelated event types at once — say, one published by a customer aggregate and another published by an account aggregate — and merges whatever arrives into one combined record per customer.
Snapshots in Event Sourcing
Why does replaying an aggregate's full event history on every command become a real problem, and how do snapshots fix it?
intermediateEvery command against an aggregate requires reconstructing its current state, and without a snapshot, that means replaying every event the aggregate has ever applied, from the very first one, on every single load. For an aggregate with a handful of events, this is instant and unnoticeable; for a long-lived, heavily used aggregate — like a bank account with fifty thousand transaction events accumulated over years — replaying all of that history on every command becomes a real, measurable performance cost, since most of it hasn't changed in a long time. A snapshot fixes this by periodically saving a copy of the aggregate's state at a specific point in its history, so loading the aggregate becomes 'load the snapshot directly, then replay only the events that happened since' instead of replaying everything from the beginning. This turns loading cost from something that grows with an aggregate's entire lifetime event count into something that only grows with events since the most recent snapshot.
Does enabling snapshots change what data is stored as the permanent source of truth?
intermediateNo. Enabling snapshots doesn't delete, replace, or modify any of the original events — the complete, permanent event history remains fully intact in the event store, exactly as it would without snapshotting. A snapshot is purely an additional, disposable optimization that sits alongside that history, used only to speed up aggregate loading. Every snapshot could be deleted at any time and the system would keep working correctly, just slower, falling back to replaying full event history on every load exactly as it did before snapshotting was configured. This matters because it means snapshotting is never a risky operation from a data-integrity standpoint — it's purely a performance optimization layered on top of a source of truth that never changes.
How would you decide which aggregates in a system actually need snapshotting?
intermediateSnapshotting is worth configuring for aggregates that genuinely accumulate a large number of events over a long lifetime — a long-lived, actively used bank account is the canonical example — where replaying full history on every load has become, or is likely to become, a measurable performance cost. It's not worth configuring by default for every aggregate type, since aggregates that stay small over their lifetime, like a customer profile with a handful of events, gain nothing from snapshotting and just pay unnecessary storage and write overhead for every snapshot taken. A practical approach is to leave snapshotting off until profiling or production metrics show aggregate-loading time becoming a real bottleneck for a specific aggregate type, then configure a trigger threshold — typically an event count — sized to that aggregate's actual event volume, rather than guessing or applying one blanket setting everywhere.
Advantages & Disadvantages of CQRS
What are the genuine advantages of adopting CQRS in a system that actually needs it?
beginnerCQRS offers independent scaling of the read and write sides to match their real, often very different traffic patterns; optimized models for each job, letting the write side stay strict and focused on business rules while the read side is shaped exactly like the screens it serves; simpler, more focused queries, since a purpose-built read model doesn't need complex joins at request time because that combining work already happened ahead of time; and better security boundaries, since it's easier to grant read-only access to a structurally separate read side. These benefits compound in a system under real load — a read model already shaped like its screen, running on infrastructure scaled specifically for reads, can respond in single-digit milliseconds even under heavy traffic, something difficult to achieve from a single shared model also trying to enforce strict write-side validation.
What does eventual consistency mean in the context of CQRS, and why does it happen?
beginnerEventual consistency means that after a write happens on the command side, there's a brief window during which the read side hasn't caught up yet, because it's updated by reacting to changes from the write side rather than updating in the exact same instant. A user who creates a loan and immediately refreshes a dashboard reading from the query side might briefly see stale data until the read side finishes processing the update. This isn't a bug or a sign of a broken implementation — it's a direct, unavoidable consequence of having two separate models updated at two separate points in time. Systems that genuinely cannot tolerate any staleness at all are a signal that CQRS may need careful extra design work, or may not be the right fit for that particular part of the system.
When should a team avoid adopting CQRS, even though it's a well-known, respected pattern?
beginnerA team should generally skip CQRS for a small internal tool, an early-stage product's core CRUD functionality, or any system where reads and writes have genuinely similar shape and similar traffic volume. In these cases, CQRS adds real costs — more moving parts, a steeper learning curve, eventual consistency to reason about — without providing any of its actual benefits, since there's no meaningful scaling mismatch or query complexity to solve. A practical, low-risk alternative is to start with a simple, single-model service and only introduce CQRS later, for the specific parts of the system that show clear, concrete signs of needing it, such as a slow reporting query or a read path becoming a genuine scaling bottleneck.
Capstone: The Complete Event-Driven Bank
Given a new requirement — when a customer closes their last account, cancel any active cards and notify the credit bureau — how would you design this as a saga, and would you choose choreography or orchestration?
advancedThis is a short, linear process — close the account, cancel any active cards, notify the credit bureau — so it's a reasonable candidate for either pattern, but the deciding factor is how many steps it's likely to grow into and whether an audit trail matters. Modeled as choreography, an Account service would publish an AccountClosedEvent when the last account closes, a Card service would listen and cancel any active cards while publishing a CardsCancelledEvent, and a credit bureau notification service would listen for that and send the notification — each service knowing only about the event it reacts to. If this process needs to guarantee, auditably, that a notification never gets skipped, or if it's expected to grow additional steps over time (say, closing a rewards account or sending a farewell communication), an explicit orchestrator holding all three steps and their compensations in one saga class is the safer long-term choice, since it keeps the entire process and its failure handling readable in one place as it grows.
In a complete event-driven system, what guarantees the event store and application database stay consistent when a command handler needs to both persist state and publish an event?
advancedThe transactional outbox pattern provides this guarantee. Instead of persisting state and publishing an event as two separate, independently-failing operations, the command handler writes the state change and an outbox record describing the event to be published in the same local database transaction — so both succeed together or neither does. A separate process then reads unpublished outbox records and publishes them as real events, retrying until publication succeeds, and marks them published once it does. This closes the dual-write problem: without it, a crash between persisting state and publishing the event can leave the database and event stream disagreeing about what actually happened, which quietly breaks the assumption that the event store is the single, authoritative source of truth everything else in the system depends on.
A team asks whether they really need event sourcing, CQRS, and sagas for a new service, or if a simpler CRUD approach would do. How would you help them decide?
advancedStart by asking whether they can name a specific, concrete problem each pattern would solve for this particular service, rather than adopting them because they sound more sophisticated. Event sourcing earns its cost when a genuine, permanent audit trail matters — financial or regulatory domains, for instance — or when being able to fix a data bug retroactively by replaying is a real, anticipated need. CQRS earns its cost when read and write load are different enough in shape or scale that optimizing them separately produces a measurable improvement, not a theoretical one. Sagas earn their cost when the service genuinely coordinates multi-step processes across other services with real failure modes that need explicit compensation. If the service is simple, largely CRUD-shaped, has a small team unfamiliar with these patterns, and a well-indexed relational schema would answer every query it actually needs to answer, a simpler approach will very likely serve the team better, at least until a concrete problem actually appears that these patterns are the right fix for.
If a live dashboard needs to reflect balances across multiple services in near real time, which combination of patterns would you reach for, and why?
advancedA materialized view combined with a subscription query is the right pair here. The materialized view combines events from every relevant service — accounts, loans, cards — into one denormalized, purpose-built read model designed specifically for this dashboard, avoiding the need to query across service boundaries at read time or stitch together several services' responses on every page load. A subscription query layered on top of that materialized view then pushes fresh results to the dashboard automatically the instant any of the underlying events update the view, instead of forcing the client to poll repeatedly for changes. Together, this gives a dashboard that's both fast to query (thanks to the materialized view doing the cross-service combination work ahead of time) and live-updating (thanks to the subscription query pushing changes as they happen), without any bespoke real-time infrastructure beyond what the projection and event-processing pipeline already provide.
The Transactional Outbox Pattern
What is the dual-write problem?
intermediateThe dual-write problem happens when a system needs to save a business change to a database and publish a message about that change to notify other services, and treats these as two separate, independent operations. Because they're two operations against two different systems, one can succeed while the other fails — most commonly, the database save commits successfully but the subsequent message publish fails due to a network issue or a crash at the wrong moment. The result is a change that's safely recorded but that nobody else in the system ever learns about, since the notification was lost. A classic real-world symptom is an order that exists in a database while the warehouse was never notified to ship it. Simply moving the publish call earlier or wrapping it in a try-catch doesn't fix the underlying issue, since the two operations remain fundamentally unable to succeed or fail together.
How does the transactional outbox pattern guarantee that a business change and its event are never inconsistent?
intermediateInstead of trying to make a database save and a message publish atomic with each other directly, which is hard because they're different systems, the pattern writes the event into a plain database table, the outbox, in the exact same local database transaction as the business change itself. Local database transactions are already atomic through ordinary database guarantees, so the business row and the outbox row either both commit or neither does — there's no window where one succeeds without the other. A separate relay process then reads unpublished outbox rows afterward and delivers them to a message broker, retrying as needed. Because the durable record of the event survived the one transaction that mattered, a crash in the relay or the broker only delays delivery; it never loses the event.
Why doesn't a typical Axon-based aggregate need a hand-built outbox table?
intermediateAn Axon-managed event store already behaves like a robust outbox by design. When a command handler calls AggregateLifecycle.apply(), the resulting event is durably stored as part of handling that command, in the same atomic operation, and Axon's own event processors are responsible for reliably delivering that event to every interested listener afterward, including retrying on failure. This is exactly the guarantee a hand-built outbox table and relay process would otherwise need to provide. The situation changes only when code writes to a plain, non-Axon-managed database and separately needs to publish a message through some other channel entirely — at that point Axon's event store isn't involved in the write at all, and the dual-write problem returns, requiring an explicit outbox table to solve.
Building the Query Side: Projections
What is a projection, and how does its job differ from an aggregate's job?
intermediateA projection is a Spring component with two responsibilities: reacting to events by keeping a simple, plain database table (the read model) up to date, and answering queries by reading directly from that same table. An aggregate, by contrast, is responsible for deciding whether commands are valid and producing events — it never stores current state directly and instead recomputes it by replaying its full event history on demand. A projection never replays anything; it maintains one always-current row per entity by applying events as they arrive, which is what makes reading through a projection a fast, ordinary database lookup instead of a replay operation. In short, the aggregate is the write side, optimized for correctness and auditability, while the projection is the read side, optimized purely for fast, simple queries.
What does 'eventual consistency' concretely mean in a system built with an Axon aggregate and a projection, and how should an API account for it?
intermediateConcretely, it refers to the small but real gap in time between an aggregate applying an event and the projection's @EventHandler method reacting to that event and updating its read model table. For most systems that gap is on the order of milliseconds, but it is never truly zero, since the event has to be published, delivered, and processed by the projection before the read model reflects it. If client code immediately issues a read right after a write completes, there's a genuine chance it reads stale or missing data, because the projection hasn't caught up yet. APIs built on this pattern should be designed with that window in mind — for example, having the write endpoint return enough information for a client to know a write was accepted without assuming an immediately following read will already reflect it, rather than treating a write as instantly visible everywhere.
Why is a projection's read model table considered disposable, and what practical benefit does that give a team?
intermediateA read model table has no direct relationship to the event store — it's an ordinary table holding no data that doesn't already exist, in a different shape, inside the event history. Because it's populated entirely by a projection's @EventHandler methods reacting to events, it can be dropped completely and perfectly rebuilt by replaying every past event for the relevant aggregates back through that same projection. Practically, this makes evolving read patterns low-risk: a team can add a brand-new projection to support a new feature, run one replay of the historical event stream to backfill it, and have it immediately caught up, with no changes needed anywhere on the write side. It also means a projection that's drifted out of sync due to a bug can be repaired by rebuilding it from the event store, rather than requiring manual data correction.
Introduction to Event Sourcing
What is event sourcing and how does it differ from traditionally storing just the current state of a record?
beginnerEvent sourcing stores every change to a piece of data as a permanent, ordered event — such as CustomerCreated, then NameUpdated, then EmailUpdated — rather than storing only the current row with its latest values. Current state is never saved directly; it's calculated by replaying every event for that object, in order, from the beginning, similar to recalculating a bank balance by re-adding every entry in a statement. A traditional system that overwrites a row loses all history the moment a new value replaces an old one, while an event-sourced system permanently preserves exactly what changed, when it changed, and often why. The trade-off is that reading current state now requires a calculation instead of a simple lookup, though real systems typically cache that calculation and only replay events since the last cached point.
Why is event sourcing usually paired with CQRS instead of being used entirely on its own?
beginnerEvent sourcing on its own is genuinely bad at answering simple read questions like 'show me this customer's profile,' because answering them could require replaying potentially hundreds of events just to reconstruct current state. Pairing event sourcing with CQRS solves this cleanly: the write side stores events as a perfect, permanent history, while a separate query side maintains a fast, ready-to-read copy that's kept up to date by listening to those same events as they happen. Each side ends up doing the job it's actually good at, rather than forcing one model to be both a complete historical record and a fast, queryable snapshot. This is why so many production event-sourced systems adopt CQRS alongside it, even though the two patterns are technically independent and neither one strictly requires the other.
Are event sourcing and CQRS the same pattern, or is it possible to use one without the other?
beginnerEvent sourcing and CQRS are separate, independently adoptable patterns that happen to pair very well together, but they solve different problems. Event sourcing is a decision about how the write side stores its history — as a sequence of events instead of just the latest state. CQRS is a decision about whether reads and writes get separate models and code paths at all, regardless of how either side stores its data. A system can use CQRS with traditional state storage on its write side and no events involved, and a system can use event sourcing without a separately optimized read side, though that's rarely a good idea since every read would then have to replay events from scratch. They travel together often in practice because they solve complementary halves of the same larger problem, not because they're actually the same idea.
Validating Commands with Interceptors
How should a team decide whether a given validation check belongs in a MessageDispatchInterceptor or inside an aggregate's command handler?
intermediateThe deciding factor is whether the check needs the aggregate's own current state to evaluate correctly. A check that's universal and stateless — every command must have a non-blank identifier field, no command payload may exceed some maximum size — applies identically regardless of which aggregate the command is ultimately headed to, and belongs in a MessageDispatchInterceptor, since it runs centrally once for every command before any aggregate is even loaded. A check that depends on an aggregate's own data — you can't withdraw more than the current balance, you can't approve a loan above a customer's pre-approved limit — can only be evaluated after that aggregate's state has been rebuilt from its event history, which only happens inside the aggregate's own command handler. Putting a stateless, universal check inside every aggregate is repetitive and easy to forget on new command types; trying to put a stateful check inside an interceptor either doesn't work at all or forces an awkward, redundant lookup outside the aggregate.
A team writes a MessageDispatchInterceptor to reject invalid commands, but the checks never seem to actually run in production. What is the most likely cause?
intermediateThe most likely cause is that the interceptor class was written and implements MessageDispatchInterceptor correctly, but was never actually registered with the CommandBus via registerDispatchInterceptor(). This is a common and genuinely silent mistake: there's no compile error and no obvious runtime warning, since Axon has no way to discover an interceptor on its own — registration has to happen explicitly, typically inside a @Configuration class that injects both the CommandBus and the interceptor. Without that registration step, the interceptor simply never gets invoked, and commands that should have been rejected pass straight through to the aggregate untouched. The fix is to confirm the registration code actually runs at startup, and to add a simple log line inside the interceptor during testing to verify it's firing, rather than assuming correctness just because the class compiled cleanly.
Besides rejecting invalid commands, what is another common real-world use of a MessageDispatchInterceptor?
intermediateA very common use is attaching a correlation ID to every command as it's dispatched, so that a single originating request can be traced consistently across every service, command, and event it touches through the system — the same tracing idea commonly applied to incoming HTTP requests, adapted here to commands. Since a dispatch interceptor runs centrally, once, on every command regardless of type, it's a natural single place to attach this kind of cross-cutting metadata without needing every individual command handler to remember to do it. Centralized audit logging of every command flowing through the system is another common use, for the same reason: registering the logic once in an interceptor guarantees it runs consistently for every command, rather than depending on each command handler remembering to log itself.
The Saga Pattern — Why We Need It
Why can't you use a single ACID transaction across multiple microservices?
beginnerA single ACID transaction requires one coordinator holding locks and a shared commit protocol across every participating resource, which in practice means a shared database, or at least a tightly coupled distributed-transaction protocol like two-phase commit. Independent microservices deliberately don't share a database — that's the entire point of splitting them — so there's no single transactional context spanning all of them. Even where a two-phase-commit-style protocol is technically possible, it requires every participating service to hold locks and stay available for the duration of the transaction, which directly undermines the independent availability and scalability microservices are meant to provide. The saga pattern exists specifically to get consistency across these independent services without requiring that shared transactional context at all.
What is a compensating transaction in the saga pattern?
beginnerA compensating transaction is a new, forward-moving action, run against a service whose earlier step in a saga already committed successfully, that reverses the business effect of that earlier step. It is not a database rollback and doesn't erase the fact that the earlier step happened — it adds a new fact on top that cancels or undoes the effect. For example, if creating a loan record succeeds but the subsequent step of depositing the loan amount fails, the compensating transaction explicitly cancels the loan record that was already created, since a loan with no matching deposit is no longer valid. Every step in a saga that could need to be undone requires its own explicit, deliberately designed compensating action, since there's no generic, automatic way to reverse an already-committed local transaction on an independent service.
Why is a saga only eventually consistent rather than immediately consistent?
beginnerA saga is built from a sequence of separate local transactions, each completed fully on its own service, one after another, rather than one single transaction spanning every service at once. Between the moment the first step commits and the moment the last step either succeeds or triggers a full chain of compensations, the overall system is genuinely in a state that hasn't finished settling yet — for example, a loan record can exist for a real span of time before the corresponding account deposit has actually happened. Any code that reads this data during that window needs to account for that possibility rather than assuming every step of a business process always completes as one atomic unit. This eventual consistency is the direct trade-off for gaining independent availability and scalability across the services involved, since none of them needs to hold a lock waiting on the others.