Abstraction Coverage Gap
also called Compatibility Residue, Unsupported Shape
The share of real usage a compatibility layer does not actually handle - small by volume, invisible in testing, and the place where every incident after the migration comes from.
A team puts a proxy in front of a sharded database so application code keeps talking to what looks like one database. The proxy understands the query shapes the team wrote down. It does not understand the shapes an ORM emits under conditions nobody enumerated, and the first production incident after cutover is a query form that existed for two years inside a quarterly report nobody reviewed.
The coverage gap is the difference between the surface an abstraction claims and the surface it actually implements. It is not a bug; it is a measurable quantity, and the error is treating it as zero because the common path works.
Why it matters
A compatibility layer is a promise that callers need not know what is behind it. The promise is binary to the caller and probabilistic to the implementer, and that mismatch is where the damage is: unsupported usage does not degrade gracefully, it fails or silently misbehaves, and the caller had no way to anticipate it.
The distribution makes it worse. Usage of any sizeable interface has a long tail, so the gap is small by request volume and large by number of distinct shapes. Testing proportional to traffic tests the part that works. A gap of 0.1% of requests can still be 15% of the distinct query forms in the estate, each belonging to a feature someone depends on.
This is distinct from a leaky abstraction. A leak is an implementation detail becoming visible through the interface. A coverage gap is usage the interface appears to accept and does not correctly serve.
Implementation patterns
- Enumerate the real surface before cutover, from traffic rather than from code. Capture distinct normalised shapes over a full business cycle — a month, including month-end and quarterly jobs — and count them. That count is the test matrix.
- Fail closed on the unknown. An unrecognised shape should be rejected with a clear error, not guessed at. A proxy that routes a query it did not fully parse is the worst available behaviour, because it returns plausible wrong answers.
- Shadow the gap before you depend on it. Run real traffic through the new layer without using its results, compare against the old path, and let the comparison find what the inventory missed.
- Publish the supported surface as a contract, with the unsupported constructs named: no cross-shard joins, no cross-shard unique constraints, no multi-shard transactions. Callers can only avoid what they have been told about.
- Gate new usage in the pipeline. A check rejecting a query without a routing key, overridable only by an explicit reviewed annotation, keeps the gap from growing after launch.
Industry example
Figma's databases team described (engineering blog, 2024) sharding Postgres behind a query proxy written in Go that parses statements into a syntax tree to decide which shard each belongs to, after first sharding logically so routing could be proven correct while all the data still sat on one machine. That sequence is a direct response to the coverage problem: prove the routing against real traffic before the data moves, because that is the only point at which a mis-routed query is recoverable. The published constraints name the residue plainly — foreign keys and unique indexes cannot span shards, and cross-shard transactions can fail partway.
Vitess, built at YouTube to shard MySQL behind a proxy and later a graduated CNCF project, has the same architecture and the same long-running concern: the supported SQL surface is documented explicitly, and unsupported constructs are rejected rather than approximated.
Failure scenarios
- The quarterly job. A report that runs 4 times a year uses a construct the proxy never saw, and it breaks with no recent change to blame.
- Silent mis-routing. A query without a routing key is answered from one shard and returns a partial result that looks like a legitimate empty set.
- The gap growing after launch, because nothing prevents new unsupported usage.
- A guarantee assumed from the old world. Code relying on a unique index that is now unique per shard compiles, reviews clean, and allows duplicates.
- Tail latency surprise. A shape handled by fanning out to every shard has a p99 near the slowest shard's p99, so a supported query is still a problem.
Trade-offs
| Choose | Gains | Pays |
|---|---|---|
| Compatibility layer | Migration invisible to callers and reversible below the layer | A surface you must cover; a tier on the critical path; a long tail of shapes |
| Make callers aware | No hidden gap; guarantees are explicit | Every call site changes at once, irreversibly, with no incremental path |
The honest comparison is about reversibility. The layer can be pointed back at the old topology in a configuration change; converted call sites cannot be un-converted. You are buying the option to retreat, and paying for it in coverage work.
When not to use it
When the surface is small enough to convert directly. A few dozen query sites, or one team's code, is cheaper to make topology-aware than to hide behind a parser you then own forever. The crossover is roughly where the number of call sites exceeds what one team can change in a quarter.
Also skip it when the abstraction would hide a guarantee change callers must know about. If cross-entity atomicity is genuinely going away and application logic depends on it, hiding that behind an unchanged interface converts a compile error into a data-integrity incident. There the right move is to break the interface deliberately so every caller is forced to look.
Interview question
Q: You are two weeks from cutting over to a proxy that hides a new sharded topology. Nothing is failing in staging. Tell me what you would do with those two weeks, and what evidence would make you delay.
What a strong answer covers: inventorying distinct normalised query shapes from a full month of production traffic rather than from the test suite, and comparing that count against what the proxy covers · running shadow traffic and diffing results, because staging traffic is not the long tail · checking specifically for month-end, quarterly and admin paths that have not run recently · deciding the fail-closed behaviour for unknown shapes and verifying it · the evidence that would delay: unexplained shapes remaining, any silent mis-route in shadow mode, or reliance on a cross-shard guarantee found in code · and the control that keeps the gap from growing after launch, which is a pipeline check on queries lacking a routing key.
Quick check
Quiz: Why does a compatibility layer's residual gap cause incidents out of proportion to its size? — Because usage has a long tail: a gap of 0.1% of requests can be a large share of distinct shapes, each belonging to a real feature, and unsupported usage fails rather than degrading.
Flashcard: What should a routing proxy do with a query shape it cannot fully parse? — Reject it with a clear error. Guessing returns plausible wrong answers, which is strictly worse than an outage because nobody notices.