Wait for the Third Case
The instinct that gets drilled into engineers early is “don’t repeat yourself,” and it’s good advice right up until it isn’t. The failure mode it causes is abstracting too soon: you write something once, you write something similar a second time, and you immediately pull the shared part into a function or a class to avoid the duplication. It feels responsible. But you’ve just made a guess — a guess about which parts of those two cases are the same in essence and which are the same by coincidence — and you made it at the exact moment you had the least evidence to guess well.
The trouble is that an abstraction encodes a theory of what varies and what doesn’t. When you extract a shared helper from two examples, you’re asserting that the axis of variation between them is the only axis that will ever matter. Then the third case arrives, and it’s similar but different along a new axis you didn’t anticipate. Now you have a choice, and both options are bad: bolt a parameter and a conditional onto the abstraction to accommodate the case it wasn’t shaped for, or unwind the whole thing. Do this a few times and you get the familiar artifact — a “shared” function with seven flags, where every caller lights up a different subset, and no one can change it without checking all of them.
This is why the wrong abstraction costs more than the duplication it replaced. Duplicated code is dumb but honest: three copies of something are three independent things you can change independently, and the cost of the duplication is visible and bounded. A wrong abstraction is coupling — it ties together things that looked alike but need to evolve apart, and it does so invisibly, so that a change made for one caller silently lands on all the others. Untangling it is worse than writing it, because by the time you realize it’s wrong, several callers depend on its exact current shape, including the parts that were mistakes.
The practical discipline is to wait — the old “rule of three.” Let yourself write the thing twice, duplication and all, and hold off on extracting until a third case shows up. The third instance is what turns a guess into data: with three examples you can actually see which parts are stable and which move, and the abstraction you draw is shaped by evidence rather than by a premature theory. It costs a little to carry the duplication in the meantime, but that cost is small and knowable, where the cost of the wrong extraction is large and hidden. Waiting is how you buy information before committing.
None of this is a license to never abstract, and duplication past a certain point is its own kind of debt — the same bug fixed in four places, the fifth one missed. The point is that the timing is a real decision with real costs on both sides, not a reflex to fire the instant you see two similar lines. Abstract too early and you couple things that should have stayed apart; abstract too late and you drown in copies. The craft is in noticing which risk you’re actually facing right now, and most of the time, on the second occurrence, the honest answer is that you don’t have enough cases yet to know — so wait for the third.