The encoder describes how an event struct is turned into data for the store and back. Every event opts in via @derive Ariadne.Flow.Event — without it, the store cannot persist the event.
Stored type
The encoder is also where an event declares the type it is stored under:
@derive {Ariadne.Flow.Event, type: "course-defined"}The type is the event's identity in the store — filters select on it, and reading an event back resolves it to the module that declares it. It defaults to the module name, so an event without type: is stored as "CourseDefined"; evolving events covers why declaring it anyway is what keeps a module free to be renamed.
Two event modules cannot declare the same type: the first read that has to resolve a type raises instead of guessing between them. So does a stored type no event module declares any more.
Default encoder
The default encoder serialises an event by taking its struct fields as a map with string keys and collecting the event's tags via tags/1. On read, the stored data is turned back into the event struct.
For
%CourseDefined{course_id: 42, capacity: 30}the default produces
%{
data: %{"course_id" => 42, "capacity" => 30},
tags: ["course:42"]
}and reverses that on decode. The default fits any event whose fields are JSON-serialisable (strings, numbers, booleans, maps, lists, nil).
Custom encoder
A custom encoder is useful when the default does not fit — for example:
- An event has been extended with a new field, and older events in the store need a default value for it.
- A field needs a non-default serialisation (a custom value object, a formatted timestamp).
- A field should be redacted or transformed before storage.
Instead of @derive, write a defimpl inside the event module and implement encode/1 and decode/3 directly. Delegate to Ariadne.Flow.Event.DefaultEncoder for the parts you do not need to change:
defmodule CourseDefined do
defstruct [:course_id, :capacity]
def tags(e), do: ["course:#{e.course_id}"]
defimpl Ariadne.Flow.Event do
alias Ariadne.Flow.Event.DefaultEncoder
def type, do: "course-defined"
def encode(event), do: DefaultEncoder.encode(event)
def decode(event, store_data, metadata) do
store_data = Map.put_new(store_data, "capacity", 0)
DefaultEncoder.decode(event, store_data, metadata)
end
end
endThe defimpl replaces the @derive line — you don't need both. Each event has its own implementation, so different events can use different encoders.
def type takes the place of the derive's type: option; an implementation without it stores the event under its module name, exactly as a derive without type: does.