Skip to content
SQA Cockpit

ADR-0031 — Outbox-tail verifier segment (Tier-4 pattern)

ADRsUpdated 6 min readEdit on GitHub ↗
4 sections··

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 familyADR-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 readMongo / S3 / ClickHouse rowsoutbox_entries rows + their status transitions
Time modelSnapshot at observation timeSequence of transitions across a window
Pass criterionRow present + content validRow present + payload schema valid + drainage transition observed + ordering preserved
Failure-as-successRow missingRow 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-pending outbox 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 #

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).

gap-row evidence shape outbox-tail uses.

the C5 amendment that ratifies 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.

Was this page helpful?