Walk through Michael Feathers' Legacy Code Change Algorithm and explain how effect sketching and pinch points help you decide where to put tests.
answer
- Change points -> test points -> break deps -> tests -> change
- Sketch how effects escape: return, params, fields, globals, I/O
- Pinch point = narrow funnel, many effects, one interface
- Test cheap at the funnel, then push tests down
- Only step 3 runs without a safety net
basics
~20 sThe algorithm is: identify change points, find test points, break dependencies, write tests, then make your change and refactor. To find test points you sketch which values a change can affect, then look for a narrow place all those effects flow through — a pinch point — and test there.
solid answer
~60 sFeathers' Legacy Code Change Algorithm has five steps: (1) identify change points — exactly where the edit must go; (2) find test points — places where you can detect the effects of that edit; (3) break dependencies so those points are reachable in a test harness; (4) write characterization tests to pin current behaviour; (5) make the change and refactor. Finding test points uses effect analysis: sketch the effect propagation — from the values you will modify, follow every way an effect escapes (return values, parameters mutated, fields, globals/statics, I/O, events) and then who reads those. That effect sketch defines what your tests must be able to observe. A pinch point is a narrow interface through which a large part of that effect set flows — typically one public method fronting many internal calls. Testing at the pinch point buys wide behavioural coverage with few, cheap tests, so it is the natural first place to characterize. Pinch points are also refactoring signals: they mark a natural boundary for future extraction, and tests written there should later be pushed down as the design improves.
go deeper
Recite the five steps in order and say a pinch point is a narrow place where you can test many behaviours at once.
Explain effect sketching concretely — list the ways an effect escapes a method — and why test points are often not the change points themselves.
Discuss selecting pinch points from the effect graph, their coverage-per-effort economics, their weakness in failure localisation, and the plan to push tests down after refactoring.
Use it as a blast-radius and scoping tool: bound the change, argue formally about what is unaffected, treat an unbounded fan-out as evidence to break globals or re-scope, and connect pinch points to future module boundaries.
## The algorithm Feathers gives a repeatable procedure for changing untested code: 1. **Identify change points.** Where exactly does the edit go? Not "in the billing module" but the specific methods and lines. Constraining this early prevents scope creep and bounds how much you must test. 2. **Find test points.** Where can you *observe* the effects of the change? Sometimes the change point itself; often further out, at a place where you can construct objects and read results. 3. **Break dependencies.** Do the minimum needed to instantiate and exercise the code at the test points — using seams (see object/link/preprocessing seams), Extract Interface, Extract and Override Factory Method, Parameterize Constructor, and so on. This step is done *without* tests, so it must use signature-preserving, mechanically safe moves. 4. **Write tests.** Characterization tests: assert what the code does today, driven by branch coverage and boundary inputs. 5. **Make changes and refactor.** Now under a net, edit freely; keep behaviour-preserving refactorings and behaviour changes in separate steps. Note the ordering discipline: the only stretch performed without a safety net is step 3, and it is deliberately restricted to low-risk mechanical edits. ## Effect sketching (effect analysis / effect reasoning) To do step 2 you need to know **what could possibly change** when you edit the change point. Feathers' informal tool is an **effect sketch**: a small hand-drawn graph. How to build one: 1. Start with the values your edit writes to or computes — a returned value, a field, an out-parameter. 2. For each, draw an arrow to anything that can observe it. Ways an effect escapes a method: - the **return value**; - **mutation of a parameter** (mutable object passed in); - **instance/class fields** changed; - **globals, statics, singletons** changed; - **I/O**: file writes, database writes, network calls, messages published, logs someone asserts on; - **exceptions thrown** (a control-flow effect); - in concurrent code, values visible to other threads. 3. Continue transitively: who reads that field? what do they in turn affect? 4. Stop when arrows leave the region you can afford to test, or reach a point you can assert on. The sketch answers two questions: *what must my tests be able to see* (the sinks), and *what is definitely unaffected* (everything not reachable), which lets you argue safely that a whole subsystem needs no tests for this change. A useful complementary heuristic: for a given method, **what could break it** — the inputs and dependencies feeding it — which is the sketch traversed backwards. ## Pinch points A **pinch point** is a narrow funnel in the effect graph: a small interface (often a single public method or a small class facade) through which the effects of many internal changes flow before reaching the outside world. Why they matter: - **Economy.** One test at the pinch point exercises many collaborating internals. When you cannot construct twelve internal classes cheaply, one test at the funnel may cover them all. - **Stability.** Tests written at a pinch point are less coupled to internal structure, so heavy refactoring behind it does not force test rewrites — exactly what you want while restructuring. - **Design signal.** A pinch point usually indicates a genuine responsibility boundary. It is where you would eventually extract a class or module; if there is no pinch point, that itself is a warning that responsibilities are tangled and effects leak everywhere. How to find one: build the effect sketch and look for a place where many arrows converge before crossing outward, or simply ask "which single public entry point do all these paths go through?" ## The trade-off: pinch-point tests are not the destination Tests at a pinch point are coarse — closer to integration tests of a cluster of classes: - Failure localisation is poor: a red test says "something in this cluster changed", not which class. - They can be slow if the cluster touches infrastructure. - They quietly enable a cluster to keep growing behind the interface. Feathers' guidance is to use them as scaffolding: characterize at the pinch point, refactor behind it, and as classes become independently testable, **push tests down** to those classes and retire or thin out the coarse ones. Otherwise the pinch point ossifies into a mini test-through-the-UI situation. A related concept is **interception points** — any point where you can detect an effect; a pinch point is simply the *best* interception point, the one covering most effects for least cost. ## Practical notes and edge cases - **Globals and statics wreck effect sketches** by connecting everything to everything; if the sketch fans out uncontrollably, that is a signal to break the global dependency first. - **Effects through concurrency** are the hardest to sketch and often cannot be characterized reliably; prefer to make the change point single-threaded or drive it deterministically. - **Hidden effects via logging or metrics** are real if someone (alerting, billing, downstream ETL) depends on them; consult before treating them as noise. - **Scope control.** If the effect sketch cannot be bounded in a reasonable time, that is evidence to reduce the change's scope or to sprout the new behaviour instead of editing in place.
- You find no pinch point — effects escape through six different globals and three I/O sinks. What does that tell you and what do you do?It says responsibilities are tangled and the change's blast radius is genuinely wide. Options: break the worst global dependency first (a signature-preserving seam), shrink the change's scope, or sprout the new behaviour into a fresh tested unit so you never edit inside the tangle. Forcing coarse end-to-end tests over the whole fan-out is slow and gives poor localisation.
- Why should pinch-point tests eventually be replaced rather than kept forever?Because they are coarse: they localise failures poorly, tend to be slower, and let the cluster behind the interface keep growing untested internally. Once refactoring makes the internal classes independently constructible, push tests down to them and keep only a thin set at the boundary.
- Which step of the algorithm is performed without any test safety net, and how do you keep it safe?Breaking dependencies (step 3). Keep it safe by restricting yourself to mechanically verifiable moves: IDE-performed automated refactorings, preserving method signatures, moving code by cut-and-paste rather than retyping, one goal per edit, and compiling/running frequently; leaning on the compiler where the language allows.
An effect sketch is the plumbing diagram of a house before you cut a pipe: it shows which taps could run dry. A pinch point is the single junction that feeds most of them — put your pressure gauge there and one reading tells you about the whole wing.
saying these in an interview costs you the question
- Reordering the algorithm to 'refactor first, then add tests'
- Claiming every change needs full-system regression tests instead of bounded effect analysis
- Confusing a pinch point with a mocking boundary or a public API by definition
- Ignoring effects that escape via static/global state, logs, or published events
- Keeping only coarse pinch-point tests permanently and calling the code well-tested
- Sketching effects forward only and never asking what feeds the method