Event Sourcing
Factos follows the interpretation described in Rico Fritzsche’s Simply Event Sourcing: Event Sourcing means accepted facts are stored as the source of truth, and the facts relevant to a decision are used before new facts are accepted.
Aggregates, CQRS, projections, stream-per-object storage, message brokers, and microservices can be useful implementation choices, but they are not the definition of Event Sourcing.
Events Are Accepted Facts
An event is something the application has accepted as true. Current state is not the authority; it is derived by folding the event history.
pub type Event {
UsernameReserved(username: String)
UserRegistered(username: String)
DisplayNameChanged(user_id: String, name: String)
}
In Gleam, each application defines its own event type. There is no need for a base event interface or runtime inheritance. The type tells readers and the compiler which facts exist in this part of the domain.
Decisions Use Relevant History
To handle a command, load the relevant facts, fold them into a temporary state, and run a pure decision function.
pub fn evolve(state: State, event: Event) -> State {
case state, event {
UsernameAvailable, UsernameReserved(_) -> UsernameTaken
UsernameAvailable, UserRegistered(_) -> UsernameTaken
UsernameAvailable, DisplayNameChanged(_, _) -> UsernameAvailable
UsernameTaken, UsernameReserved(_) -> UsernameTaken
UsernameTaken, UserRegistered(_) -> UsernameTaken
UsernameTaken, DisplayNameChanged(_, _) -> UsernameTaken
}
}
That folded state is only the state needed for the decision. Read models,
reports, caches, and UI projections can be built separately with View values.
Context-First Consistency
Factos models command context consistency with Query, Context, and
AppendCondition.
For username registration, the context may be all events of selected types tagged with the requested username:
pub fn username_context(username: String) -> factos.Query {
factos.query([
factos.query_item(
types: [
factos.event_type("UsernameReserved"),
factos.event_type("UserRegistered"),
],
tags: [factos.tag("username:" <> username)],
),
])
}
After the decision is made, the backend should append the new facts only if no matching facts appeared after the position used for the decision. This protects the invariant without forcing every command into a single aggregate stream.
Applying the Idea in Gleam
The typical Factos flow is:
- Model domain commands, events, states, and errors as Gleam custom types.
- Write an
evolvefunction that folds accepted events into decision state. - Write a
decidefunction that returnsResult(List(Event), DomainError). - Define the command context with event types and tags.
- Let a backend load matching facts and protect the append with the returned condition.
This style keeps Event Sourcing concrete. The important parts are plain Gleam functions and types, while storage backends handle persistence, codecs, and append guarantees.