advanced 3 min answer

You own a design system used by 40 applications. Last quarter two releases labelled "no breaking changes" broke consuming layouts. An interviewer asks how you would make "this release breaks nothing" a claim CI can check rather than one a reviewer asserts. How do you answer?

design systemcontract testingvisual regressionversioningrelease engineering
Show the full answer Hide the answer

What the interviewer is testing

Whether you know that the contract consumers depend on is much larger than the contract you declared. Props and types are the part your version policy governs. What broke those 40 applications is the rest: DOM structure, class names, computed spacing, font metrics, focus order, CSS specificity and the shape of your package exports.

A weak answer promises review discipline. A strong one converts the undeclared surface into assertions.

The clarifying questions that change the answer

  • How do consumers upgrade - pinned versions or one version at HEAD? At HEAD you find breakage in your own CI; pinned, you find it in their incident channel months later.
  • What did the two breaking releases actually change? Spacing and a wrapper element, almost always, which tells you which assertions to build first.
  • Can I require anything of consumers? That decides whether a lint rule against reaching into internals is enforceable.
  • How many themes and viewports are supported? It sets the snapshot matrix and therefore its cost.

A strong answer's arc

  1. Diff the declared surface mechanically. Generate the public type surface per release candidate and fail the build on any removal or narrowing. Cheap and exact - and it catches the smallest share of real breakage.
  2. Pin the computed surface. Render every component against its variant matrix and snapshot what consumers build on: outer box size, margin, line box height, DOM depth, role and accessible name. A spacing change is a breaking change even when no prop changed, and this is the assertion that says so.
  3. Pixel-diff a fixed gallery at two viewports and two themes, gated on a threshold rather than exact equality, with baseline updates requiring a stated reason. Sixty components x six variants x two themes x two viewports is about 1,400 renders - 8 to 12 CI minutes, which is affordable; baseline churn is the real cost, because one legitimate token change invalidates most of the matrix at once.
  4. Build five consumers against the release candidate, chosen for surface coverage rather than importance. Forty is unaffordable; five catches the integration failures no in-repo test sees.
  5. Budget the package shape - export map, side-effect flags, per-entry byte delta - so a packaging change cannot quietly land in 40 route bundles.

What CI cannot check, and what to do instead

It cannot check intent, content-dependent layout (a consumer's 40-character label in a space you designed for 20), or whatever consumers did with !important and descendant selectors reaching into your internals. Publish the stated contract, then make the unstated parts hard to depend on: a cascade layer for your styles, hashed internal class names, and a lint rule in consumers against selectors targeting internals.

Common weak answers

  • "More thorough code review." The two releases were reviewed. Humans do not diff computed spacing across 1,400 renders.
  • "Semantic versioning, properly applied." A policy, not a detector. Semver names a change after you know it broke something.
  • "Write more unit tests." They assert your intent, and the breakage is in what consumers inferred.

What a strong answer adds

The cost and the limit. The gate runs in about 12 minutes of CI, and its real cost is baseline churn: one legitimate token change invalidates most of the matrix, so unless bulk approval carries a stated reason the team approves without looking and the check becomes decoration.

Prefer this machinery at roughly eight or more independently released consumers; below that it is waste. At two or three in one repository, importing from source at HEAD plus one shared test run is strictly better - breakage surfaces in the pull request that caused it. Semantic versioning, specified in 2013, tells you what to call a change; it never tells you the change happened, which is the gap these assertions fill.