intermediate~2h

Subscription Queries & Event Replay

Two capabilities that come almost free from an event-driven architecture already in place: push-based live queries that stream updates to a UI the instant something changes, and the ability to rebuild a read model from absolute scratch by replaying its full event history.

Learning objectives

  • Implement a subscription query that streams live updates to a client as a Flux
  • Explain what mechanism actually powers a subscription query's push updates
  • Explain why a projection is safe to delete and rebuild from event history
  • Trigger a full replay of a tracking event processor to rebuild a projection from scratch
  • Recognize the operational risk of running a replay carelessly on a busy production system

◆ Story

There are two ways to find out if something you care about has changed. You can keep checking — refresh a webpage, pull down to reload an app, ask "has anything changed yet?" over and over. Or you can subscribe to be told — turn on notifications, and let the update arrive the instant it happens, with zero effort on your part after the initial subscription.

A normal query in a CQRS read model is the first kind: ask once, get one answer back, and if you want a fresher answer later, ask again. That's perfectly fine for a page that loads once and doesn't need to change until the user navigates away. But some parts of an interface — a live balance ticking up as a deposit lands, a dashboard tracking loan approvals in real time — genuinely need the second kind: push the update the moment it happens, without the client having to ask again.

This distinction is at the heart of a Subscription Query: instead of returning one answer and closing, it stays open, and pushes a fresh result to the caller automatically, every time the underlying data changes.

A subscription query returns two things at once: an initial result (the current state, right now) and a stream of updates (everything that changes from this point forward). Combining both into one continuous stream is exactly what Flux — a reactive, potentially-infinite sequence of values — is built for.

The endpoint below subscribes a client to live updates for one customer's profile, streamed over Server-Sent Events (text/event-stream):

The call to queryGateway.subscriptionQuery(...) takes the original query (FindCustomerQuery), the shape of the initial result, and the shape of each subsequent update — in this case, both happen to be the same CustomerReadModel type, though they don't have to be. result.initialResult() fetches the current snapshot immediately; result.updates() is a stream that emits a new value every time the projection changes for this customer. Concatenating them means the client first receives "here's the current state," then keeps receiving "here's what changed," all over one continuous connection.

Nothing about the projection itself needs to change to support this. The same CustomerReadModel and the same query type used for an ordinary one-shot query work here unmodified — subscription is purely about how the response is delivered, not about what's being queried.

💻 Code example

@GetMapping(value = "/customers/{id}/live", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<CustomerReadModel> getCustomerLive(@PathVariable String id) { SubscriptionQueryResult<CustomerReadModel, CustomerReadModel> result = queryGateway.subscriptionQuery( new FindCustomerQuery(id), ResponseTypes.instanceOf(CustomerReadModel.class), // The initial result. ResponseTypes.instanceOf(CustomerReadModel.class) // The shape of each update that follows. ); return result.initialResult().concatWith(result.updates()); }

It's worth being precise about what actually triggers a push update, because the mechanism is simpler than it might look from the outside — and it's not new machinery built specifically for subscription queries.

A tracking event processor — the default kind Axon uses to deliver events to projections — continuously watches the event store for new events. The moment a fresh event is appended, the processor picks it up and delivers it to the relevant @EventHandler. That's the exact same @EventHandler responsible for updating a projection's read model on an ordinary write.

◆ Under the hood

When that @EventHandler updates a read model, Axon checks whether any open subscription queries are currently watching that same piece of data — say, the same customer ID — and if so, pushes the freshly updated result straight to them. No separate code path, no additional event listener written specifically for "live updates." The subscription query mechanism piggybacks entirely on the projection's existing event handling, which is what makes it a genuinely free capability once a projection already exists: you get live updates without writing any new event-handling logic at all.

This also means a subscription query's freshness is exactly as good as the projection's freshness. If the tracking event processor is running behind — under heavy load, or recovering from an outage — subscription query updates will lag by the same amount. There's no separate, faster path for live updates; they ride on the same event-delivery pipeline as everything else.

◆ Story

If a spreadsheet has a formula bug and every total in a column ends up wrong, the fix isn't to manually retype each cell with a guessed correct number. It's to fix the formula and let the spreadsheet recalculate every value from the original inputs, automatically, correctly, all at once. The original inputs were never wrong — only the calculation built on top of them was.

Event replay is exactly this idea applied to a projection. A read model is a derived, disposable calculation — never the actual source of truth. The event store, holding the complete, permanent history of everything that happened, is the source of truth. That distinction matters enormously here: because the read model is just a calculation over that history, it's always safe to delete it entirely and recompute it, correctly, from the very first event.

This becomes genuinely valuable the moment you discover a bug in a projection's logic after it's already been running in production for months — meaning every existing row in that read model might be subtly, silently wrong. A traditional system, where the current data is the only copy that exists, usually has no clean fix for this beyond a painful, custom, one-off data migration script trying to patch existing rows. An event-sourced system's fix is far simpler: correct the bug in the projection's code, delete the read model table, and replay. The exact same historical events, now processed by the corrected code, produce a perfectly correct read model automatically — no guessing, no manual patching, no risk of missing a row.

This single capability — the ability to fix a data-correctness bug retroactively, with total confidence, just by fixing the code and replaying — is one of the most commonly cited practical reasons teams adopt event sourcing in the first place.

Triggering a replay means telling a tracking event processor to forget its current position in the event stream and start over from the very beginning.

EventProcessingConfiguration gives access to any named event processor by its processor group name (here, "customer-projection"). Calling resetTokens() on a TrackingEventProcessor resets its tracking token — the internal bookmark recording how far through the event store it has already processed — back to the start. The next time the processor runs, it delivers every event from the first one ever stored, in order, to the projection's event handlers, exactly as if the projection had never run before.

This is a deliberately explicit, on-demand operation — it doesn't happen automatically, and it should be triggered from an operational context (an admin endpoint, a deployment script, a runbook step) rather than as a routine part of normal request handling. While a replay is running, the read model being rebuilt is temporarily incomplete or stale — queries against it during that window may return partial results, since events are being reprocessed in order but haven't all landed yet.

💻 Code example

@Autowired private EventProcessingConfiguration processingConfiguration; public void replayCustomerProjection() { processingConfiguration.eventProcessor("customer-projection", TrackingEventProcessor.class) .ifPresent(TrackingEventProcessor::resetTokens); // Tells it to start over from the beginning. }

▲ Common mistake

Triggering a full replay on a large, busy production system without considering the load it puts on the event store, or the temporary staleness of the read model while it rebuilds, is a genuine operational risk — not a theoretical one. Replaying millions of events means the event store has to serve millions of reads it wasn't otherwise planning to serve, right when the system is also handling normal live traffic. Meanwhile, anyone querying the projection being rebuilt sees an incomplete picture until the replay finishes catching up.

The practical fix is to treat a replay as a planned operation, not a casual one: run it during a low-traffic window, monitor the event store's load while it happens, and communicate to anyone depending on that projection that results may be temporarily stale or incomplete. For a very large projection, it's also worth estimating how long a full replay will actually take before triggering one against production — replaying tens of millions of events is not an instant operation.

A second, easy-to-miss trap: replaying a projection replays every event exactly as it originally happened, including events tied to bugs or edge cases that were already worked around by a manual data fix at the time. If a projection's logic doesn't account for that historical workaround, a replay can reintroduce a bug that was already "fixed" by hand — which is worth checking for before replaying anything with a long, messy history.

Q: What does a subscription query provide that a normal query doesn't? A: It stays open after the initial result and continues pushing fresh updates to the caller automatically, whenever the underlying data changes — no need to ask again.

Q: What existing mechanism actually powers a subscription query's push updates? A: The same @EventHandler methods already written for the projection. When they update the read model, Axon checks for open subscription queries watching that data and pushes the fresh result to them.

Q: Why is it safe to delete a read model and rebuild it from scratch? A: Because the read model is disposable and derived — never the actual source of truth. The permanent event history alone is enough to perfectly reconstruct it.

Q: What's the real-world value of event replay after finding a bug in a projection? A: Instead of a painful, custom data migration script, you fix the bug in the projection's code, delete the read model, and replay — the same correct history, processed by the fixed code, produces a fully correct read model automatically.

Q: What operational risk should you consider before triggering a full replay in production? A: The load a full replay puts on the event store, and the temporary staleness of the read model while it rebuilds — plan replays deliberately, ideally during low-traffic windows.

Want a visual for this concept?

Generate a diagram tailored to “Subscription Queries & Event Replay” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Snapshots in Event Sourcing← Back to all Event-Driven Microservices chapters