01Verdicts bind to data versions, not to runs
The single decision the whole architecture rests on. It makes history re-judgeable, lets a rule written in September condemn a window in July, and turns 'how far back does this go' into a query.
ADR-0120 views, in reading order. Every view ships three ways: an HTML page, an SVG that re-opens in diagrams.net fully editable, and draw.io source.
A consumer super-app's lakehouse has one recurring outage that never gets a status page: a table that is present, fresh-looking and wrong. This set architects the service that judges whether data is fit to use, binds that judgement to an immutable data version rather than to the pipeline run that produced it, and decides whether bad data is allowed to circulate. Read it in seven acts — the boundary first, then who it serves and what goes wrong for them, then structure, data, runtime, operations and assurance. Every view is one page with its reasoning beside it, and every page links its editable SVG and draw.io source.
A verdict binds to an immutable data version, never to the pipeline run that produced it — and the plane that judges data is separate from the plane that circulates it.
Every consumer product people use daily runs a lakehouse behind it, and every one of those lakehouses has the same recurring outage that never gets a status page: a table that is present, fresh-looking and wrong. The job succeeded. The dashboard rendered. The pricing model scored. Nobody was paged, because nothing failed. Four weeks later a merchant disputes an invoice and someone discovers that a currency column silently switched units, or that a `restaurant_is_open` flag has been true for a chain that shut in March. At that point the useful question is not 'how do we fix it' — the fix is an hour's work upstream — but 'how much of the last four weeks do we have to disbelieve, and who else read it'. Almost no data quality tooling can answer that, because almost all of it records that last night's run passed rather than that a specific version of a specific table was judged and found sound.
Assertions are versioned, owned, priced and compiled to bounded query plans, then pushed down into the lakehouse engine so no dataset ever leaves its storage layer to be judged. Evaluation is triggered by a table-version commit, by a schedule, and by a reconciliation sweep that exists because the event stream will lose something eventually. Every evaluation writes an immutable verdict keyed by the data version and the assertion version that produced it — never by the run — and a derived state index answers the platform's one hot question, 'is this dataset safe to read right now', in under twenty milliseconds with an explicit staleness bound. Enforcement is a separate step that reads that state: an offending row-level batch is quarantined under the source's own classification, a dataset-level failure withholds promotion of the new version so consumers keep reading the last known good one, and nothing anywhere writes a corrected value back into a dataset. Because verdicts are bound to versions, a rule written today can be evaluated against any past version, which is what turns the four-week question into a corruption window, a blast-radius list, and a set of suspect marks somebody has to clear.
The single decision the whole architecture rests on. It makes history re-judgeable, lets a rule written in September condemn a window in July, and turns 'how far back does this go' into a query.
ADR-01No component writes a corrected value into a dataset. Blocking is non-promotion, so the rejected version stays inspectable and a service outage degrades to 'consumers read the last known good'.
ADR-02The quality service publishes a claim about a version; promotion is a distinct act that consults it. That keeps the service off every write path without making enforcement a matter of goodwill.
ADR-03Four thousand reads a second against a far lower verdict rate. The index is rebuildable from the verdict store, which is why its recovery objective is a rebuild rather than a restore.
ADR-04'Unknown' and 'no coverage' are first-class states, and a Tier 1 dataset in either is not promoted. Fail-safe, not fail-open.
ADR-05Assertions compile to engine-native plans and fuse into one pass per partition. Backfill runs in its own lane so re-judging history never delays judging the present.
ADR-07A new rule is backfilled over a 30-day lookback and the result reported before it may enforce, because a rule that condemns 18 of the last 30 versions is as likely to be a bad rule as a discovery.
ADR-10Per-assertion circuit breaking, probation for new rules, and automatic demotion above a 5% false-positive rate — structural, not operational courtesies.
ADR-12Overrides and suppressions are objects with an owner, a justification and an expiry, written to an immutable log before the state changes. Unbounded suppression is unsupported.
ADR-16A data quality service outlives the warehouse it was built against, because the judgements it recorded are what a future team uses to decide whether to trust a number from today. These are the properties that should survive a change of engine, of cloud, and of the people who built it.
ADR-01 needs one thing from the storage layer: an immutable, addressable version of a dataset. Delta gives it, Iceberg gives it, Hudi gives it, and a partitioned object layout with a manifest gives it. What would break the decision is not a migration but a future team recording verdicts against a job id because it was easier to join — which is why the version key is drawn in the data model rather than left to an implementation note.
Every threshold in this design will be renegotiated: the 2% ceiling, the 5% false-positive rate, the 15-minute Tier 1 SLA, the 2.5% cost cap. None of them is load-bearing in the way ADR-05 is. A platform that renders 'nobody checked' and 'forty checks passed' identically will produce the silent-corruption case again no matter how good its rules are, and that is true of any tooling in any decade.
Because the quality service only publishes claims, its own failure has a defined, benign meaning: consumers read the last known good version. The moment a future team puts promotion inside the quality service to make enforcement airtight — a reasonable-sounding change — a quality service outage becomes a platform-wide write outage. ADR-03 states the boundary explicitly so that trade is made deliberately if it is ever made.
Quality debt does not accumulate because people are careless; it accumulates because an exception granted during an incident has no natural end. ADR-16 makes the end structural. Of everything here, this is the decision most likely to be quietly weakened — by a 'permanent exemption' field added for a good reason — and the one whose loss would be hardest to detect.
The uncomfortable truth about data quality platforms is that they fail socially, not technically: the alerts become noise and the gate becomes advisory in practice while remaining mandatory on paper. Measuring the false-positive rate per assertion and acting on it automatically is the only defence that does not depend on someone's attention, and it will still be the answer when the assertions are generated by a model rather than typed by an engineer.
Every number here is a stated assumption from the requirement, chosen to be argued with. The right-hand column names the view where the mechanism that meets it is drawn.
| Quality | Target | How it is met | View |
|---|---|---|---|
| State read availability | ≥ 99.95% monthly | Derived projection in a DynamoDB global table, read path replicated to a second region, and readable independently of the control plane so a control-plane outage does not stop consumers getting an answer. | 16 |
| Control plane availability | ≥ 99.5% monthly | Stateless APIs on EKS across three AZs with a leader-elected scheduler; Aurora multi-AZ behind it. A control-plane outage delays new verdicts, it does not change existing state. | 07 |
| Evaluation plane availability | ≥ 99.0% monthly | Deliberately the weakest target of the three: a missed evaluation window is recoverable by the reconciliation sweep, a wrong verdict is not. | 12 |
| Verdict publication latency | ≤ 90 s p95, ≤ 180 s p99 after a Tier 1 commit | Commit-event trigger, metadata assertions evaluated first without a scan, and all scan assertions for a partition fused into a single pass. | 11 |
| State read latency | ≤ 20 ms p99, ≤ 8 ms p50 in-region | Key-value projection keyed by dataset and partition, sized for the orchestrator's pre-run checks rather than for human traffic. | 08 |
| Freshness detection | ≤ 6 min for Tier 1 (1 cadence + 60 s) | Five-minute evaluation cadence against a 15-minute staleness SLA, with an unevaluated freshness assertion counted as breached. | 13 |
| Throughput | 180,000 evaluations/day; 9,000/hour peak; 4× burst for 90 min | Per-dataset work partitioning with per-domain concurrency quotas, coalescing by (dataset, partition) rather than per commit so a backfill storm does not multiply work. | 14 |
| State read throughput | 4,000 reads/s sustained, 12,000/s peak | Read path scaled independently of evaluation, since it is exercised by every consumer job and orchestrator task at roughly 3,000 times the verdict write rate. | 16 |
| Scale | 12,000 datasets, 40,000 active assertions, 2.4 PB | Assertions fused per dataset scan and compiled to bounded plans, so cost grows with data scanned rather than with assertion count. | 06 |
| Verdict retention | 25 months, hot for 90 days | Append-only Delta on S3 with lifecycle tiering; sized to two annual audit cycles plus margin. | 09 |
| Override audit retention | 7 years, immutable | S3 Object Lock in compliance mode, written before the state change it authorises, so the record cannot be the thing that failed. | 20 |
| Recovery | Control plane RPO 5 min / RTO 30 min; verdict store RPO 1 min / RTO 60 min; state index RPO 15 min / RTO 10 min | The state index has the tightest RTO and the loosest RPO because it is fully re-derivable: its recovery is a rebuild from verdicts, not a restore. | 09 |
| Detection quality | MTTD ≤ 4 h p90 (Tier 1 correctness), ≤ 24 h p90 (silent-corruption class) | Measured per assertion type as a first-class platform metric, because MTTD rather than alert count is the measure of whether the service works. | 18 |
| Alert credibility | ≤ 5% false-positive rate per assertion over 30 days | Tracked per assertion with automatic demotion to advise above the threshold. The single most consequential number in the design: above it, the gate is decorative. | 15 |
| Coverage | 100% of Tier 1 datasets carry schema + freshness + one aggregate assertion | Coverage rendered as part of the verdict, and zero-coverage datasets surfaced on the domain scorecard as debt. | 18 |
| Cost | ≤ 2.5% of data platform compute spend | Per-dataset budgets, authoring-time cost estimation, metadata-only evaluation preferred over scans, and the full-scan fraction reported per dataset. | 17 |
Three of this architecture's claims are cheap to test and expensive to be wrong about. A prototype that proves these can be built on; one that skips them has demonstrated running a SQL query against a table, which is the easy half.
| Risk | If it lands | Response |
|---|---|---|
| The gate is advisory in practice | Consumers and pipelines read state until the first incident where the gate is inconvenient, then stop. The platform continues reporting verdicts nobody acts on, and the silent-corruption case returns with a dashboard attached. | Instrument the gate read itself: 'no gate reads from a consumer that used to read' is an alert in view 18. Enforcement that cannot be observed is not enforcement. |
| Distributional baselines learn the corruption | A slow drift — a unit change rolled out gradually, a growing share of a new merchant category — is absorbed into the 28-day baseline and never fires. The assertion type intended to catch silent corruption becomes the one that ratifies it. | Keep reconciliation assertions against independently computed control totals as the backstop for anything financial, and treat a baseline retrained during an open corruption window as invalid. |
| Suspect marks accumulate and stop meaning anything | One-hop propagation from a handful of upstream defects leaves hundreds of tables marked, nobody clears them, and consumers learn to ignore the mark — the same failure as alert fatigue, one layer down. | Marks are debt items with owners and ages, surfaced on domain scorecards. If the median age climbs, the propagation depth is wrong and ADR-11 gets revisited. |
| Quality work is starved by the compute it shares | Tier 1 verdicts arrive late precisely during a platform incident, because the warehouses are saturated by the pipelines whose output most needs judging. | Reserved capacity for the Tier 1 live lane with backfill on spot; measure verdict lag during the next real incident rather than in steady state, and revisit ADR-07 if the lane cannot be held. |
| Coverage is thin where it matters and thick where it is easy | Twelve thousand datasets with forty thousand assertions can still leave the currency column of the payments fact table unguarded, which is exactly the shape of the original defect. | Coverage measured against each dataset's declared contract rather than as an absolute count, with zero-coverage Tier 1 datasets blocked from being declared consumable. |
| Lineage precision bounds the blast-radius claim | The corruption-window report reads as complete but omits any downstream consumer the catalogue does not know about — a notebook, an export, a model trained from a snapshot. | State the lineage coverage percentage on the report itself, so a blast-radius list is read as a floor rather than as an inventory. |
Sixteen decisions make up this architecture. Everything else across the twenty views is convention, sizing or consequence. Each record states the forcing question, the context that makes it hard, what was decided, how it is realised on Databricks and AWS, the options weighed, what the choice buys and costs, the conditions that would flip it, why it should still be right in ten years, and the lesson worth carrying to a different system.
Status of this document. This is a design, not a report on a running system. The rates, latencies, volumes, retentions and thresholds throughout are stated assumptions, sized for a mid-size consumer super-app's lakehouse — roughly 12,000 datasets, 2.4 PB, 40 TB a day — and chosen to be explicit enough to be argued with rather than vague enough to be unfalsifiable. Where a number is load-bearing, the record says what would change if it were wrong. Nothing here has been measured in production.
What a judgement is attached to, what the service is allowed to do with it, and what it means when there is none.
ADR-01A verdict binds to an immutable data version, never to the pipeline run ADR-02The service judges and never repairs; blocking is non-promotion ADR-05Absence of a verdict is never a pass; unknown and no-coverage are first-class statesWhere the gate sits, how the answer is served to four thousand readers a second, and why a lost event is not a pass.
ADR-03The circulation gate is a separate step that reads published state ADR-04Current state is a derived projection with a published staleness bound ADR-09The commit event stream is an optimisation; the reconciliation sweep is the guaranteeWhose compute does the judging, how a rule's price is bounded before it runs, and what happens when the budget runs out.
ADR-06An assertion must compile to a bounded, fusable plan or it is refused ADR-07Computation goes to the data, on declared priority lanes ADR-08Cost is bounded per dataset, priced at authoring time, and capped in aggregateRe-judging the past, and how far the consequences of a discovery are allowed to travel.
ADR-10Retroactive evaluation runs in its own lane, behind a mandatory human judgement ADR-11Suspect propagates one hop; the blast radius is a report, not a cascadeKeeping rules worth believing, keeping authority with the owner, and making every exception end.
ADR-12A bad rule cannot take down a pipeline: probation, circuit breaking and automatic demotion ADR-13Blocking authority belongs to the dataset owner, and the action is fixed at authoring time ADR-16Every escape hatch is an expiring object that creates a debt itemWhy a verdict must not leak what it judged, and why quarantine is custody rather than storage.
ADR-14A verdict must not become a side channel ADR-15Quarantine only where violations are row-attributable, with a ceiling that escalates to blockEvery capability below names the choice, where it comes from, the alternative that was weighed, and why this one won. The stack is Databricks Lakehouse on AWS because the topic genuinely belongs to the table layer — a verdict bound to an immutable table version needs a format that provides one — and because the practice's recent use cases have leaned on open-source-on-premises and Azure; nothing in the requirement is written to a vendor's service catalogue.
| Capability | Choice | Origin | Credible alternative | Why this one | Record |
|---|---|---|---|---|---|
| The judged estate | Delta Lake tables on S3, read through Databricks | Databricks / AWS | Apache Iceberg on S3 with a separate query engine | The design needs one thing from the format — an immutable, addressable table version that survives long enough to be re-judged. Delta and Iceberg both provide it; Delta is chosen because Unity Catalog supplies the grants, lineage and version history the service reads, in one governance model rather than two. | ADR-01 |
| Assertion execution | Databricks SQL warehouses, reserved live lane | Databricks | A dedicated Spark cluster owned by the quality service | Pushing down keeps 2.4 PB where it is and inherits catalogue grants rather than re-implementing them. Reservation, not isolation, is what protects Tier 1 latency under contention. | ADR-07 |
| Retroactive backfill | Separate warehouse on spot capacity | Databricks / AWS EC2 Spot | The same warehouse pool at lower priority | Backfill is the one workload here where interruption is genuinely acceptable: no consumer is waiting on a retroactive verdict. Physical separation guarantees re-judging history cannot delay judging the present. | ADR-10 |
| Governance, lineage and grants | Unity Catalog | Databricks | An independent access model inside the quality service | A verdict must not become a side channel, which means evaluation has to honour the same grants a human would face. Reading the catalogue's model is the only way to guarantee that without maintaining a second one that drifts. | ADR-14 |
| Verdict store | Append-only Delta on S3, partitioned by evaluation date | Databricks / AWS | Commit metadata and table properties inside each judged table | Verdicts must survive the deletion of what they describe, and scorecards query across thousands of datasets at once. Storing them inside the judged table gives inseparability and loses both. | ADR-01 |
| Current-state index | DynamoDB global table, replicated to a second region | AWS | Aurora read replicas, or the catalogue's table properties | Twelve thousand reads a second at peak against a far lower write rate, read directly by orchestrator tasks that must get an answer while the control plane is down. A key-value projection with an explicit staleness bound fits that shape; a relational store on the hot path does not. | ADR-04 |
| Rule catalogue and debt register | Aurora PostgreSQL, multi-AZ | AWS | DynamoDB for both | Both are small, relational and query-shaped — expiring overrides, debt by owner and age, assertions by dataset and tier. These are the two mutable stores in the design, and they want transactions and ad-hoc queries rather than scale. | ADR-16 |
| Rule authoring surface | Declarative DSL in Git, compiled to Spark SQL | Built | An existing expectations framework as the authoring API | The design depends on two things an opaque function call cannot provide: a cost estimate before publication and a fusion rewrite that turns forty assertions into one scan. The DSL's vocabulary stays deliberately close to the familiar frameworks so authors are not learning an alien model. | ADR-06 |
| Commit and verdict eventing | Amazon MSK | AWS | EventBridge, or polling the catalogue for version changes | One durable log carries inbound commit notices and the outbound verdict feed, with replay. Polling remains in the design regardless, as the reconciliation sweep — the event stream is an optimisation, never the guarantee. | ADR-09 |
| Control plane runtime | EKS across three AZs, leader-elected scheduler | AWS | Step Functions and Lambda for the scheduler | The scheduler holds priority lanes, per-domain quotas and coalescing state, which is long-lived work with in-memory structure rather than a state machine per item. Serverless would fit the sweeper, not the scheduler. | ADR-09 |
| Quarantine storage | Delta tables inheriting the source's classification and masking | Databricks | A shared quarantine bucket with its own policy | Quarantine is custody of someone else's regulated data, created by an automated process, in a table no domain team thinks of as theirs. Inheriting the source's policy is the only default that cannot silently downgrade governance. | ADR-15 |
| Override audit log | S3 Object Lock, compliance mode, 7 years | AWS | Append-only rows in Aurora alongside the debt register | This is the artefact an auditor asks for when a regulatory report turns out to have been built on suspect data. It has to be the one store in the design that the platform's own operators cannot rewrite. | ADR-14 |
| Detection-quality metrics | MTTD and false-positive rate per assertion, as platform metrics | Built | Alert counts and pass-rate dashboards | Alert count measures activity; MTTD and false-positive rate measure whether the service works and whether anyone will keep believing it. The second pair drives automatic demotion, so it has to be computed rather than displayed. | ADR-12 |
When the service records that a dataset passed or failed, what exactly is the subject of that claim — the run that produced the data, or the data itself?
When the service finds bad data, may it change anything — fix a value, drop a row, roll back a version — or may it only decline to let the data circulate?
What does the platform say about a dataset that has not been evaluated, could not be evaluated, or has no assertions at all?
Does the quality service itself promote table versions — owning the final step of every write so nothing can circulate unjudged — or does it publish a state that each pipeline and consumer is expected to consult?
Where does the answer to 'is this dataset safe to read right now' live, and is a consumer allowed to act on an answer that is slightly out of date?
If the platform learns about new table versions from an event stream, what happens to a version whose event was never delivered?
Is an assertion arbitrary code the platform runs and hopes finishes, or a declaration the platform must be able to cost and combine before accepting it?
Does evaluation run in the same engine the pipelines use, or in compute dedicated to quality work?
What stops quality coverage from becoming the line item that gets cut, and what happens when a dataset's assertions cannot finish inside their budget?
When a new or revised assertion is published, does the platform evaluate it against history — and if it fails eighteen of the last thirty versions, who decides whether that is a discovery or a bad rule?
When a corruption window is found in one dataset, how far does the resulting suspicion travel through the lineage graph?
What protects the platform from its own assertions — a rule that crashes the engine, a rule that fires every night on a legitimate business change, a rule whose author has moved teams?
Who may declare that a failing assertion stops a pipeline — and is that declaration made when the rule is written or when it fires?
How does an override, a suppression or a demoted rule end — and who is accountable for it while it lasts?
How does the platform make failures diagnosable without letting a verdict, its samples or its authorship reveal data the reader is not entitled to see?
When is diverting bad rows the right answer, and at what point does diverting them become a way of hiding a broken upstream?
Twelve terms this package uses precisely. Where a definition has a load-bearing alternative — a way the term is commonly used that would produce a different architecture — the alternative is named.
| Package | What it is | What it does here | Considered instead |
|---|---|---|---|
| Verdict | An immutable record that one assertion version, evaluated against one dataset version, produced a named outcome at a named time. | The atom of the whole design. Because its subject is a data version rather than a run, history can be re-judged and a corruption window can be computed as an interval. | A record that a pipeline run passed its checks — which cannot be re-derived, cannot be revised by a rule written later, and makes 'how far back does this go' a forensic exercise. |
| Dataset version | One immutable, addressable state of a table, as the table format produces on every commit. | The subject of every verdict and the unit blocking operates on: a blocked version is one that exists, is inspectable, and has not been promoted. | Treating the table as a single mutable thing, which makes blocking a rollback and destroys the evidence of what was rejected. |
| Promotion | The act of advancing a dataset's consumable pointer to a written version, performed by the platform's ingestion framework after consulting published state. | The circulation control. Keeping it outside the quality service is what keeps a quality outage from becoming a write outage. | Promotion performed by the quality service itself — airtight enforcement, at the cost of making the judge a hard dependency of every write. |
| Unknown | A state meaning the platform has not reached a verdict: not yet evaluated, unable to evaluate, or unable to read the target. | The state that prevents the silent-corruption case. On Tier 1 it withholds promotion exactly as a block does. | Folding unknown into pass, which is what most platforms do by accident and is how an unmonitored table looks healthy for a month. |
| Coverage | What assertions exist for a dataset, measured against its declared contract, and rendered wherever state is rendered. | The second dimension of every answer. 'Passing with three assertions' and 'passing with forty' are different claims and must not render identically. | Coverage as a separate dashboard, which means coverage nobody consults at the moment of the decision. |
| Assertion type | The class of claim a rule makes — schema, row-level, aggregate, referential, distributional, freshness, reconciliation. | An architectural property, not a taxonomy: row-attributability decides whether quarantine is available at all, and cost profile decides whether an assertion can be fused into a scan. | Treating all assertions as interchangeable predicates, which leads directly to trying to quarantine an aggregate failure. |
| Quarantine | Diversion of row-attributable violations to a separate table under the source's classification, with the remainder promoted. | The middle verdict action, bounded by a ceiling that escalates to block so it cannot disguise a systemic break as partial success. | Quarantine as a general safety valve for any failure, or as a lower-governance staging copy of regulated data. |
| Corruption window | The interval between the first failing and first passing version of a dataset under a retroactive verdict, together with everything derived from or reading the affected versions. | The deliverable of the silent-corruption case. The blast-radius list, not the alert, is what someone can act on. | An alert saying a table was wrong, with no statement of when it started or who consumed it. |
| Suspect | An inherited mark on a downstream dataset derived from a condemned window, cleared only by re-derivation or explicit dismissal. | Propagates one hop as state while the full transitive set is published as a report, so the mark stays meaningful. | Transitive propagation as state, which marks half the warehouse from one defect and teaches consumers to ignore the mark. |
| Probation | A period — thirty days by default — during which a newly published assertion runs as advise only, whatever its declared action. | The mechanism that lets a rule's false-positive rate be measured before it can stop anything. | Enforcing from publication, which is one mistyped predicate away from a retroactive outage. |
| False-positive rate | The share of an assertion's failing verdicts over a trailing thirty days that were overridden, dismissed, or followed by no upstream change. | The number that decides whether the platform is enforced or decorative. Above 5%, the assertion is demoted automatically. | Alert volume, which measures activity rather than credibility and rises hardest when a platform is working worst. |
| Quality debt | The enumerable set of active overrides, suppressions, probations, unfixed quarantine batches, demoted assertions and zero-coverage datasets, each with an owner and an age. | What makes exceptions visible and finite. Every escape hatch creates an entry in the same transaction that grants it. | Exceptions as configuration flags, which are invisible after the week they were created and cannot be enumerated at all. |
svg/<view>.svg or drawio/<view>.drawio in draw.io Desktop or at app.diagrams.net to edit. The SVG carries the diagram inside it, so it is both the picture and the source. This folder is self-contained — copy it whole and every link still resolves.