An order's status says shipped. Useful, but it doesn't tell us how the order got there.
When was it created? Was payment confirmed before shipping? Did someone change the delivery details? A latest-state record can answer some of these questions if we store extra history. Event sourcing takes a different approach: the recorded changes themselves become the source for the current state.
Let's rebuild a tiny order from its events, then look at the responsibilities that come with keeping that history.
Store what happened, in order
In an event-sourced design, the order has a stream of accepted facts, such as OrderCreated, PaymentConfirmed and OrderShipped. We append these events instead of replacing the history with the latest status.
A request to ship an order is a command. OrderShipped records that the accepted transition happened. Keeping that distinction clear stops the history from becoming a list of wishes that may never have succeeded.
The stream needs an identity, ordering and a stable meaning for each event. For our example, the order ID identifies the stream and a sequence number identifies its position. A timestamp is helpful context, but doesn't alone solve concurrent writes.

Rebuild the part of the order we need
Here's a deliberately small Python example. It reconstructs a status view from three events:
events = [
{"type": "OrderCreated", "order_id": 318},
{"type": "PaymentConfirmed"},
{"type": "OrderShipped"},
]
def rebuild(history):
state = {}
for event in history:
if event["type"] == "OrderCreated":
state = {"id": event["order_id"], "status": "pending"}
elif event["type"] == "PaymentConfirmed":
state["status"] = "paid"
elif event["type"] == "OrderShipped":
state["status"] = "shipped"
return state
assert rebuild(events)["status"] == "shipped"
assert rebuild(events[:2])["status"] == "paid"
The second assertion shows the interesting part: applying only the first two events gives us the earlier state. The full history lets us ask how a view looked at a particular stream position.
This function assumes a valid example history. The real command handler must decide whether a new transition is allowed before appending it. A reducer that can replay OrderShipped doesn't establish that shipping an unpaid order was permitted.
Good thing I wrote that down.
Protect the stream from conflicting changes
Suppose two requests read the order at version 2. One wants to ship it; another wants to cancel it. Both make their decision using the same starting state.
An event store can enforce an expected version when appending. The first successful append moves the stream forward. The second must detect that its expected version is old, reload the state and reconsider the command.
That is more useful than blindly appending both outcomes and hoping the replay order fixes the business conflict. The store protects concurrency under its contract; the application still needs rules for what may follow what.
Build convenient views from the history
Replaying the entire stream for every page load may be unnecessary. A projection can maintain a display-friendly view as events arrive, while the event stream remains the authoritative history.
That view may be briefly behind if updates are asynchronous. Track its progress and make repeated processing safe. If it breaks, rebuilding from the stream can recover the view, provided the event meanings and projection code are still compatible.
Snapshots can also speed up loading a long stream. A snapshot records derived state at a known position; load it, then apply the later events. It is a shortcut over the history, not permission to forget what the snapshot means or which version produced it.
Replaying history shouldn't repeat the real-world action
Suppose OrderShipped originally triggered a notification. Rebuilding a status view must not send the shipping email again for every historical event.
Are we reconstructing what happened, or asking the outside world to do it again?
Keep state reconstruction separate from external effects. A live event handler may decide to send a notification, with its own delivery and repeat policy. A replay handler should rebuild the chosen view without accidentally charging, emailing or booking anything.
This is worth testing early. A successful replay that quietly sends thousands of old messages isn't a successful recovery.
Old events are part of the long-term design
Today's event schema may look different from next year's. Version it and plan how newer readers will interpret older records. Changing projection code can also change a rebuilt view, so verify that its new behaviour is intended.
When a recorded business fact needs correction, an explicit corrective event often preserves the history better than silently editing an earlier one. Sensitive data needs additional care: minimise what events contain and design appropriate retention or redaction rather than promising everything stays untouched forever.
The event-sourcing guidance discusses these costs alongside the benefits. An audit trail alone doesn't automatically justify making events the entire application's source of truth.
Keep the history because you will use it
Event sourcing can make it practical to inspect earlier states, understand transitions and rebuild different views. Those capabilities are valuable when the business genuinely needs them.
For our order, the key question isn't whether we can replay three events. We can. It's whether we're ready to own the event definitions, concurrency rules, replay behaviour and long-term compatibility. Choose the pattern when that history helps enough to justify the work of preserving its meaning.
Try it yourself
You rebuild an order-status projection by replaying its full stream. The normal OrderShipped handler also sends an email. Should the rebuild run that external action?
Share this article with your friends
Know someone who would enjoy this? Send it their way.