Adding a ticket state breaks the build at every match site — how do you sequence that change across a large codebase?
answer
- the errors are the work list
- declare the variant before producing it
- catch-alls are the sites it cannot list
- decoders of stored values need a case
- readers deployed before the first producer
basics
~20 sAdd the variant first with nothing producing it, then work the compiler's error list site by site. That list is complete only for matches that never had a catch-all, so audit existing catch-alls and every decoder of stored values separately.
solid answer
~50 sThe build errors are the migration plan, and the order that makes them cheap is: declare the variant, let nothing construct it yet, fix every site the compiler names, and only then start producing it. Because no ticket can be in the new state while you work, each error is a decision made calmly rather than an incident. What the list cannot include is the set of sites that were already total without listing the variant — matches ending in a catch-all, chains of equality tests on a state tag, tables keyed by state. None of those break, so they need a separate review pass. Outside the program the guarantee stops entirely: stored records, messages in flight and any component built before the variant existed will meet a value it has no case for, which is why readers are updated and deployed before the first producer is switched on.
go deeper
Recognise that the compile errors after adding a variant are a to-do list, and that the fix for each one is a decision about the new state rather than a default value.
Explain why the list is complete for matches that list their variants and empty for those ending in a catch-all, and why that makes catch-alls the sites to audit by hand.
Show the sequencing: declare the variant with no producer, work the errors under no traffic, audit the silent sites, then deploy readers before any writer.
Own the boundary question — how far the closed type is allowed to travel, and whether a variant addition is a single build or a cross-component release that needs a compatibility window.
## The error list is the migration plan The reason a closed, exhaustively checked type is worth its cost shows up exactly here. In most codebases, "where does this change need attention?" is answered by searching, by memory and by whoever has been here longest. With this mechanism the answer is computed: add the variant, compile, and the build hands you a list of sites that must decide what an escalated ticket means. Within the code being compiled, and excluding the gaps named below, that list is **complete** — it includes the paths no test covers and the modules nobody remembers. The skill being probed is not enjoying the list. It is knowing the order to work it in, and knowing what it leaves out. ## Sequence it so the errors arrive under no traffic 1. **Declare the variant, construct it nowhere.** The type grows a fifth state, but no code path can produce one. The build breaks immediately and broadly, with zero tickets in that state anywhere. 2. **Work the compiler's list, deciding each site.** For every error, answer the actual question — where does an escalated ticket route, does it count as open work, does the response-time clock run? Each site gets a decision, not a copied default. 3. **Resist the shortcut.** Pasting a catch-all at each site to get to green converts a one-off cost into a permanent loss of the guarantee, and the next variant will be silently absorbed at every site you touched. 4. **Audit the sites the compiler could not name** (below) before declaring the migration done. 5. **Then turn on production of the variant**, readers first, producers last. The ordering principle is that a build error under no traffic is the cheapest possible form of this change, and every step that delays production of the variant keeps the change in that cheap regime. ## What the list cannot contain The compiler reports **gaps**, and a site with no gap is invisible to it regardless of whether it is correct: - Matches that already ended in a **catch-all** — still total, still silent, and now quietly applying an old branch to the new state. - Dispatch built from a **chain of equality tests** on a state tag, whose final fallback absorbs anything unrecognised. - **Tables or maps keyed by state**, which are data rather than code; a missing key surfaces, if at all, wherever the lookup happens. - Code matching a value that was **widened** to a more general type before the match, where the variant set being enumerated is not the one you changed. - Anything **outside this compilation** — a separate component, or persisted data. | Where the new variant can hide | Does the build flag it? | How it shows up instead | |---|---|---| | Match listing every variant | Yes | A compile error naming the site | | Match ending in a catch-all | No | The new state runs an old branch | | Chain of equality tests on a tag | No | The final fallback absorbs it | | Table keyed by state | No | A lookup miss or a default, at run time | | A component built before the variant | No | Its decoder meets a value it cannot name | A search for all-matching cases over that type is the cheap first pass on rows two and three, and it is worth doing *before* the variant is added rather than during. ## Outside the program The proof covers values that are already instances of the closed type. It says nothing about the text or bytes those values were decoded from. So the boundary needs its own plan: - Every **decoder** of a stored or received state gains a case for the new variant, and keeps an explicit error path for input it does not recognise. - A component **built before the variant existed** now reads records containing it. Its decoder meets a value it has no case for and does whatever it does with unrecognised input: fail loudly if it has an error path, produce a wrong default if it does not. Nothing inside the closed type can protect it. - Therefore **update and deploy every reader first**, and only then allow anything to write the new state. Reversing that order is the most common way a well-checked change still causes an incident. ## Deciding, not defaulting The last piece is cultural rather than mechanical. A large error list is uncomfortable, and the temptation to make it disappear uniformly is strong. But the list is precisely the set of places where somebody once wrote down what each state means, which makes them the places that must be told about a new one. Working them individually is the cost of the guarantee; paying it once per variant is the deal the closed type offered in the first place, and it is a deal that only pays out while the catch-alls stay out.
- Which sites does the compiler's list miss, and how do you find them?Every match that already ended in a catch-all, every dispatch built from conditionals on a state tag, and every table keyed by the state. None of them break, so none appear. Searching for all-matching cases over that type is the cheap first pass, and replacing the ones that are omissions rather than policy is what makes the next variant addition self-reporting.
- What happens to a component that was built before the variant existed and now reads data containing it?Its decoder meets a value it has no case for, so the behaviour is whatever that decoder does with unrecognised input: a clean failure if it has an explicit error path, a wrong default if it does not. Nothing inside the closed type protects it, which is why every reader is updated and deployed before anything starts producing the new state.
- Why declare the variant before any code can produce it, rather than doing both together?Because it separates the decisions from the traffic. With nothing constructing the new state, the build errors are a list of questions to answer at leisure, and a half-finished migration is harmless. Once something can produce the state, every unfixed site is a live wrong behaviour, and the pressure to paste a default rises exactly when it is most expensive.
saying these in an interview costs you the question
- Pastes a catch-all at each site to get the build green
- Assumes the compiler's list covers stored and in-flight data
- Starts producing the new state before every reader handles it
- Thinks matches that already had a catch-all will also fail
- Treats a green build as proof that other components cope