Event sourcing in Elixir
Chronicle is an open-source (MIT) event-sourcing database and processing runtime, and cratis_chronicle is its idiomatic Elixir client. It exposes OTP-native constructs (use Chronicle.Events.EventType, use Chronicle.ReadModels.ReadModel, use Chronicle.Reactors.Reactor, use Chronicle.Reducers.Reducer, and use Chronicle.Seeding.Seeder) plus model-bound constraints, context-aware appends with process-scoped identity, correlation, and causation metadata, optimistic concurrency, transactions, jobs and webhooks, and automatic reconnection with exponential backoff. The client supervises a connection to the Chronicle kernel as part of your application’s supervision tree.
With event sourcing, every state change is captured as an immutable event instead of an update in place. Chronicle stores those events, and read models are derived from them: declared model-bound projections execute server-side, or you can fold events into read models in your own process with a reducer.
The event store is not BEAM-only. Chronicle’s kernel exposes a language-agnostic gRPC/protobuf boundary, and the same store is reachable from the .NET, TypeScript, and Kotlin/Java clients as well. Storage is pluggable: MongoDB by default, with PostgreSQL, SQL Server, SQLite, and in-memory providers implemented in the kernel.
Install
Section titled “Install”Add cratis_chronicle to your dependencies in mix.exs, using the current version from Hex:
defp deps do [ {:cratis_chronicle, "~> <version>"} ]endmix hex.info cratis_chronicle prints the current version and the matching dependency line. Then run mix deps.get.
A taste
Section titled “A taste”An event type, a read model projected from it, a supervised client, and an append, from the client repository’s quick example. It assumes the Chronicle development kernel on localhost:35000; Get started with Chronicle shows how to run one locally, and the Elixir client docs cover connection settings, including the development kernel’s TLS certificate.
defmodule MyApp.Events.AccountOpened do use Chronicle.Events.EventType, id: "account-opened"
# Typed defaults: the client derives the event's JSON schema from them. defstruct owner: "", balance: 0end
defmodule MyApp.ReadModels.Account do use Chronicle.ReadModels.ReadModel
defstruct id: "", owner: "", balance: 0
# owner and balance are mapped by name; id comes from the event source id. from MyApp.Events.AccountOpened, set: [id: :event_source_id]end
defmodule MyApp.Application do use Application
@impl true def start(_type, _args) do children = [ {Chronicle.Client, # Development-only credentials for the local development kernel. connection_string: "chronicle://chronicle-dev-client:chronicle-dev-secret@localhost:35000", event_store: "my-app", otp_app: :my_app} ]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor) endendThe connection string carries the development kernel’s well-known credentials. Never use them outside local development.
Then, with the application running (for example in iex -S mix), wait for the client to register and append:
alias Chronicle.Connections.Lifecycle
# The client connects and registers in the background; until then, calls return# {:error, :not_connected}.:ok = Lifecycle.wait_until(Lifecycle.name_for(Chronicle.Client), :registered)
:ok = Chronicle.append("account-42", %MyApp.Events.AccountOpened{owner: "Alice", balance: 1000})
# Projections run asynchronously: this can be {:ok, nil} until the projection catches up.{:ok, account} = Chronicle.read_model(MyApp.ReadModels.Account, "account-42")The append’s success does not mean the projection has caught up, so an immediate read can return no model or an older value. In an automated check, wait for the expected account state with a bounded timeout; inspect the projection or failed partition if that timeout expires.
Where to go next
Section titled “Where to go next”Looking for another language? See Chronicle in your language for the .NET, TypeScript, and Kotlin/Java clients.