Write a Backend
A backend is a module that implements Wow's storage ports on one technology. The built-in PostgreSQL and MongoDB backends are backends like any other: they implement the same ports, are found the same way, and pass the same contract tests. The ports are @WowSpi: a minor release may change them, and using them needs @OptIn(WowSpi::class) (API Levels).
The ports
A store holds a node's durable state. Its ports are in :spi:
| Port | Holds | Contract |
|---|---|---|
EventLog | A context's log: committed streams by partition and position, unique by version and by request | EventLogSpec |
SnapshotStore | The latest snapshot of each aggregate | SnapshotStoreSpec |
CheckpointStore | Where each subscription stands, per partition | CheckpointStoreSpec |
LeaseStore | Which node owns which partition | LeaseStoreSpec |
FailureLedger | The parked aggregates | FailureLedgerSpec |
ReservationStore | The reserved unique keys | ReservationStoreSpec |
TimerStore | The commands scheduled for later | TimerStoreSpec |
A store may hold logs, snapshots or both: an Elasticsearch store holds snapshots and never a log. Beside the stores, a Sink copies commits somewhere else (SinkSpec), a topic transport carries published streams and commands between contexts (TopicTransportSpec), and a query backend serves a model's queries (QueryBackendSpec, EventQueryBackendSpec and DatasetQueryBackendSpec in :query-tck).
The provider
A backend gives the host a StoreProvider: the type a configuration names (wow.stores.<name>.type), what it needs from the application, whether it is available from the application's resources alone (what wow.backend: auto chooses by), and open, which returns a Store for one configured instance. open touches nothing in the database: the store opens each port on its first use, one log per context, and remembers what creates its schemas, so wow.ddl decides whether they are created, validated or left alone.
The host finds providers through Java's ServiceLoader, from the module's META-INF/services/me.ahoo.wow.spi.store.StoreProvider, and among the application's beans; a bean takes the place of a provider of the same type on the class path. A provider is stateless: each store instance gets its own Store, over the resources its configuration finds, a DataSource or a driver's client.
The contract tests
A backend's correctness is the contract. Its container tests extend each spec of the ports it implements and supply a fresh, empty store:
class PostgresEventLogTest : EventLogSpec() {
override fun createLog(context: String, partitions: Int): EventLog = PostgresFixture.log(partitions, context)
}Each test of a spec runs on a context of its own, so a backend that keeps a table or a collection per context isolates them. A query backend declares the features its storage may decline, keyset pages or percentiles say, and the spec skips their tests; a known divergence from the filter semantics is listed by id rather than hidden. :tck-containers starts the shared containers with pinned images, and the backend's module runs the suites in src/integrationTest.
The rules
- A backend depends on
:spi, and on:querywhen it serves queries; it never depends on another backend or on the runtime.:architectureholds this, and the new module joinsALLOWED_PROJECT_DEPENDENCIES. - It records the format of every structure it creates and refuses one of another format, so a node never writes a layout it does not understand.
- It is a published module: it keeps an ABI dump, a README that links to the site, and a page under Operate for its operators.
- A store it cannot reach is reported as
StorageUnavailableException, which the node treats as systemic: a subscription pauses and retries, and parks nothing.
The PostgreSQL backend, backends/postgres, is the reference to read alongside its tests.