Reading Events Directly from the Event Store
Learn to browse the raw event store directly instead of trusting projections blindly, and why checking the actual recorded events first is the fastest way to debug a misbehaving aggregate.
Learning objectives
- Query the event store by aggregate identifier to see an aggregate's full event history
- Explain what the sequence number on each event guarantees
- Use raw event history to isolate whether a bug lives in a command handler or a projection
- Build the habit of checking the event store before reaching for logs or a debugger
◆ Imagine this
You could rely on secondhand reports about what's sitting in an archive room, or you could just walk in and look for yourself — pull the actual folder, read the actual entries, in the actual order they were filed. There's a real, practical difference between believing a system works correctly and having personally verified it, even once.
Every event-sourced system makes a strong promise: nothing about an aggregate's state is a mystery, because every change is durably recorded as an event, forever, in order. It's easy to trust that promise abstractly while never actually looking at what's stored. This is about building the habit of looking — opening Axon Server's own event store directly and reading the raw, recorded history for a specific aggregate, exactly as it was written, with nothing standing between you and the source of truth.
Axon Server ships with a small operational dashboard, reachable at http://localhost:8024 in a typical local setup. Beyond confirming the server itself is alive, its search section lets you query the event store directly — by aggregate identifier, by event type, or across everything stored — and see the actual, raw events exactly as they were persisted, with no projection or application code standing in between.
To look up everything recorded for one specific customer, you search by that aggregate's identifier:
aggregateIdentifier: "customer-123"
The same search works for an account, a card, or a loan aggregate — anything with an @AggregateIdentifier-annotated field gets its own independent, ordered stream of events, and the dashboard can pull up any one of those streams on demand. This is worth doing early and often, not just when something looks broken: the more familiar the raw event shape becomes, the faster you'll recognize when something about it looks wrong later.
After running a create command and then an update command against one customer, a search for that customer's aggregate identifier returns a small, ordered list that should feel completely predictable — essentially a bank statement for that one aggregate:
| Sequence | Event type | Payload |
|---|---|---|
| 0 | CustomerCreatedEvent | {name: "John Smith", email: "john@email.com"} |
| 1 | CustomerUpdatedEvent | {name: "John A. Smith", email: "john@email.com"} |
◆ Under the hood — the sequence number is not decorative
That sequence column (0, 1, 2, ...) is precisely what guarantees replay always happens in the correct order. Axon stores and retrieves events strictly by this sequence, per aggregate, which is exactly why replaying a stream always rebuilds the same, correct final state, every time — no matter how many concurrent commands were actually involved in producing that history in the first place. If two commands raced against the same customer, the sequence numbers still resolve them into one strict, final order, and that order is what any rebuild honors.
Each row here is a fact, not a current value — CustomerUpdatedEvent doesn't overwrite anything in the store, it just adds a new fact to the sequence. The "current" name, John A. Smith, only exists as the result of folding both events together, which is exactly what an event-sourcing handler or a projection does when it reconstructs state.
💻 Code example
// Representative shape of what the raw event store returns for one aggregate. // Not application code -- this is what you'd see reading the dashboard's // search results directly. [ { "aggregateIdentifier": "customer-123", "sequenceNumber": 0, "type": "CustomerCreatedEvent", "payload": { "name": "John Smith", "email": "john@email.com" } }, { "aggregateIdentifier": "customer-123", "sequenceNumber": 1, "type": "CustomerUpdatedEvent", "payload": { "name": "John A. Smith", "email": "john@email.com" } } ]
◆ Under the hood — a real debugging habit
When something looks wrong with an aggregate's state in a running system, an experienced Axon developer's first instinct is almost always to check the raw event history first — not to sprinkle debug print statements through the aggregate's code. If the recorded events themselves tell a clear, correct story, the bug is somewhere downstream, most often in a projection. If the events themselves already look wrong — a deposit missing, an amount recorded incorrectly — the bug lives further upstream, in a command handler.
This habit turns debugging from guesswork into a binary search. A read model showing an incorrect account balance could mean the balance was computed wrong from correct events, or that the events themselves were wrong from the start. Reading the raw store first answers that question directly, in one lookup, instead of requiring you to reason backward through logs or a debugger attached to a running process.
▲ Common mistake
Never actually looking at the raw event store, and instead debugging exclusively through application logs or an attached debugger, skips the single most direct, most reliable source of truth an event-sourced system offers. Every other view of the data — a read model, a log line, a UI screen — is a derived, secondhand account of what the event store itself already records with total precision. Make checking there the first step, not a last resort.
▲ Common mistake — treating a payload as the whole truth
A single event's payload only tells you what changed at that step, not the aggregate's current state. Reading CustomerUpdatedEvent's payload alone, without also reading CustomerCreatedEvent before it, can lead you to draw wrong conclusions about fields the update event didn't touch. Always read a full sequence for the aggregate you're investigating, not just the most recent event.
▲ Edge case — searching across a busy shared store
On a system with many aggregate types and a high event volume, searching without an aggregate identifier — say, by event type alone across the whole store — can return an overwhelming, hard-to-scan result set. Narrowing to a specific aggregate identifier first, then broadening only if needed, keeps this kind of investigation fast and focused.
▲ Edge case — the store is append-only, not editable
It's worth remembering while browsing that nothing here can be edited or deleted through the dashboard in the normal course of operation — the event store is deliberately append-only. If a mistaken command produced a wrong event, the fix is a new, corrective event applied afterward, not an edit to history. Seeing this firsthand while browsing makes the append-only guarantee concrete rather than theoretical.
- Q: How do you look up everything recorded for one specific aggregate?
A: Search the event store by that aggregate's identifier, for example
aggregateIdentifier: "customer-123", in Axon Server's dashboard. - Q: What does the sequence number on each stored event guarantee? A: That events for a given aggregate are always replayed in the exact correct order, producing the same final state every time, regardless of how many commands ran concurrently while producing that history.
- Q: If an aggregate's state looks wrong, what's the fastest first place to check? A: The raw event history in the event store — it tells you definitively whether the problem is in what was recorded, or in how it's being read and combined afterward.
- Q: Does an event's payload alone tell you an aggregate's current state? A: No — a payload only describes what changed at that one step. The current state is the result of folding every event in the sequence together.
- Q: Can you edit or delete an event through the dashboard? A: No — the event store is append-only by design. Correcting a mistake means applying a new, corrective event, not editing history.
Want a visual for this concept?
Generate a diagram tailored to “Reading Events Directly from the Event Store” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →