We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Concepts
Documentation
Examples
Small, runnable examples that cover the everyday patterns: appending events, folding a stream into current state, and enforcing an invariant. They use Track A (the reckon-gateway over HTTP/JSON) so they run from any language; the native BEAM equivalent with evoq is noted at the end.
Prerequisites
reckon-gateway running with an embedded store (see
Installation), listening on HTTP :8080. Each example
uses its own store name in the URL, so they stay isolated.
Counter
The simplest possible event-sourced value: a counter whose current value is the sum of its increments. There is no counter row to update; the events are the value.
BASE=localhost:8080/v1/stores/counters
# Create the counter (only if it does not exist yet)
curl -sX POST $BASE/streams/counter-1/events \
-H 'content-type: application/json' \
-d '{"expected_version":"no_stream","events":[
{"event_type":"counter_initialized_v1","data":{"value":0}}]}'
# => {"version":0,"count":1}
# Increment twice (additive, so no concurrency constraint needed)
curl -sX POST $BASE/streams/counter-1/events \
-H 'content-type: application/json' \
-d '{"expected_version":"any","events":[
{"event_type":"counter_incremented_v1","data":{"by":5}}]}'
# => {"version":1,"count":1}
# Read the stream and fold it client-side
curl -s "$BASE/streams/counter-1/events?from=0&limit=1000"
# sum the "by" deltas (and the initial value) => 5
TODO list
A list is one stream; each change is an event. The current list is a fold over the stream, so completion and removal are just more events, never an in-place update.
BASE=localhost:8080/v1/stores/todos
# Start a list
curl -sX POST $BASE/streams/list-1/events \
-H 'content-type: application/json' \
-d '{"expected_version":"no_stream","events":[
{"event_type":"list_started_v1","data":{"title":"Shopping"}}]}'
# => {"version":0,"count":1}
# Add two items in one append (atomic: both or neither)
curl -sX POST $BASE/streams/list-1/events \
-H 'content-type: application/json' \
-d '{"expected_version":0,"events":[
{"event_type":"item_added_v1","data":{"item_id":"i1","title":"Milk"}},
{"event_type":"item_added_v1","data":{"item_id":"i2","title":"Bread"}}]}'
# => {"version":2,"count":2}
# Complete one item
curl -sX POST $BASE/streams/list-1/events \
-H 'content-type: application/json' \
-d '{"expected_version":2,"events":[
{"event_type":"item_completed_v1","data":{"item_id":"i1"}}]}'
# => {"version":3,"count":1}
To render the list, read the stream and fold the events into whatever shape your
UI needs: start with an empty list on list_started_v1, push on item_added_v1,
flag completion on item_completed_v1. Pass an integer expected_version when a
change depends on the list not having moved under you; a mismatch returns HTTP
409 (re-read and retry).
Bank account
A balance that must never go negative. The withdrawal is a decision over the account’s own history: read, fold the balance, append only if the funds are there, using the head version as the concurrency check.
BASE=localhost:8080/v1/stores/bank
# Open the account
curl -sX POST $BASE/streams/account-1/events \
-H 'content-type: application/json' \
-d '{"expected_version":"no_stream","events":[
{"event_type":"account_opened_v1","data":{"owner":"Alice","currency":"EUR"}}]}'
# => {"version":0,"count":1}
# Deposit (additive)
curl -sX POST $BASE/streams/account-1/events \
-H 'content-type: application/json' \
-d '{"expected_version":"any","events":[
{"event_type":"funds_deposited_v1","data":{"amount":100.00}}]}'
# => {"version":1,"count":1}
# Withdraw: read first, fold balance, append only at the version you read
curl -s "$BASE/streams/account-1/events?from=0&limit=1000"
# fold: deposits minus withdrawals => 100.00, head version = 1
curl -sX POST $BASE/streams/account-1/events \
-H 'content-type: application/json' \
-d '{"expected_version":1,"events":[
{"event_type":"funds_withdrawn_v1","data":{"amount":25.00}}]}'
# => {"version":2,"count":1} (HTTP 409 if someone wrote first: re-read and retry)
If the balance is insufficient, your code simply does not append. If two withdrawals race, one gets the 409 version conflict, re-reads, and re-decides. That read, decide, append, retry-on-conflict loop is the heart of consistent event sourcing. The bank-account tutorial builds this out, including unique account numbers across every stream with a Dynamic Consistency Boundary.
Going further
-
Order lifecycle (native evoq). On the BEAM the
evoq guides
build an order domain end to end: an aggregate (
execute/2+apply/2), an order-summary projection, process managers across aggregates, and DCB/CCC decisions for cross-aggregate invariants, all persisted by the reckon-evoq adapter. - Polyglot client (Go). reckon-go is the native Go client for the gateway, with verified append, read, and DCB examples in its README, the model for driving the store from any language over gRPC.
Track B: the native BEAM version
On Erlang or Elixir you would not curl the gateway. You define an
evoq_aggregate for each thing (the command handlers and the state fold), an
evoq_projection for any read model, and an evoq_decision for cross-stream
invariants, all persisted by the
reckon-evoq adapter, which
calls the store directly rather than over HTTP. See the
evoq guides.
Next Steps
- Work through the bank-account tutorial
- Read the DCB and CCC guide
- Explore the full Reckon stack