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
- Events are immutable facts. Prefer new event types or upcasters over rewriting history.
- JSON payloads are the default codec. Envelope fields live in store columns. The body should omit redundant envelope keys when practical.
- Past-tense, small, flat events. Renaming a type requires a registry entry and an upcaster, or a data migration.
- 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
- Additive fields. Use optional JSON keys with defaults in the appliers. No upcaster is needed.
- Rename or reshape. Write an upcaster from the old type name. Or keep the type name and transform only the payload.
- Semantic break. Add a new event type in compensating style, for example
PaymentReversed. Leave the old events as they are. - Snapshots. Bump
SnapshotVersionwhen 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
UPDATEorDELETEof 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
LoadStreamafter registering upcasters. - Rebuild projectors after type changes. See Projection rebuild.
Related
Docs track the go-sequent library. When APIs drift, the source wins.