An Elixir event store adapter that integrates Commanded with EventSourcingDB – a purpose-built database for event sourcing.
Documentation:
Supported Features
- ✅
append_to_stream- Write events to a stream with expected version handling - ✅
stream_forward- Read events from a stream - ✅
subscribe- Transient subscriptions for real-time notifications - ✅
subscribe_to- Persistent subscriptions with checkpointing - ✅
ack_event- Event acknowledgment for checkpoint updates - ✅
unsubscribe- Cancel subscriptions - ✅
delete_subscription- Remove subscriptions and checkpoints - ✅ Correlation and causation ID tracking via metadata
- ✅ CloudEvents format for event storage
- ❌ Snapshots - ESDB has no snapshot storage/feature. Read more about the snapshot paradox
Installation
The package can be installed by adding commanded_eventsourcingdb_adapter to your list of dependencies in mix.exs:
def deps do
[
{:commanded_eventsourcingdb_adapter, "~> 0.0.1"}
]
endConfiguration
Configure the adapter in your application config:
# config/config.exs
config :my_app, MyApp,
event_store: [
adapter: Commanded.EventStore.Adapters.EventSourcingDB,
client: [
api_token: "your-api-token",
base_url: "http://localhost:3000"
],
stream_prefix: "myapp",
source: "https://my.app"
]Configuration Options
:client- Required. ESDB client configuration containing:urland:token.:stream_prefix- Optional. Prefix for stream subjects. Defaults to"".:source- Required. Event source URI for CloudEvents.
CloudEvents
Commanded has its own internal event representation as RecordedEvent whereas EventSourcingDB follows the CloudEvents specification. The adapter maps between the two as described in ADR 0001.
Event Source
Define in your config, see above.
Event ID
The event ID within commanded's RecordedEvent is a concatenation of the source + event id from ESDB: #{event.source}/#{event.id}
Event Types
Commanded has a default type provider, that (de)serializes your module name as
event type. In the example below, the event has Elixir.AccountOpened as event
type. This is perhaps not the best value for your event type, consider your
custom type provider
for naming your events types.
Event Subject
Commanded is stream-oriented, which translates to subjects as per CloudEvents spec, ADR 0002 specifies the used semantics.
The subject: /#{stream_prefix}/#{identity_prefix}/#{aggregate_uuid}
stream_prefix: defined in yourconfig/config.exswithin yourevent_storeconfig for your commanded appidentity_prefix: Using the prefix option inidentifyaggregate_uuid: define aggregate identity in your router
Metadata Storage
Commanded stores correlation_id, causation_id, and metadata as part of the
event's data field using a special __commanded_metadata__ key:
# Data field stored in ESDB
%{
"__commanded_metadata__" => %{
"correlation_id" => "uuid-string",
"causation_id" => "uuid-string",
"metadata" => %{"key" => "value"}
},
# ... event data fields
}Sample CloudEvent
This is what an event looks like when stored in EventSourcingDB:
{
"specversion": "1.0",
"id": "5",
"source": "https://my.app",
"subject": "/myapp/bank-account/ACC123",
"type": "Elixir.AccountOpened",
"datacontenttype": "application/json",
"data": {
"__commanded_metadata__": {
"correlation_id": "aaa-bbb-ccc",
"causation_id": "ddd-eee-fff",
"metadata": {}
},
"account_number": "ACC123",
"initial_balance": 1000
},
"time": "2025-04-15T10:00:00Z",
"predecessorhash": "0000000000000000000000000000000000000000000000000000000000000000",
"hash": "abc123..."
}Testing
Run tests using:
mix test
Tests use Testcontainers to spin up an EventSourcingDB instance.