Designing under pressure · chapter 30 of 33
Saying what you will not build
Chapter 4.2 · Part 4, Designing under pressure · about 25 minutes
What you need before this chapter: chapter 4.1, Reading a vague prompt without panicking. You already have three of the four lists an extraction produces. This chapter is about the fourth.
When you finish this chapter you will be able to:
- Tell a real scope refusal apart from a list of things nobody would have asked for anyway
- Apply a three-part test to your own out-of-scope list: tempting, expensive, and already named
- Explain why a refusal that gets overturned by a curveball later was still worth writing down
- Estimate, from a scope decision alone, roughly how expensive granting it later will turn out to be
1. A list that refuses eight real things
corpus/rate-limiter/problem.json carries an out_of_scope list with eight entries, and it is the strongest example of this list anywhere in the corpus. Here is the whole thing:
- a shared store, a cluster, or copying data between machines of any kind
- keeping data around after a restart
- dropping a key that has gone quiet for a while
- charging a request anything other than one flat unit
- making a caller wait its turn instead of simply saying no
- rules that differ by endpoint, and checking who the caller is
- counting or reporting how often a caller was refused
- any background thread, timer, or scheduled refill
Read that list next to what a rate limiter is actually for. Running across several machines at once is not a fringe idea. It is usually the first thing anyone suggests when "rate limiter" comes up in conversation, because most real ones do run that way. Reporting on refusals is not obscure either. An operator who just got paged wants to know how often the limiter said no, and to which caller. Every one of these eight is a feature a reasonable engineer would ask for, and the list says no to all eight, in writing, before anyone gets the chance to ask.
2. The weak version, and why it fails
Picture a different out-of-scope list for the same problem: "no support for SOAP," "no support for XML config files," "no plugin marketplace." Nothing on that list was ever going to be asked for, so refusing it costs nothing and defends nothing. A list like that is not scope negotiation. It reads like decision-making, but no decision was actually made, because nothing tempting was ever on the table.
The rate limiter's real list passes a test that one fails. Every entry names something a competent engineer would reasonably want, badly enough that leaving it out has to be a decision rather than an oversight. must_haves.requirements says what you are building. out_of_scope says what you looked at and chose not to build, and a refusal only means something when the thing refused was worth wanting.
3. The list gets tested, on purpose
Scope negotiation stops being a writing exercise the moment someone asks for the thing you refused. corpus/rate-limiter/curveballs/03-denials-are-counted does exactly that: mid-round, the requirement becomes "count the denials per client key and let an operator read the number." Read the eighth entry on the list again. Metrics and reporting on denials were ruled out from the start, in writing, and this curveball asks for precisely that back.
A candidate who wrote that refusal down is not caught off guard. The feature was always a live possibility, named and set aside, and now it is simply being granted. A candidate who never wrote it down never considered reporting worth refusing in the first place. They are now retrofitting it into code that was never designed to expose it, mid-round, under the same clock. Both candidates end up building the same thing. Only one of them saw it coming.
4. The threshold, stated as a test with three parts
A real out_of_scope entry clears three bars at once.
Tempting. A competent engineer, looking at the same prompt, would plausibly want this. If nobody would ever ask for it, refusing it defends nothing.
Expensive. Granting it would reshape more than one line. If a feature is one call to something you already built, refusing it is not a negotiation, because there was never a cost to weigh.
Named. It traces back to the prompt or to one of your own clarifying questions, not to something invented afterward to pad the list out. corpus/rate-limiter's twelve good_questions are exactly where several of its eight refusals come from. Ask whether the caller needs reporting on denials, and get told it is out of scope for now. That single exchange produces a clarifying question and an out_of_scope entry together.
5. Being caught off guard is not the same as being principled
Say the interviewer asks for something that never made your list at all. Refusing it on the spot, because it feels outside what you have built so far, is not the same skill this chapter teaches. It means the request caught you by surprise, and a sharper-sounding refusal next time does not fix that. What fixes it is having seen the request coming, which is a matter of having read enough real prompts to recognise the shape of the ones that turn up. corpus/rate-limiter's own list maps three things that tend to get asked about a system that says no to callers. Where does its state live, how long does that state survive, and who gets told about a denial?
Going deeper
An out_of_scope decision is not only a sentence you say out loud. It is a prediction about cost, and this corpus checks that prediction against a real number. tools/rebudget.mjs computes every curveball's line budget from one formula: budget = max(3, d + min(ceil(d / 2), 6)), where d is reference_diff, the number of lines the reference solution actually changed to absorb the change.
Look at what happened to the three curveballs against this exact out_of_scope list. corpus/rate-limiter/curveballs/01-sliding-window-log/budget.json records reference_diff: 1 for a third counting algorithm, because the seam for a new algorithm already existed and adding one meant a single line of registration. 02-global-backstop-cap/budget.json records reference_diff: 0, because a second scope implementation slotted into a seam that was already there. 03-denials-are-counted is the one curveball that grants back a refused item, and it records reference_diff: 19. Its own note says why: the base contract lists metrics as out of scope in writing, so there was no seam waiting for a denial counter. Building one against a requirement ruled out on purpose would have been effort spent on a guess.
That is the honest trade this whole chapter is about, made numeric. A seam you build for a feature you expect costs you nothing later and something now, in code you carry the whole round. A feature you correctly scoped out costs you nothing now and a real, measured price later, if and when it comes back. Nineteen lines, in one file, is what that price turned out to be here. The out_of_scope list does not make the cost disappear. It tells you, in advance, which costs you are choosing to defer and which ones you are choosing to build for now. D4 of STANDARD v1.0, extensibility, is what eventually checks whether that bet paid off.
Next: chapter 4.3, Drawing a class diagram fast — where the classes your two lists named get a shared picture, before either of you writes a line of code.
← 4.1 Reading a vague prompt without panicking · All chapters · 4.3 Drawing a class diagram fast →