Docs / ArchitectureKit 1.0 / Guides / Versioning Events

Versioning Events

The database keeps the schema of an event type forever. If the shape of an event changes, introduce a new event type, and translate the stored events of the old type with an upcaster. If you change the schema of an existing event type instead, RegisterSchemas fails on the next start (see Registering Event Schemas).

Suppose an earlier version of the library wrote events of the type io.eventsourcingdb.library.book-lent, with the fields lentTo and until. To translate them into BookBorrowed events, create a set of upcasters with the NewUpcasters function. Then call the Upcast function on the set, and hand over the old event type and a function that receives the stored event and returns the translated events:

var libraryUpcasters = architecturekit.NewUpcasters().
  Upcast(
    "io.eventsourcingdb.library.book-lent",
    func(event eventsourcingdb.Event) ([]eventsourcingdb.Event, error) {
      var old struct {
        LentTo string `json:"lentTo"`
        Until  string `json:"until"`
      }
      if err := json.Unmarshal(event.Data, &old); err != nil {
        return nil, err
      }

      data, err := json.Marshal(BookBorrowed{
        BorrowedBy:    old.LentTo,
        BorrowedUntil: old.Until,
      })
      if err != nil {
        return nil, err
      }

      event.Type = BookBorrowed{}.EventType()
      event.Data = data

      return []eventsourcingdb.Event{event}, nil
    },
  )

The function has the type Upcaster. Upcasters may return more than one event, for example to split an event that recorded two facts into one event per fact. Derive each of them from the stored event, as above, so that they all keep its ID. A projection then applies every one of them to a view, also if several of them change the same item (see Defining Views). If a returned event has an upcaster of its own, that one runs as well, so every version needs only a single step to the next one. The translated events are never written back.

A chain ends after 16 steps: if the event still has an upcaster then, reading fails with an error of the category ErrPermanent, which catches an upcaster that keeps its event type and would otherwise run forever. An error that an upcaster returns belongs to ErrPermanent as well.

To use the upcasters, call the UpcastWith function on the state and hand over the set. The upcasters then run before the Evolve rules:

bookState.UpcastWith(libraryUpcasters)

Upcasting belongs to the event types, not to a single state, so register the upcasters once and hand the same set to every state, and to every projection that reads these events (see Defining Projections). That way, the write side and the read side see the same events.

The kit decodes the data of an event with encoding/json, which matches the names in the data to the fields of the struct regardless of case, and ignores names that match no field. So data that names a field BorrowedBy fills the field that BookBorrowed tags as borrowedBy, without an upcaster. A name that differs by more than case, such as lentTo, matches no field, and the field stays empty, so a field whose name changes by more than case still needs an upcaster, as above.

Note that calling Upcast with nil as the function, or twice for the same event type, panics, as does calling UpcastWith twice, or with nil.