Chronicle Elixir Client
Idiomatic Elixir client for the Chronicle event-sourcing platform.
Overview
Chronicle is an event-sourcing kernel that stores domain events and projects them into read models. This library provides a clean, idiomatic Elixir interface built on top of the Chronicle gRPC API.
Key features:
use Chronicle.EventType— annotate structs as event types with stable IDsuse Chronicle.Reactor— react to events with side effectsuse Chronicle.Reducer— build read models by folding events into stateuse Chronicle.ReadModel— define read models with model-bound projections- Resilient connection — automatic reconnection with exponential backoff
- OTP-native — fits naturally in your supervision tree
Installation
Add the dependency to your mix.exs:
def deps do
[
{:cratis_chronicle, "~> 0.1"}
]
endQuick Start
This guide uses projections as the default because they run inside Chronicle and keep read model updates close to the event store.
1. Define event types
defmodule MyApp.Events.AccountOpened do
use Chronicle.EventType, id: "account-opened-v1"
defstruct [:account_id, :owner_name, :initial_balance]
end
defmodule MyApp.Events.FundsDeposited do
use Chronicle.EventType, id: "funds-deposited-v1"
defstruct [:account_id, :amount]
end2. Define a read model
defmodule MyApp.ReadModels.Account do
use Chronicle.ReadModel
alias MyApp.Events.{AccountOpened, FundsDeposited}
defstruct account_id: nil, owner_name: nil, balance: 0
from AccountOpened,
set: [
account_id: :event_source_id,
owner_name: :owner_name,
balance: :initial_balance
]
from FundsDeposited,
add: [balance: :amount]
end3. Define projection mappings (recommended)
Projection mappings are registered on Chronicle and executed server-side.
Each from/2 maps an event type to:
-
A read model key (
$eventSourceIdby default when:keyis omitted) - A set of property assignments
- Optional expressions that can use event fields and existing model values
That means Chronicle can maintain read models directly from the event stream without reducer code running in your client process.
The projection mapping is declared directly in the read model module using
from, join, removed_with, and from_every.
For expressions, atoms are preferred and more natural:
:owner_name,:amountfor event fields:event_source_id,:occurredfor built-in context values- string expressions only for advanced cases
4. Define a reactor (optional)
Reactors react to events with side effects:
defmodule MyApp.Reactors.NotificationReactor do
use Chronicle.Reactor
@handles MyApp.Events.AccountOpened
@impl true
def handle(%MyApp.Events.AccountOpened{} = event, _context) do
MyApp.Mailer.send_welcome(event.owner_name)
:ok
end
end5. Start Chronicle.Client in your supervision tree
If your Chronicle artifacts are defined in one OTP app, use otp_app and let
Chronicle discover event types, reactors, reducers, and read models automatically.
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
{Chronicle.Client,
connection_string: "chronicle://localhost:35000?disableTls=true",
event_store: "my-app",
otp_app: :my_app}
]
Supervisor.start_link(children, strategy: :one_for_one)
end
end6. Append events and query read models
# Append a single event
:ok = Chronicle.append("account-42", %MyApp.Events.AccountOpened{
account_id: "account-42",
owner_name: "Alice",
initial_balance: 1000
})
# Append multiple events atomically
:ok = Chronicle.append_many("account-42", [
%MyApp.Events.FundsDeposited{account_id: "account-42", amount: 500},
%MyApp.Events.FundsDeposited{account_id: "account-42", amount: 200}
])
# Read back the current read model
{:ok, account} = Chronicle.read_model(MyApp.ReadModels.Account, "account-42")
IO.inspect(account)
# => %MyApp.ReadModels.Account{account_id: "account-42", owner_name: "Alice", balance: 1700}
# Get all instances
{:ok, accounts} = Chronicle.all(MyApp.ReadModels.Account)Quick Start (Reducer Alternative)
Use reducers when you want the read model folding logic in Elixir code in your app process. In this mode, Chronicle streams events to the reducer and your reducer returns the next model state.
1. Define a reducer
Reducers fold events into a read model, one event at a time:
defmodule MyApp.Reducers.AccountReducer do
use Chronicle.Reducer, model: MyApp.ReadModels.Account
@handles MyApp.Events.AccountOpened
@handles MyApp.Events.FundsDeposited
@impl true
def reduce(%MyApp.Events.AccountOpened{} = event, _model, _context) do
%MyApp.ReadModels.Account{
account_id: event.account_id,
owner_name: event.owner_name,
balance: event.initial_balance
}
end
def reduce(%MyApp.Events.FundsDeposited{} = event, model, _context) do
%{model | balance: model.balance + event.amount}
end
end2. Start Chronicle.Client with reducers
With auto-discovery:
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
{Chronicle.Client,
connection_string: "chronicle://localhost:35000?disableTls=true",
event_store: "my-app",
otp_app: :my_app}
]
Supervisor.start_link(children, strategy: :one_for_one)
end
endConnection Strings
Chronicle connection strings use the chronicle:// scheme:
| Format | Use |
|---|---|
chronicle://localhost:35000 | No authentication (development) |
chronicle://localhost:35000?disableTls=true | Disable TLS for local dev |
chronicle://client-id:secret@server:35000 | Client credentials |
chronicle://server:35000?apiKey=my-key | API key authentication |
chronicle+srv://service-name:35000 | SRV record lookup |
alias Chronicle.Connections.ConnectionString
# Parse a string
cs = ConnectionString.parse("chronicle://localhost:35000?disableTls=true")
# Use helpers
cs = ConnectionString.default() # chronicle://localhost:35000
cs = ConnectionString.development() # includes dev credentials
# Modify
cs = ConnectionString.with_api_key(cs, "my-api-key")
cs = ConnectionString.with_credentials(cs, "client-id", "secret")Declarative Projections
Projections are the recommended default. They are model-bound mappings declared
on Chronicle.ReadModel and executed server-side by Chronicle:
defmodule MyApp.ReadModels.Account do
use Chronicle.ReadModel
alias MyApp.Events.{AccountOpened, FundsDeposited}
defstruct account_id: nil, owner_name: nil, balance: 0
from AccountOpened,
set: [
account_id: :event_source_id,
owner_name: :owner_name,
balance: :initial_balance
]
from FundsDeposited,
add: [balance: :amount]
endMultiple clients
Run multiple Chronicle.Client instances for different event stores:
{Chronicle.Client,
name: :bank,
connection_string: "chronicle://bank-server:35000",
event_store: "bank",
event_types: [...]}
{Chronicle.Client,
name: :crm,
connection_string: "chronicle://crm-server:35000",
event_store: "crm",
event_types: [...]}
# Specify which client to use
Chronicle.append("customer-1", event, client: :crm)
Chronicle.read_model(Account, "account-1", client: :bank)Running the Console Sample
A working example is in the Samples/console directory.
Prerequisites: A Chronicle kernel running locally on port 35000.
cd Samples/console
mix deps.get
mix run --no-halt
Set CHRONICLE_CONNECTION_STRING to override the default connection:
CHRONICLE_CONNECTION_STRING="chronicle://myserver:35000?apiKey=secret" mix run --no-haltLocal Development
Prerequisites
- Elixir 1.14+ and OTP 25+
- A running Chronicle kernel (see Chronicle)
Setup
cd Source/chronicle
mix deps.get
mix compile
mix testRunning tests
The unit tests do not require a running Chronicle instance:
mix testCode formatting
mix formatGenerating documentation
mix docs
open doc/index.htmlPackage structure
Source/
chronicle/ # The cratis/chronicle Hex package
lib/
chronicle.ex # Convenience API
chronicle/
connections/
connection_string.ex
connection.ex
client.ex # Supervisor entry point
artifacts.ex # Artifact auto-discovery helpers
event_type.ex # use Chronicle.EventType macro
reactor.ex # use Chronicle.Reactor behaviour
reducer.ex # use Chronicle.Reducer behaviour
read_model.ex # use Chronicle.ReadModel macro
event_log.ex # Append and query events
event_types.ex # Register event types with Chronicle
constraints.ex # Register event constraints
read_models.ex # Query read model instances
reactors/
handler.ex # gRPC streaming reactor handler
reducers/
handler.ex # gRPC streaming reducer handler
projections/
registrar.ex # Projection registration GenServer
Samples/
console/ # Runnable console exampleLicense
MIT — see LICENSE.