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

  1. Work through the bank-account tutorial
  2. Read the DCB and CCC guide
  3. Explore the full Reckon stack