We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Concepts
Documentation
Core Concepts
This guide covers the core concepts behind event sourcing and how they are implemented in ReckonDB.
Event Sourcing
Event sourcing is a design pattern that captures all changes to an application’s state as a sequence of events. Instead of storing just the current state, event sourcing maintains the complete history of changes that led to that state.
Key Benefits
- Complete Audit Trail: Every change is recorded as an immutable event
- Time Travel: Ability to reconstruct the state at any point in time
- Event Replay: Can rebuild state or create new views from existing events
- Debugging: Better understanding of bugs by seeing the exact sequence of events
- Business Intelligence: Rich data about how the system is used over time
Events
Events are immutable facts that describe something that happened in your domain. They are always expressed in the past tense and should be as specific as possible. An event is just a typed payload; a version suffix on the type (order_placed_v1) lets the shape evolve without rewriting history.
Over the gateway, an event is JSON:
{"event_type": "order_placed_v1",
"data": {"customer_id": "c-42", "total_amount": 89.50}}
On the BEAM with evoq it is the same shape as an Erlang map:
#{event_type => <<"order_placed_v1">>,
data => #{customer_id => <<"c-42">>, total_amount => 89.50}}
Event Properties
- Immutable: Once created, events cannot be changed
- Sequential: Events have a natural order based on when they occurred
- Descriptive: Events should be self-descriptive and include all relevant data
- Domain-Focused: Events should use domain language and concepts
Aggregates
Aggregates are the main building blocks of domain-driven design. They ensure consistency boundaries and encapsulate business rules. In evoq an aggregate is a module implementing the evoq_aggregate behaviour: execute/2 validates a command against current state and returns events; apply/2 folds each event back into state.
-module(order).
-behaviour(evoq_aggregate).
-export([init/1, execute/2, apply/2]).
init(_OrderId) ->
{ok, #{status => new, total => 0}}.
%% Validate the command, produce events (or refuse)
execute(#{status := new}, #{command_type := place_order, items := Items}) ->
{ok, [#{event_type => <<"order_placed_v1">>,
data => #{items => Items, total => total_of(Items)}}]};
execute(#{status := placed}, #{command_type := place_order}) ->
{error, already_placed}.
%% Fold the event into state
apply(State, #{event_type := <<"order_placed_v1">>, data := #{total := Total}}) ->
State#{status => placed, total => Total}.
Aggregate Properties
- Consistency Boundary: Ensures business rules are enforced
- Event Source: Produces events based on commands
- State Machine: Maintains state based on applied events
- Command Validation: Validates commands before producing events
Projections
Projections create specific views of your data by processing events. They build read models, calculate statistics, or maintain denormalized views. An evoq projection declares which event types it cares about (interested_in/0) and folds each one into a read model (project/4).
-module(order_summary).
-behaviour(evoq_projection).
-export([interested_in/0, init/1, project/4]).
interested_in() ->
[<<"order_placed_v1">>, <<"payment_received_v1">>].
init(_Config) ->
{ok, RM} = evoq_read_model:new(evoq_read_model_ets, #{}),
{ok, #{}, RM}.
project(#{event_type := <<"order_placed_v1">>, data := D}, Meta, State, RM) ->
Id = maps:get(aggregate_id, Meta),
{ok, NewRM} = evoq_read_model:put(Id, #{status => pending, total => maps:get(total, D)}, RM),
{ok, State, NewRM};
project(#{event_type := <<"payment_received_v1">>}, Meta, State, RM) ->
Id = maps:get(aggregate_id, Meta),
{ok, Summary} = evoq_read_model:get(Id, RM),
{ok, NewRM} = evoq_read_model:put(Id, Summary#{status => paid}, RM),
{ok, State, NewRM}.
Projection Properties
- Read Optimized: Built for efficient querying
- Eventually Consistent: Updated asynchronously as events are processed
- Purpose-Built: Designed for specific use cases
- Rebuildable: Can be recreated from event stream
Event Store
The event store is the persistent storage for your events. ReckonDB is BEAM-native: events are stored in Khepri (a tree database built on the Ra Raft library) and replicated across the cluster by Raft consensus. There is no PostgreSQL or external database to operate; the store runs in the Erlang VM alongside (or embedded in) your application.
Event Store Properties
- Append-Only: Events are only ever added, never modified
- Sequential: Events are stored in order, with a global sequence across streams
- Raft-Replicated: Khepri/Ra replicate the log for durability and consistency
- Indexed: An optional write-maintained secondary index (by tag, event type, or metadata key) turns cross-cutting reads into index lookups instead of scans
- Durable: Ensures events are never lost
Snapshots
Snapshots are point-in-time captures of aggregate state used to optimize performance: instead of replaying every event on load, evoq restores from the latest snapshot and replays only what came after. Implement the two optional evoq_aggregate callbacks and set how often a snapshot is taken with the snapshot_every config key.
%% In the aggregate module (optional callbacks)
snapshot(State) -> State. %% serialize state to snapshot data
from_snapshot(Data) -> Data. %% rebuild state from snapshot data
# config/config.exs
config :evoq, snapshot_every: 100 # snapshot after every 100 events
Snapshot Properties
- Performance Optimization: Reduces need to replay all events
- Optional: Not required for functionality
- Rebuildable: Can be recreated from event stream
- Configurable: Control when snapshots are taken
Consistency models
ReckonDB gives you three levels of write consistency, from a single stream out to arbitrary cross-aggregate invariants. Reach for the narrowest one that expresses your rule.
Per-aggregate (classic)
The default. Append only if the stream is at the version you read (expected_version); the check and the write are atomic for that one stream. This is the standard optimistic-concurrency model for a single aggregate, and it is all you need when the invariant lives inside one aggregate (“an order cannot be placed twice”).
Dynamic Consistency Boundary (DCB)
For invariants that span aggregates, a single stream’s version is not enough. DCB is a conditional append whose precondition is a filter over event tags and types (“no event matching this filter since sequence N”), with the check and the write in one atomic Raft operation. It handles uniqueness, allocation, and reservation across many streams without a saga or a lock (“no two accounts share a number”).
Command Context Consistency (CCC)
CCC extends DCB to condition on opaque payload fields, via declared payload indexes, not just the tags a producer applied. The decision can depend on data inside the event body that was never tagged, while keeping the same single-Raft-operation atomicity.
See the DCB & CCC guide for the full model of the cross-stream boundaries, including the conflict and retry loop.
The aggregate, projection, and decision samples above use evoq, the CQRS/ES framework that provides these building blocks on the BEAM (persisted to ReckonDB through the reckon-evoq adapter). From any other language, drive the store through reckon-gateway over gRPC or HTTP/JSON (with a native Go client, reckon-go).
Next Steps
Now that you understand the core concepts, you can:
- Follow our Getting Started Guide
- Explore Advanced Features
- Check out Example Applications