Don't Repeat Yourself vs Repeatable Patterns
and Knowing When to Hand It a Glass of Water
The DRY principle famously says: Don’t Repeat Yourself.
At its best, DRY reduces bugs, keeps behavior consistent, and ensures that a fix in one place ripples out everywhere it should. Fewer copies mean fewer haunted corners where ancient logic lingers, quietly judging your new code.
Applied well, DRY is a gift.
Applied aggressively, it becomes… very thirsty.
Why We Like DRY
Duplication is sneaky. It starts small and harmless, then slowly diverges:
- One copy gets fixed
- Another gets “temporarily” tweaked
- A third is forgotten entirely
Suddenly, similar-looking code behaves differently in subtle, frustrating ways.
DRY helps by:
- Centralizing logic
- Reducing inconsistency
- Making changes safer and faster
When you fix a bug once and it disappears everywhere, that’s DRY doing its job.
When DRY Starts Asking for Sacrifices
DRY goes wrong when reuse becomes more important than clarity.
A classic warning sign:
“This component can do everything… if you pass the right 14 props.”
If a component or function starts demanding an army of configuration flags just to do a slightly different job, that’s not elegance—that’s abstraction debt.
A Props Circus in the Wild
type ButtonProps = {
label: string;
size?: "sm" | "md" | "lg";
tone?: "primary" | "secondary" | "danger";
iconLeft?: string;
iconRight?: string;
isLoading?: boolean;
isDisabled?: boolean;
fullWidth?: boolean;
rounded?: boolean;
withShadow?: boolean;
align?: "left" | "center" | "right";
href?: string;
onClick?: () => void;
};
export function Button(props: ButtonProps) {
// ...
}
This button isn’t reusable—it’s exhausting.
Each new feature adds another boolean, another branch, another mental tax on anyone trying to use it correctly.
Duplication vs. Repeatable Patterns
Here’s the important distinction:
- Bad duplication: copy-paste with no boundaries
- Good duplication: clear, intentional patterns with focused responsibility
If a component wants to behave differently in meaningful ways, it often deserves its own file and its own name.
Splitting this into:
PrimaryButton.tsxIconButton.tsx
…isn’t a failure of DRY. It’s good design.
Smaller, Kinder Components
// PrimaryButton.tsx
export function PrimaryButton(props: { label: string; onClick?: () => void }) {
// ...
}
// IconButton.tsx
export function IconButton(props: { icon: string; label?: string }) {
// ...
}
These components:
- Are easy to understand
- Are hard to misuse
- Don’t require memorizing a prop matrix
They can still share styles, helpers, or base components under the hood—without exposing that complexity to consumers.
DRY Applies to Backend Code Too
This pattern shows up everywhere:
// user.get.ts
export const getUser = () => {
// ...
};
// user.list.ts
export const listUsers = () => {
// ...
};
Clear names. Clear files. Clear intent.
You could combine them into one ultra-flexible function—but then every call site needs to remember which flags summon which behavior. That’s not reuse; that’s trivia.
A Rule of Thumb That Rarely Fails
Ask yourself:
“Would a new developer understand this pattern in five minutes?”
If the answer requires:
- A long README
- Tribal knowledge
- Or “well, it depends…”
…it’s probably over-DRY.
Patterns should be obvious, not clever.
DRY Is About Behavior, Not Shape
It’s fine for code to look similar.
What matters is that:
- Shared behavior is shared intentionally
- Differences are explicit
- Boundaries are clear
Copying a small function and renaming it is sometimes the most honest thing you can do.
Final Thought
DRY is a tool, not a commandment.
Use it to:
- Eliminate accidental duplication
- Keep behavior consistent
- Reduce bugs
Ignore it when it:
- Obscures intent
- Bloats APIs
- Turns simple things into configuration puzzles
Good code is readable first, reusable second.
And no principle should require a novel to explain.