Skip to content

Security

Event evolution

Compatibility policy for changing go-sequent events over time: upcaster rules, versioning strategies, a compatibility matrix, forbidden practices and test expectations.

Event evolution

Event histories live for years. This page is the policy for changing event shapes without rewriting history.

Principles

  1. Events are immutable facts. Prefer new event types or upcasters over rewriting history.
  2. JSON payloads are the default codec. Envelope fields live in store columns. The body should omit redundant envelope keys when practical.
  3. Past-tense, small, flat events. Renaming a type requires a registry entry and an upcaster, or a data migration.
  4. Never import or migrate a Ruby Sequent database. Sequent is conceptual inspiration only. There is no dump or wire compatibility.

Upcaster rules

Upcasters live in event/upcast. They run when events are loaded.

Rule Detail
Register by stored type One upcaster per stored type name. Chain.Register panics on a duplicate FromType.
Chain until stable The default limit is 8 hops. Exceeding it returns an error, so avoid cycles.
Target the current type The final type must be registered on the event.Registry.
Pure transform No I/O. Deterministic JSON in, JSON out.
Wire on load Call store.SetUpcasters(chain) before LoadStream.

Example: rename the field amt to amount, and the type bank.MoneyDeposited.v0 to bank.MoneyDeposited.

app.RegisterUpcaster(upcast.FuncUpcaster{
    From: "bank.MoneyDeposited.v0",
    Fn: func(p json.RawMessage) (string, json.RawMessage, error) {
        var m map[string]json.RawMessage
        if err := json.Unmarshal(p, &m); err != nil {
            return "", nil, err
        }
        if v, ok := m["amt"]; ok {
            m["amount"] = v
            delete(m, "amt")
        }
        out, err := json.Marshal(m)
        return "bank.MoneyDeposited", out, err
    },
})

store.SetUpcasters(app.Upcasters)

The amount stays a decimal string throughout. The upcaster moves the value and does not parse it.

Versioning strategies

  1. Additive fields. Use optional JSON keys with defaults in the appliers. No upcaster is needed.
  2. Rename or reshape. Write an upcaster from the old type name. Or keep the type name and transform only the payload.
  3. Semantic break. Add a new event type in compensating style, for example PaymentReversed. Leave the old events as they are.
  4. Snapshots. Bump SnapshotVersion when the aggregate state shape changes incompatibly. Delete or ignore old snapshots. See Snapshotting.

Compatibility matrix

Change Events Snapshots Projectors
Add an optional field OK Usually OK Handle a missing value
Rename a field Upcaster Bump the version Upcaster, or dual-read
Rename an event type Upcaster and registry Rebuild from events Handle both until catch-up
Remove a field Keep it in old events, ignore it in the applier Bump if required Stop projecting the field
Money representation Must stay a decimal string, never float64 Same Same

Forbidden

  • Silent UPDATE or DELETE of event rows for business corrections. Use compensating events.
  • Event signatures as a framework feature. The hash chain is what the framework implements.
  • Crypto-shredding as the GDPR path. Erasure is by tombstone only.
  • Loading Ruby Sequent SQL dumps into go-sequent schemas.

Editing a stored event row also breaks the hash chain, and LoadStream will return ErrHashChainTamper. See Event store.

Test expectations

  • Write a unit test for each production upcaster.
  • Replay a fixture stream through LoadStream after registering upcasters.
  • Rebuild projectors after type changes. See Projection rebuild.

Docs track the go-sequent library. When APIs drift, the source wins.