ALTER TABLE or UPDATE statements to modify historical event payloads because doing so would destroy our tamper-proof audit log.
However, business requirements evolve. Fields are added, renamed, or deprecated. To resolve this without mutating historical data, we use Event Upcasting.
What Is Upcasting?
An Upcaster is a lightweight, raw-level transformer that intercepts old event payloads during the loading/replay phase and upgrades them on-the-fly to the latest schema version before they are deserialized into your domain types: Because upcasters operate during the loading loop, the database remains completely untouched, preserving our historical audit guarantees, while your application code only ever has to reason about the latest, current event structure. Upcasting is related to Event Sourcing because it keeps old event streams replayable after payload schemas change, but it is not the Decider/Evolver pattern.EventUpcaster transforms stored payload bytes before deserialization. Command decisions still belong in Aggregate::handle, and aggregate state evolution still belongs in Aggregate::apply.
Implementing the EventUpcaster Trait
Our framework provides a built-in EventUpcaster trait that works directly on raw vector bytes. This ensures that upcasting is decoupled from whatever serialization engine (JSON, MessagePack, Protobuf) your databases use.
Registering Upcasters
Once you have defined yourEventUpcaster, you must register it with your EventStore adapter (such as SqliteEventStore or PostgresEventStore). The store’s deserialization loop will automatically query the registered upcasters and apply them sequentially if it loads an event matching the registered event type name and source version.
v1 -> v2 and v2 -> v3), the engine automatically constructs an upcaster chain and applies them sequentially when replaying events.
Upcasters run in adapters that deserialize stored event payload bytes, including
the SQL stores and Redis. InMemoryEventStore stores already-typed Rust events
and intentionally does not run raw-byte upcasters. For tests that need to prove
event schema migration behavior, use SqliteEventStore::in_memory() or the same
SQL adapter family that production uses instead of InMemoryEventStore.
Upcasting Design Guidelines
To manage schema changes cleanly as your product grows:- Keep Upcasters Small: Each upcaster should only be responsible for a single step transformation (e.g., v1 to v2).
- Chain Upcasters: If an event has evolved from v1 to v3, the framework will automatically chain upcasters (v1 -> v2, then v2 -> v3) sequentially, so you don’t need to write a complex v1 -> v3 upcaster.
- Run Unit Tests: Always write unit tests for your upcasters. Assert that passing raw bytes representing an old JSON schema correctly returns the expected, modified JSON schema.