ADR-0007 — All durable state goes through Substrate's four SPIs; no per-backend mocapi modules¶
- Status: Superseded by ADR-0020
- Date: 2026-04-11
Context¶
A multi-node MCP deployment has four kinds of durable state:
- Sessions. Each
McpSession(session ID, protocol version, client capabilities, log level, initialized flag) must be readable by any node a client lands on, and must expire when idle. - Pending elicitation/sampling rendezvous. When a tool calls
elicit()orsample(), the request goes out via the transport on one node; the client's response may arrive on a different node. The handler's virtual thread on the originating node must unblock when that response lands. See ADR-0008. - SSE event journals. A client that drops its connection reconnects
with
Last-Event-IDand expects to replay missed events. Across nodes, the journal must be shared. - Cross-node notifications. A
notifications/logpublished on one node must reach a client connected to a GET stream on another.
An earlier mocapi design shipped a McpSessionStore interface and a
matrix of per-backend modules (mocapi-session-store-redis,
-hazelcast, -jdbc, -cassandra, -mongodb, -dynamodb, -nats),
each with its own auto-configuration, conditional ordering, and
connection bean wiring. Adding a backend meant a new mocapi module;
keeping all of them in lockstep meant duplicated effort across
specs 113, 114, 115, 130, 131, 134, 136, 146.
Decision¶
Mocapi delegates every durable-state concern to Substrate's four SPIs and ships no backend-specific modules of its own.
| Substrate SPI | Mocapi usage |
|---|---|
AtomFactory |
Session storage (AtomMcpSessionStore) |
MailboxFactory |
Elicitation / sampling rendezvous |
JournalFactory |
SSE event journaling for resumption |
NotifierFactory |
Cross-node fan-out for the GET notification channel |
Rules:
- The backend is a pom-level decision. Adding
substrate-redis/substrate-jdbc/substrate-hazelcast/substrate-dynamodbto the classpath causes Substrate to auto-configure the four SPIs on that backend. Mocapi never names the backend. - The default backend is in-memory (Substrate's fallback). It works for development and single-node deployments. The application logs a visible warning at startup when the in-memory fallback is in use, so nobody ships to production by accident (spec 052).
McpSessionStoreis a thin facade overAtom<McpSession>. The adapter is roughly thirty lines:find(id)reads the atom,savewrites it,touchrefreshes the TTL on access,deleteremoves it. No per-backend code lives in mocapi.- Sessions carry a configurable TTL; reads refresh it. Backend choice determines persistence semantics — Postgres and DynamoDB persist across restarts; Redis can be configured either way; in-memory is process-local.
- Storage-at-rest encryption is a Substrate concern. Adding
substrate-cryptoto the classpath wraps the configuredAtomFactory/MailboxFactory/JournalFactorywith AES-GCM encryption transparently — values are encrypted before they leave the JVM. This is a separate concern from the SSE event-ID encryption in ADR-0005; the two keys are independent and serve different threat models. Production deployments should set both. - Mocapi publishes no backend-specific Spring Boot starters
(
mocapi-redis-spring-boot-starteretc.). Users wire the backend themselves: addmocapi-spring-boot-starter, the Substrate backend, and the appropriate Spring Boot data starter. The examples module shows the pattern for each backend.
Consequences¶
Wins:
- Adding a new backend is a Substrate problem, not a mocapi problem. Substrate ships an SPI for it; mocapi inherits the support without any code change here.
- The reactor lost seven session-store modules and four backend starters during the migration. Maintenance burden dropped sharply.
- Multi-node correctness is built in: with a clustered Substrate backend, sessions, mailboxes, journals, and notifiers all work across nodes. No mocapi-specific clustering code.
Costs:
- Users now pick Substrate dependencies directly. The docs make this
visible (
docs/backends.md); the examples module carries one app per backend so the pom recipe is copy-pasteable. - Mocapi cannot tune backend-specific behavior (Redis pipelining, Postgres connection pooling, DynamoDB read consistency) — that is Substrate's job. If a tuning knob is missing, the right answer is to push it into Substrate.
- A development server that forgets to add a Substrate backend silently uses in-memory storage and loses sessions across restarts. The startup warning is the mitigation.
Non-goals: mocapi does not abstract over Substrate. The
McpSessionStore facade exists because session lookup is a hot path
and a typed session API is more pleasant than raw atom reads; the
mailbox/journal/notifier APIs are used directly. There is no
"mocapi storage abstraction layer" sitting on top of Substrate.
Code anchors: mocapi-server/.../AtomMcpSessionStore.java. Substrate migration landed in commit 83bbdc74 (2026-04-11).