Event Versioning
As your system evolves, event schemas change. Ironflow supports event versioning with upcaster functions that automatically transform old events to the latest schema when they’re consumed.
How It Works
Events are stored with their original version. When a consumer reads them, upcasters transform the data through each version step until it reaches the latest schema.
Two language-specific rules to keep in mind:
- TypeScript — each
defineEvent({ version: N, upcast })registers theN-1 → Nstep. You do not specify the source version; it’s implicit. - Go —
upcasters.Register(name, from, to, fn)is explicit, buttomust equalfrom + 1. The chain walks one step at a time; a registered1 → 3hop is unreachable.
Defining Versioned Events
import { defineEvent } from "@ironflow/core";
// Version 1 — original schemaconst userCreatedV1 = defineEvent({ name: "user.created", version: 1,});
// Version 2 — added email fieldconst userCreatedV2 = defineEvent({ name: "user.created", version: 2, upcast: (v1Data) => ({ ...v1Data, email: v1Data.email ?? "unknown@example.com", }),});
// Version 3 — added role fieldconst userCreatedV3 = defineEvent({ name: "user.created", version: 3, upcast: (v2Data) => ({ ...v2Data, role: v2Data.role ?? "member", }),});import ( "encoding/json" "github.com/sahina/ironflow/sdk/go/ironflow")
upcasters := ironflow.NewUpcasterRegistry()
// v1 → v2: add email fieldupcasters.Register("user.created", 1, 2, func(data json.RawMessage) (json.RawMessage, error) { var m map[string]any if err := json.Unmarshal(data, &m); err != nil { return nil, err } if _, ok := m["email"]; !ok { m["email"] = "unknown@example.com" } return json.Marshal(m)})
// v2 → v3: add role fieldupcasters.Register("user.created", 2, 3, func(data json.RawMessage) (json.RawMessage, error) { var m map[string]any if err := json.Unmarshal(data, &m); err != nil { return nil, err } if _, ok := m["role"]; !ok { m["role"] = "member" } return json.Marshal(m)})Registering Events
Register event definitions when setting up your serve handler or worker:
import { defineEvent, createEventDefinitionRegistry } from "@ironflow/core";import { serve, createFunction } from "@ironflow/node";
const userCreatedV1 = defineEvent({ name: "user.created", version: 1 });const userCreatedV2 = defineEvent({ name: "user.created", version: 2, upcast: (data) => ({ ...data, email: data.email ?? "unknown@example.com" }),});
const registry = createEventDefinitionRegistry();registry.register(userCreatedV1);registry.register(userCreatedV2);
const handleUser = createFunction( { id: "handle-user", triggers: [{ event: "user.created" }], }, async ({ event }) => { // event.data is always v2 shape — upcasted automatically console.log(event.data.email); // always present },);
export default serve({ functions: [handleUser], eventDefinitions: registry,});upcasters := ironflow.NewUpcasterRegistry()upcasters.Register("user.created", 1, 2, func(data json.RawMessage) (json.RawMessage, error) { var m map[string]any if err := json.Unmarshal(data, &m); err != nil { return nil, err } m["email"] = "unknown@example.com" return json.Marshal(m)})
// Pass upcasters to Serve or NewWorkerhandler := ironflow.Serve(ironflow.ServeConfig{ Functions: []ironflow.Function{HandleUser}, Upcasters: upcasters,})Emitting Versioned Events
Specify the version when emitting events:
// Emit a v2 eventawait client.emit("user.created", { name: "Alice", email: "alice@example.com" }, { version: 2 },);_, err := client.Emit(ctx, "user.created", map[string]any{ "name": "Alice", "email": "alice@example.com",}, ironflow.WithEmitVersion(2))Events emitted without a version default to version 1.
Versioning with Entity Streams
Entity stream events also support versioning:
// Append a versioned event to an entity streamawait client.streams.append("user-123", { name: "user.profile_updated", data: { name: "Alice", email: "alice@example.com" }, entityType: "user",}, { expectedVersion: 2, version: 2, // event schema version});When reading entity streams, events are returned with their original version. Upcasting is applied automatically inside push handlers (serve) and pull workers (createWorker) when an event triggers a function — not on the raw read path. Calling client.streams.read(...) returns event data as it was stored; if you need the latest schema there, run it through your registered upcasters yourself.
Best Practices
Always add, never remove
Add new fields with defaults. Never remove or rename fields in a new version — consumers on older code versions may still expect them.
// Good: add field with defaultconst v2 = defineEvent({ name: "order.created", version: 2, upcast: (data) => ({ ...data, currency: data.currency ?? "USD" }),});
// Bad: remove field (breaks older consumers)// upcast: (data) => { delete data.oldField; return data; }One change per version
Keep each version bump focused on a single schema change. This makes the upcaster chain easier to understand and debug.
Version numbers are linear
Use simple incrementing integers (1, 2, 3). The upcaster chain runs sequentially from the stored version to the latest registered version.
Test your upcasters
// Verify the full chain worksconst v1Data = { name: "Alice" };// After v1→v2: { name: "Alice", email: "unknown@example.com" }// After v2→v3: { name: "Alice", email: "unknown@example.com", role: "member" }Event Schema Registry
Ironflow provides a server-side schema registry for storing JSON Schema definitions per event type and version. This enables schema validation, documentation, and discovery.
Registering Schemas
# Register a schema for order.created v1curl -X POST http://localhost:9123/api/v1/events/schemas \ -H "Content-Type: application/json" \ -d '{ "event_name": "order.created", "version": 1, "schema_json": "{\"type\":\"object\",\"properties\":{\"orderId\":{\"type\":\"string\"},\"total\":{\"type\":\"number\"}},\"required\":[\"orderId\",\"total\"]}", "description": "Order creation event" }'Querying Schemas
# List all schemascurl http://localhost:9123/api/v1/events/schemas
# List schemas for a specific eventcurl "http://localhost:9123/api/v1/events/schemas?event_name=order.created"
# Get latest version of a schemacurl http://localhost:9123/api/v1/events/schemas/order.created
# Get a specific versioncurl http://localhost:9123/api/v1/events/schemas/order.created/1Testing Upcasts
Test the upcaster chain for an event without processing real data:
curl -X POST http://localhost:9123/api/v1/events/upcast \ -H "Content-Type: application/json" \ -d '{ "event_name": "order.created", "from_version": 1, "to_version": 3, "data": {"orderId": "123"} }'This returns the version chain that would be applied. Phase 1 returns schema metadata without actual data transformation — complex SDK-defined upcasters require a running worker.
Domain-Driven Design
Event versioning is a critical part of maintaining domain events in a DDD system — as your domain model evolves, upcasters ensure consumers always see the latest event schema. See Commands, Events & Reactions for how domain events fit into the broader DDD picture.