skip to content

In an event-sourced system, why can't you just edit the shape of an already-stored event when your code's model changes, and what technique is normally used to let old and new event shapes coexist?

level: juniorimportance: must knowfreq 70%

answer

  1. log is immutable, code isn't
  2. version tag + chain of pure functions
  3. translate at read time, not write time
  4. snapshots need re-validation too

basics

~20 s

Old events are stored forever and get replayed to rebuild state. If you change what a field means without a plan, old events break the code that reads them. Upcasting is a small translator that turns an old-shaped event into the new shape before your code sees it, so replay still works.

solid answer

~40 s

Event sourcing treats the event log as the source of truth and replays it to rebuild state, so every historical event must remain readable forever, not just the latest ones written today. Because event payloads are immutable once appended, you cannot retroactively 'fix' old events in place the way you'd migrate a database column. Upcasting is the standard fix: a small transformation function per event version that converts an old-shaped event into the current shape (or the next version, chained) at the moment it's deserialized, so business logic only ever sees the latest schema. This keeps the append-only log immutable while still letting the domain model evolve.

go deeper

for a junior

Should know that events are immutable and get replayed, so old shapes must stay readable, and that 'upcasting' is the name for the read-time translation step — doesn't need to write the chaining logic themselves.

for a middle

Should be able to sketch a concrete upcaster (old field -> new fields with an explicit default/assumption) and explain why it runs at deserialization time rather than as a database migration.

for a senior

Should discuss the cost side: chain length growing over the system's life, replay/rehydration performance on long streams, and the interaction with snapshots needing invalidation.

for a principal

Should reason about when upcasting is the wrong tool entirely (context-dependent transforms), how to retire old upcasters safely at organizational scale, and the audit/compliance implications of never editing the raw log.

## Why the log outlives the code that wrote it Event sourcing stores every state change as an **immutable, append-only event** rather than storing only current row values. Current state — whether an aggregate in the write model or a projection in a read model — is produced by replaying the full (or snapshot-plus-tail) sequence of events for that stream. This gives a complete audit trail and lets you rebuild any past state, but it also means the event log has to outlive the code that first wrote it. A field that made sense in an `OrderPlaced` event two years ago — say a single `total` amount — may need to become `subtotal` and `tax` once the business adds tax reporting. The events already on disk still say `total`; you cannot go back and rewrite millions of historical payloads without destroying the very thing that makes the log trustworthy (its immutability), and without risking that some other service already consumed the old shape. ## How upcasting works **Upcasting solves this by moving the translation work to read time instead of write time.** 1. Every event is tagged with a **type name** and a **version** (e.g. `OrderPlaced.v1`, `OrderPlaced.v2`). 2. For each schema change, the team writes a small, pure transformation function — an **upcaster** — that takes the raw representation of the old version and produces the raw representation of the next version. 3. When an event is deserialized, a pipeline runs it through the chain of applicable upcasters (v1→v2, then v2→v3, and so on) until it reaches the version current code expects, and only the fully-upcast object is ever exposed to domain logic. For the `OrderPlaced` example, the v1→v2 upcaster might read the old `total` field and emit `subtotal = total` and `tax = 0`, encoding an explicit assumption directly in the transformation rather than leaving it implicit. ## Why not a one-off migration script The reason this pattern exists, rather than a one-off migration script, is that it preserves two properties event-sourced systems depend on: - **immutability** of the stored fact, and - **continuous deployability**. A migration script that rewrites events in place requires downtime or a careful backfill dance, and it destroys the literal historical record, which matters both for compliance and for debugging (a bug replayed years later should see exactly what was written then). Upcasting treats 'what happened' and 'how current code models it' as separate concerns: the log stays a faithful, unedited record, while the upcaster chain is just another piece of versioned application code that ships and rolls back like anything else. ## The trade-off **The trade-off is that this code never gets to die.** - **Every upcaster you've written has to stay correct** for as long as any event of that version might still exist in the log — often years or forever. - **Long-lived aggregates pay a real CPU cost:** replaying an aggregate with a deep history might mean running early events through five or six chained upcasters before domain logic even sees them, which shows up as slow rehydration or slow snapshot rebuilds. - **There's also a correctness risk baked into every upcaster:** because it decides how to interpret old data (like the `tax = 0` default above), a wrong assumption doesn't crash anything — it silently produces plausible-looking but incorrect derived state, and because the raw event never changes, that mistake is easy to miss and often surfaces much later. ## Failure modes - **In production, the most common failure is forgetting to version-tag events at write time,** making it structurally impossible to tell v1 payloads from v2 payloads on read. - **Another is an upcaster that assumes external context** (a currency rate, a since-changed tax rule) that isn't actually recoverable from the event itself — a sign upcasting is the wrong tool and a full copy-and-replace migration is needed instead. - **Snapshots complicate this further:** a snapshot is a materialized replay result, so changing an upcaster invalidates existing snapshots built under the old interpretation, or they'll silently disagree with a fresh replay. Frameworks like Axon Framework formalize this as an `UpcasterChain` applied during deserialization; stores like EventStoreDB leave it to the application layer, but the pattern is the same everywhere: **translate at the boundary, keep the log untouched.**

  • What happens to an existing snapshot if you change or add an upcaster for an event type it depends on?
    The snapshot is a materialized result of a past replay under the old interpretation, so it can silently disagree with what a fresh replay through the new upcaster chain would produce. Teams typically bump a snapshot format version alongside upcaster changes and invalidate old snapshots so they get rebuilt from the event log rather than trusted as-is.
  • When would upcasting not be enough, forcing you toward a full migration instead?
    When the transformation needs information that isn't recoverable from the event payload itself — e.g. an exchange rate at the time, or a business rule that has since changed — a pure per-event function can't correctly compute the new shape. In that case you need a one-time, context-aware backfill rather than an on-the-fly upcast.
  • How do you keep an upcaster chain from becoming a performance problem on long-lived aggregates?
    Common mitigations are periodic snapshotting so replay only needs to upcast the tail of recent events, and occasionally 'graduating' an old version by running a batch rewrite that collapses several upcast steps into fewer, though that reintroduces some of the migration risk upcasting was meant to avoid.

Like keeping the original signed paper contract in a vault forever, but photocopying it through a translator each time someone needs to read it in today's language — you never edit the vault copy, you just get a fresh, current-language reading of it.

saying these in an interview costs you the question

  • Says you can just UPDATE the event row in the database when the shape changes
  • Doesn't mention that events need a version tag to know which upcaster to apply
  • Assumes upcasting is free at replay time with no cost for long streams
  • Forgets that snapshots also need to be revalidated when upcasting logic changes
  • Thinks upcasting requires stopping writes or taking downtime

context