August 3, 2026
YAGNI Isn't 'Don't Plan Ahead': It's 'Don't Pay for a Guess'
The Situation
I was building a small shared utility layer meant to be reused across more than one feature: the kind of code that formats output, wires up a couple of constants, and gets imported wherever that formatting is needed. Partway through, I split it into two files: one for the "core" formatting logic, one for a narrower helper I was confident would need to grow independently as more consumers showed up.
It felt responsible. I was thinking about the shape of the codebase six months out, not just the feature in front of me.
Code review disagreed. The feedback was short: merge the two files back into one. Split them again only once there's a second real case that actually needs the separation, not before.
The Principle, Properly
YAGNI (You Aren't Gonna Need It) gets summarized as "don't over-engineer," which makes it sound like a warning against thinking ahead. That's not quite it. The real claim is narrower and more useful:
Every piece of structure you add for a future requirement is a bet. You're paying a cost now (more files, more indirection, more surface area to read and maintain) for a benefit that only materializes if your guess about the future turns out to be correct.
The asymmetry is the important part. If you don't build the structure and you turn out to need it later, the cost is: you build it later, when you have real information about what it should look like. If you do build it now and you don't end up needing it, the cost is: everyone who reads this code from now on pays a small tax for a decision that never paid off.
Splitting early doesn't just risk being wrong about whether you'll need the separation. It risks being wrong about where the seam should go. A guess made before the second use case exists is a guess about someone else's requirements. A split made after the second use case exists is just... describing what's already there.
How I Applied It
Illustrative version of the before/after (not the real code):
// Before: split preemptively, ahead of any second real consumer
// export.core.ts
export function buildWorkbook(rows: Row[]): Workbook { /* ... */ }
// export.attachment.ts
export const EXPORT_MIME = 'application/vnd...';
export function toAttachment(buffer: Buffer, filename: string) { /* ... */ }// After: one file, until a second concrete need justifies splitting again
// export.util.ts
export function buildWorkbook(rows: Row[]): Workbook { /* ... */ }
export const EXPORT_MIME = 'application/vnd...';
export function toAttachment(buffer: Buffer, filename: string) { /* ... */ }Nothing about the logic changed, only the number of files someone has to open to understand one cohesive piece of behavior. Merging them back removed a level of indirection that wasn't earning its keep yet.
The Broader Takeaway
The instinct to plan for scale isn't wrong. It's just aimed at the wrong moment. Good foresight in code isn't "add the abstraction now so it's ready later." It's "write the current thing clearly enough that adding the abstraction later is cheap when the second real case actually arrives."
I still think about future consumers when I write something reusable. I just no longer build the fork in the road before there's a second road.