ADR-0031 — Outbox-tail verifier segment (Tier-4 pattern)
ADR-0031 — Outbox-tail verifier segment (Tier-4 pattern) #
- Status: Proposed
- Date: 2026-05-11
- Deciders: Natan
- Source: PRD-14 ratification (Convergence E of the
2026-05-09 master synthesis ↗ §2.5). Complements ADR-0026. The pattern emerged from re-reading PRD-08 §08.2.7 (C5) against Bellemare's outbox chapter and DDIA Ch 12, which insist that change-stream observation is a different claim shape from snapshot-state observation.
Context
Context #
ADR-0026 names two segment roles inside one synthetic transaction: drive + verify, where verify reads each store the workflow was supposed to write to and asserts the side-effect landed. Every verify step today is a snapshot-state query — "does this row exist now, with this content?"
That answers half the question for systems built around an outbox or a change-data-capture log. The outbox row's purpose is to be drained, transformed, and observed downstream (Bellemare 2nd ed., Building Event-Driven Microservices Ch 6, lines 4546–4609). A row that exists with the right shape but never drains is a failure of the system the outbox exists to serve — yet a snapshot-state query says "row found, pass." The contract is asserting state, not change.
Two books push the same way:
- DDIA Ch 12 §"Change Data Capture" (line 14341) and
§"CDC and Database Schemas" (14460): the outbox schema is a public API; ordering and schema-evolution are part of what the outbox guarantees, not free.
- Bellemare Ch 6 (4546–4609): outbox row carries
status (pending → claimed → delivered), an ordering identifier, and a payload whose schema must match the published event schema. The drainer's job is to advance status; the verifier's job is to assert the drainer is doing its job.
Bellemare also distinguishes two CDC patterns (4261–4429): log-tailing CDC (Debezium-style, reading the database's WAL) and transactional outbox CDC (application-managed table). Snappy implements the second — outbox_entries is an application-level construct that the outbox-writer + outbox- processor pair (src/publishing/outbox-{writer,processor}.ts) read and advance. The verifier reads the outbox row itself as the changelog event.
The first Tier-1 synthesis (DDIA §3.2) proposed an ADR named "CDC-tail verifier segment." Bellemare's distinction (Ch 6) means the right name for snappy's pattern is outbox-tail, not CDC-tail — the latter implies log-tailing, which snappy doesn't do.
Decision
Decision #
A scenario whose claim includes a state-changing event the SUT emits (outbox row, change-stream, durable-execution log) gets a fourth segment role: outbox-tail verifier.
The outbox-tail verifier is a kind of verifier segment in the ADR-0026 sense, but it asserts a different family of claims:
| Claim family | ADR-0026 verifier (snapshot-state) | ADR-0031 outbox-tail verifier |
|---|---|---|
| Question answered | "Did the row land?" | "Did the change-event land + drain in order with the right schema?" |
| Surface read | Mongo / S3 / ClickHouse rows | outbox_entries rows + their status transitions |
| Time model | Snapshot at observation time | Sequence of transitions across a window |
| Pass criterion | Row present + content valid | Row present + payload schema valid + drainage transition observed + ordering preserved |
| Failure-as-success | Row missing | Row stuck at pending; payload drift; out-of-order delivery |
The verifier observes the outbox row as the event, not as a row whose existence is a proxy. Per Bellemare 4548, durability is the guarantee the outbox offers; drainage is the success criterion the contract must assert. PRD-08 §08.2.7 (C5) splits into four sub-claims in PRD-14 to reflect this: C5a (existence) + C5b (payload-schema fidelity) + C5c (drain / transition invariant) + C5d (ordering invariant).
Why "outbox-tail," not "CDC-tail" #
Bellemare Ch 6 (4261–4429) treats log-tailing CDC and outbox CDC as distinct patterns with different consistency models, isolation properties, and operational profiles. Snappy implements outbox. Calling the verifier "CDC-tail" would suggest SQA reads the underlying database WAL — it does not, and the snappy team would not let it. The verifier reads outbox_entries rows. The name should match.
Why this is Tier-4, not a slot in ADR-0026 #
ADR-0026 names three Tier-1–Tier-3 segment shapes implicitly (drive, verify-by-state, gap-log; the third was added by ADR-0027). The outbox-tail pattern adds a fourth: verify-by- change. It is complementary, not replacement — many verify steps remain snapshot-state queries (S3 object existence, Mongo document well-formedness) because those stores aren't a changelog. The two shapes coexist in one segment file when both apply (PRD-08 §08.2.3 stays snapshot-state for C1; §08.2.7 moves to outbox-tail for C5).
Implementation shape (informative, not normative) #
PRD-08 §08.2.7 lands the first instance. The implementation reads snappy.outbox_entries for {eventType: "snapshot.created", entityId: snapshotId}, then asserts the four C5 sub-claims by re-reading the row (or watching the change-stream when reachable) over a budgeted window. The verifier emits one Result per sub-claim, all under the same C5 parent step.
Polling vs change-stream is an implementation detail. The claim shape is the same: change observed, schema valid, drainage observed, ordering preserved.
Consequences
Consequences #
Architectural #
- Glossary §segment gains a fourth example: **outbox-tail
verifier** alongside drive / verify / gap-log. The Durable Execution Engine entry (added in PRD-14 §14.3.1) names Hatchet as the canonical workflow runtime — the outbox the outbox-tail verifier reads is the durable side-effect of a Hatchet workflow step.
- A contract doc that asserts an outbox row exists now must
assert all four sub-claims (existence + payload-schema + drainage + ordering). One-line "outbox row present" claims are deprecated.
- PRD-08 §08.2.7's spec is the test case for this ADR: if the
amended spec is unimplementable on outbox_entries as snappy ships it, the ADR has failed.
Behavioural #
- C5 in PRD-08 now produces ≥4 leaf Results (one per sub-claim)
instead of 1. Cron noise increases proportionally; this is the cost of catching drainage stalls.
- A stuck-at-
pendingoutbox row was previously a pass (row
exists). It is now a fail with a gap row citing C5c (drain invariant). This is a genuine behaviour change in what SQA considers "snappy did its job."
- Payload-schema drift (e.g., snappy adds a field, removes one,
renames one) was previously invisible to SQA. C5b makes it a fail with a gap row citing the missing / unexpected field.
Falsifiability #
- If a future verifier implements outbox-tail by querying the
underlying database WAL instead of outbox_entries, this ADR has failed — the name "outbox-tail" implies application-level observation, not log-tailing.
- If C5b's payload-schema check is implemented as deep equality
against a frozen example instead of a schema match, this ADR has failed — schema-evolution rules (Bellemare 4590, DDIA 14460) must allow additive change without contract failure.
- If contributors collapse C5a–C5d back into a single "row
present" check to reduce leaf counts, this ADR has failed — the four sub-claims are the contract.
See also
See also #
- ADR-0026 — the parent
verifier-segment pattern this ADR extends. ADR-0026 carries a soft amendment in PRD-14 §14.1.3 (Newman citation softened; see-also pointer to ADR-0032).
- ADR-0027 — the
gap-row evidence shape outbox-tail uses.
the C5 amendment that ratifies this ADR.
- PRD-14 ↗ — the PRD this ADR
ships under.
- Bellemare, Building Event-Driven Microservices (2nd ed., 2024)
Ch 6 (lines 4261–4609) — outbox vs log-tailing CDC; outbox contract elements (existence, schema, drainage, ordering).
- Kleppmann, Designing Data-Intensive Applications Ch 12
§"Change Data Capture" (14341) and §"CDC and Database Schemas" (14460) — outbox schema as public API; ordering and schema-evolution.
docs/research/2026-05-09-event-driven-microservices-dossier.md↗ §1, §3 — file:line citations.docs/research/2026-05-09-designing-data-intensive-applications-dossier.md↗ §3.2, §3.5 — the original CDC-tail proposal renamed here.