Software Design Reference Master Designer · Complexity Trade-Offs

Lesson 0028 · Master Designer · Module 1

Abstraction vs Duplication

DRY (lesson 0002) says don't repeat knowledge. The Master's version: duplication is often the cheaper rent — the wrong abstraction is a mortgage you pay forever.

Mission tie-in: the second Complexity Trade-Off — where the principles of Phase I become something you balance instead of obey.

Knowledge: two kinds of rent

Duplication costs you on every change — edit N copies. Abstraction costs you on every read — everyone must understand the shape, and if the shape is wrong, every consumer pays. The trade is between these two rents.

Why the wrong abstraction is so expensive:

# the abstraction that guessed wrong
class FeeCalculator:                 # "all fees are one thing" — wrong
    RATE = Decimal("0.01")

    @classmethod
    def fee(cls, amount: Money, kind: str) -> Money:
        if kind == "bank":
            return amount * cls.RATE
        if kind == "wire":
            return amount * cls.RATE
        raise ValueError(kind)

# now wire fees change (clearing house), and the abstraction must be bent:
def fee(self, amount, kind):
    if kind == "wire":
        return amount * Decimal("0.02")      # a branch, because one RATE was wrong

The two fees were never one knowledge (lesson 0002's false DRY) — the abstraction forced them into one shape, and every future divergence has to fight the shape. The duplicated version would have been two obvious functions, each change contained.

Sandi Metz's rule: duplication is far cheaper than the wrong abstraction. And the corollary — the abstraction isn't wrong if it was earned: three occurrences with the same change pattern, then extract. The rule of three is a rent calculation: by the third occurrence, the duplicated copies' change-rent exceeds the abstraction's read-rent.

Pricing the two rents

Three copies of a validation routine, changed four times a year. Duplication rent: 3 edits per change × 4 changes = 12 edits a year, plus the cost of the copy someone forgets (roughly once a year, found in production).

Abstraction rent: 1 edit per change — if the three always change together. If they change independently, the abstraction converts three independent edits into one negotiated edit, and each divergence adds a branch that all three consumers must then read past.

So the deciding number is not "how many copies" but how often the copies change together. Three copies that always move as one are an abstraction waiting to be extracted. Three copies that move on separate schedules are three facts, and merging them buys 12 cheap edits at the price of every future divergence.

The asymmetry: you can always add the abstraction later when the third copy appears. You can rarely remove a wrong abstraction — every consumer's code now depends on its shape.
Field notes · what it looks like in real code
You see thisWhat it costsThe move
a parameter named kind / type / modeThe abstraction is asking callers which one it really is.Two functions with two honest names.
if isinstance(self, …) inside a base classThe shape was wrong and is now being bent from inside.Split the hierarchy along the real difference.
a helper with six optional parametersOne function serving six callers, none of them well.Inline it back; re-extract what actually repeats.
three copies that changed together, twiceThe shared shape is evidence now, not a guess.Extract it — this is what earned looks like.

Skill: whose rent is cheaper?

Two fees look identical today but are set by different contracts. Extract?

The rule of three says extract when:

The worst position to be in is:

Practice on your own code

Find an abstraction in your codebase that was extracted before its time (a helper with a kind/type flag, a base class with branchy subclasses). Ask: was it earned by three occurrences with one change pattern? If not, sketch the duplication it should have been.

Reveal: a wrong-abstraction rescue

A Notifier base class with send() overridden by Email and SMS — extracted at two occurrences. When a third channel arrived with a different retry semantics, the base class's shape had to bend. Rescue: delete the base, keep two functions (lesson 0016's callables), let each channel evolve alone until a real shared step appears three times.

Your win

You can compute the two rents — change-rent of duplication vs read-rent of abstraction — and you know the asymmetry that makes wrong abstractions the expensive mistake.

Read and watch deeper

Bring a flag-driven helper to your agent-teacher and decide whether it's an earned abstraction.