This is part 10 of Rheo. Rheo 0.2 cleaned the architecture: opaque backend handles, a local Rheo.Group, a portable %Rheo.Query{}. That is necessary and not sufficient. Until two stores implement the same public flows, the behaviour still models Mongo.

v0.3.0 ships Rheo.Backend.ETS and a shared conformance suite so the contract is exercised twice.

Why a second backend is the real test

Mongo shaped the first callbacks: collections, indexes, find_one_and_update. Those nouns leak. You start calling everything a document. You start assuming compare-and-set is a single round trip. You start treating connection topology as a given.

ETS cannot hide behind any of that. If ETS cannot implement create stream, append, fetch, renew, ACK, retry, reject, and query, then Rheo.Backend is a Mongo driver with extra steps.

ETS is deliberately weak as a store (durable: false). That is useful. If the semantics still hold in memory — fencing, immutable events, independent groups — the semantics belong to Rheo.

Opaque handles vs topology

A Rheo instance starts whatever child_spec/1 the backend returns.

  • Mongo’s handle is a driver process name.
  • ETS’s handle is an owner GenServer that owns per-instance tables.

Callers never pass table refs or Mongo URLs through Rheo.fetch/3. They pass rheo: MyRheo. The day that rule slips, every backend grows a special case and the facade is dead.

Capabilities without forked correctness

Rheo.Backend.capabilities/0 declares honest differences:

FlagMongoETS
durabletruefalse
secondary_indexestruefalse
atomic_compare_and_settruefalse

Capabilities gate optional suites: replay, partitions, things a backend might not offer yet. They must not make lease fencing optional. Both backends must refuse a stale ACK. If a capability could turn off correctness, it is not a capability. It is an excuse.

Later revisions split capabilities into guarantees and mechanisms (part 15). The 0.3 version already had the right instinct: declare what you are, do not fork the meaning of ACK.

Conformance, not copy-paste

The BackendContract ExUnit template (test/support/backend_contract.ex) injects the same cases into:

  • ETS contract tests (always on in CI)
  • Mongo contract tests (@tag :mongo)

When a test fails on one backend only, you found an abstraction leak. The suite is the contract. The markdown ADRs are commentary.

Correctness cases are never skipped because a backend is “simple.” Only cases for undeclared guarantees are skipped. ETS does not get to skip stale-lease tests because it is in memory.

Ownership and restart

ETS tables die with the owner process. Restarting the ETS child empties the log. Document that. Do not pretend ETS is a production durable backend. Do not hide it behind a retry that “usually” works if the owner did not crash.

ETS is for tests, Livebook, ephemeral apps, and proving the API. Mnesia later gives you an ETS-shaped durable single node. Postgres and Redis give you multi-node. The ladder only works if the first rung is honest.

What 0.3 refused to weaken

  • At-least-once delivery with fencing tokens
  • Immutable events; mutable deliveries per group
  • Portable query shape (order_by, not Mongo :sort)
  • Consumption never deletes events

Everything that came next — search and replay (0.4), partitions and frontiers (0.5), Ecto (0.6), Broadway (0.7), the 0.8 reset, Redis (0.9), Mnesia, ops, the 1.0 freeze — builds on a contract two backends already satisfied.

If you are writing a backend today, start with ETS in the suite and stay there until fencing is boring.

Read next: Search and Replay the Event History

Docs: ETS guide · ADR 012 · ADR 014