You’ve been told for years that duplication is bad. Don’t repeat yourself. Extract the method. Make it reusable. You know the script. But when you’re working in a real system the rules never feel as clean as the slogans.
So here’s the real question: what are you actually trying to manage when you remove duplication?
Why This Matters
Duplication isn’t dangerous because it offends your sensibilities. It’s dangerous because it can hide multiple competing truths. Change one, and the others must change too. Miss a spot, ship a bug.
But the uncomfortable bit is that sometimes duplication is fine. Sometimes the cure is worse than the disease.
The Classic Mindsets Aren’t Enough
DRY (Don't Repeat Yourself)
Simple idea. Don’t repeat yourself. Avoid rewriting the same thing in multiple places. It reduces the chances you’ll forget to update something later.
The catch: DRY often pushes you to unify things that only look similar today.
YAGNI (You Ain't Gonna Need It)
The opposite instinct. Don’t build abstractions for problems you don’t yet have. It stops you engineering cathedrals out of two lines of code.
The catch: it can also leave real duplication hanging around until it hurts.
WET (Write Everything Twice)
A compromise. Write everything twice and refactor on the third. Sensible on paper. Still not the whole story.
Because the real distinction isn’t about repetition. It’s about truth.
A Walk Through “Looks The Same” Duplication
This progression shows how easy it is to misread two things as “the same” and end up forcing the wrong abstraction.
You start with something simple.
export class Person {
constructor(private firstName: string, private lastName: string, private nickName: string) {}
buildName(): string {
return `${this.firstName} "${this.nickName}" ${this.lastName}`;
}
}
const person = new Person('Dwayne', 'Johnson', 'The Rock');
console.log(person.buildName()); // prints "Dwayne \"The Rock\" Johnson"
Then you add another method.
getFunEmailSignature(): string {
return `
Catch you on the flip side, buddy!
${this.firstName} "${this.nickName}" ${this.lastName}
`;
}
Your DRY-sense tingles. You refactor.
getFunEmailSignature(): string {
return `
Catch you on the flip side, buddy!
${this.buildName()}
`;
}
Still tidy.
Enter Doctor. Same Shape, So Surely The Same?
class Doctor {
constructor(private firstName: string, private lastName: string, private nickName: string) {}
buildName(): string {
return `${this.firstName} "${this.nickName}" ${this.lastName}`;
}
}
Same structure. Tempting to unify.
Inheritance To “Fix” Duplication
class BasePerson {
constructor(private firstName: string, private lastName: string, private nickName: string) {}
buildName(): string {
return `${this.firstName} "${this.nickName}" ${this.lastName}`;
}
}
Person and Doctor extend BasePerson. Duplication eliminated.
Until One Requirement Breaks Everything
class Doctor extends BasePerson {
buildName(): string {
return super.buildName() + ' MD';
}
}
Suddenly the abstraction cracks. Person and Doctor weren’t the same truth.
And Then Teacher Arrives
class Teacher extends BasePerson {
constructor(protected firstName: string, protected lastName: string, protected nickName: string, protected title: string) {
super(firstName, lastName, nickName);
}
buildName(): string {
return `${this.title} ${this.lastName}`;
}
}
Now every subclass overrides buildName. The base class no longer represents a real shared truth. The duplication you removed comes back as overridden behaviour.
The Real Distinction: Accidental vs Deliberate Duplication
Key question: Does this happen to be the same, or does it have to be the same?
Key question: Am I duplicating code or am I duplicating a truth?
Two bits of code can look identical for completely different reasons.
Deliberate duplication: they must behave the same for correctness.
Accidental duplication: they only happen to be the same right now.
Your job is to decide which one you’re looking at.
SPOT: Single Point Of Truth
SPOT asks: what is the actual truth this code represents?
Person has one truth about names.
Doctor has another.
Teacher has yet another.
If you force them into the same implementation, you aren’t removing duplication. You’re overwriting truths.
What You Should Aim For
Remove duplication when it represents the same truth.
Keep duplication when it represents different truths.
Prefer clarity over premature abstraction.
Let behaviour diverge naturally instead of forcing everything into one place.
Practical Questions To Ask
If this changes, what else must change with it?
Do these things genuinely need the same behaviour?
Am I removing duplication or erasing a separate truth?
Conclusion
Duplication is not the enemy. Incorrect truth is. Your job isn’t to chase repeated lines. It’s to protect the right truth in the right place.