"I added an interface here, for future extension."

"Are you sure what the future extension will look like?"

"...Not sure."

This conversation appears in code review more often than I'd like to admit. I've said the first half myself, more than once. Every time I say "for future extension," I know in my heart that what follows is a guess — extrapolating from current information about a future that hasn't happened. Then I turn that guess into code structure that everyone after me has to pass through.

"For future extension" is a bad reason not because foresight is bad, but because you don't know what you'll need later. You're guessing. And abstractions, configurations, and branches introduced based on guesses are mostly wrong. What happens when you guess wrong? A pile of code already depends on the wrong structure, and fixing it is more painful than having no structure at all.

Common Forms

"For future extension" appears in code dozens of ways, but a few categories are high-frequency. I've written all of them.

Pre-defining an interface with only one implementation.

The classic example: building a notification feature. Currently only email is needed. The requirement says "send email notification after order payment succeeds." The code includes a NotificationSender interface and an EmailSender implementation.

Why the interface? Because "later we might have SMS, push, in-app messages."

Three months later, the extension arrives. Add SMS. Now you discover the interface's method signature is entirely wrong — SMS doesn't need a subject field, push doesn't need an email address, in-app messages don't need to but need a title and body. Your original Send(to, subject, body string) error has one redundant parameter for each of the three new channels.

So the interface becomes awkward: Send(to, subject, body, channel string), all values are strings, semantics entirely by parameter name convention. Or you change the parameters to Send(notification *Notification) error with all channels sharing one generic struct — clean method signature, but the Notification struct now contains every field any channel might need, most of which are meaningless for most channels.

This interface didn't hide complexity — it flattened it. Callers don't need to know which channel they're invoking, but they need to know which fields are valid — this knowledge went from an implementation detail to a caller prerequisite. More dangerous than no interface at all.

Turning hardcoded values into configuration, because "different scenarios might arise."

A business rule is hardcoded as three lines of if-statements. Very simple. Someone feels it's inelegant — "what if the rule changes later?" So it's extracted into a config file, a config table, or a rules engine.

Now changing one rule requires finding the config file, understanding the config syntax, modifying, validating the format, confirming no impact on other logic sharing the same config. Previously you'd change three if-statements and read the context to confirm impact.

"Rules will change" — true in most cases. But is configuration the right response? Not necessarily. If rule changes are infrequent — once a year — the understanding cost and error surface of the config file may exceed the maintenance cost of three if-statements. If rule changes require quick rollback, canary, A/B — configuration has value. But "might have different scenarios" alone isn't sufficient reason.

Adding bool parameters, creating implicit branches.

func Process(order Order, skipValidation bool) — "some scenarios might not need validation later."

What does this bool parameter tell you? Nothing. You don't know what "some scenarios" are. You don't know what this method does or doesn't do when skipValidation = true. You don't even know how many behaviors inside the method are controlled by this bool — could be 2 if-blocks, could be 10.

A bool parameter essentially forces two different operations into one function signature. Different callers, different preconditions, different failure modes — but they share a name. When you write if skipValidation { ... } else { ... }, you should probably write two functions.

Why Guesses Usually Miss

Because the materials you guess with — current business requirements, current tech environment, current team understanding — aren't the same materials you'll have when the future actually arrives.

When you define the NotificationSender interface, you only know what email notifications look like. You fit a curve from one data point — "notifications are to + subject + body" — and expect all future notification types to land on that curve. They probably won't. SMS goes through a different gateway entirely. Push notifications are asynchronous. In-app messages need read/unread state. You're trying to cover four different communication paradigms with one method signature.

This isn't a skill problem. It's an information problem. Designing with insufficient information isn't design — it's gambling. Betting the future matches your current imagination. In software engineering, the house wins this bet far more often than the gambler.

Another subtler error: you think you're "preparing for the future," but you're actually preparing for "the future as you currently imagine it." The gap between those two is the unnecessary abstraction layer in your code.

The Cost of Guessing Wrong

Guessing wrong doesn't mean returning to the starting line. It's more expensive — a pile of code already depends on the wrong structure.

The NotificationSender interface is already imported in a dozen places. Tests already mock it. Callers already assume a .Send() method exists. Now SMS arrives and .Send() doesn't work — either change the interface and drag all implementations along, or build an adapter layer outside to disguise the new channel as the old interface. Both paths have been taken. Both result in code growing an ecosystem around the interface that's more complex than the interface itself.

Had you not defined the interface — just written an EmailSender struct that callers use directly — three months later when SMS arrives, you add an SmsSender and let callers decide which to use. Maybe a simple factory function, maybe a strategy pattern, maybe a message queue. Whatever you choose, the decision happens when information is sufficient. Design with sufficient information has far higher accuracy than guessing with insufficient information.

Sandi Metz said it: "Duplication is far cheaper than the wrong abstraction." Duplicated code is local — changing one instance doesn't affect another. Wrong abstractions are global — changing an interface signature affects all implementations and callers. Pre-paying complexity not only costs more code now, it makes future changes more expensive. Double cost.

YAGNI Isn't Laziness

YAGNI — You Aren't Gonna Need It — is often misunderstood as "never abstract," "don't write extensible code," "code quality doesn't matter." That's not what it means.

YAGNI says: don't do it before there's a real need. Real need means: at least a second use case has appeared, and you clearly know their commonalities and differences.

First implementation is exploration. You figure out what this feature actually needs to do in the business.

Second implementation is pattern. You see what's truly shared, what's only coincidentally similar, what's completely different.

Third implementation is abstraction. Now you have data — the first two changed in the same direction, abstract. Different directions, don't unify.

This is the same logic as waiting for the second use case before abstracting. One sentence: the first use case tells you what steps exist; the second tells you which are common and which are variable. With only one use case, you can't even see all the differences — you can only guess. Writing guesses as code structure is worse than writing the code simply and waiting for evidence.

This isn't called lack of vision. It's called honesty about uncertainty.

When There Are Exceptions

Exceptions exist, but the conditions are strict.

Having built three or more similar systems in the same domain — your judgment of change directions isn't guessing but pattern recognition backed by experience.

Writing infrastructure or public libraries where the cost of backward-compatible API changes requires upfront evaluation. "Ship first, fix later" is itself expensive in these scenarios. But most people don't write public libraries — they write business code.

Change drivers locked down by the domain itself — like double-entry bookkeeping in a financial system. The fundamental structure of double-entry won't be changed by business requirements. The domain has done the design for you.

For most everyday business code, none of these three conditions hold. You're facing a business still being explored, a domain you don't fully understand. Designing "for the future" under these conditions means guessing wrong most of the time.

Practical Alternatives

Wait for the second use case. First scenario: write it straightforwardly. Second: compare carefully. Third: you have data to judge.

Don't write guesses as code — write them as ADRs. Have an idea — "this might need to become a strategy pattern later" — don't rush to write code. Write it in the ADR's "Consequences": if X, Y, Z types of change occur, the current design may need restructuring into a strategy pattern. This preserves the judgment without pre-paying complexity.

Isolate "likely change points" into thin layers rather than spreading abstractions globally. Don't build the entire notification system on a universal interface. Just isolate the "send" action with a simple function type: type Sender func(msg Message) error. No preset channel types, no predefined parameter structures.

Delaying decisions isn't not deciding. Choosing not to abstract today is an active architectural decision — you've judged current information insufficient for a correct abstraction and chosen to wait for more. This isn't procrastination. The difference is a clear trigger condition: "When a second notification channel appears, we'll re-evaluate the abstraction shape."

Simple Isn't Crude

My first years writing code, I thought adding interfaces, configurations, and abstraction layers demonstrated "engineering sophistication" — the more flexible and configurable the code, the more it looked like "a senior engineer wrote this."

Later I realized that's not how it works.

Senior engineers tend to write fewer abstractions. Not because they can't abstract, but because they know the cost. Every interface, every config option, every bool parameter charges readers a cognitive tax. "For the future" appears far more often in code than it does in requirements documents and PRDs.

The line of code you don't write today might be the biggest favor you do for your future self.

Not all foresight needs to become code. Some foresight is more valuable written into design notes and trigger conditions than pre-baked into code structure.

References