Loading learning content...
A thoughtful developer reads about YAGNI and raises a reasonable objection:
"If I never think about the future, won't I box myself into corners? Won't I create systems that are impossible to extend without complete rewrites? Doesn't good architecture require foresight?"
This is the central tension that every practitioner of YAGNI must navigate. Taken to an extreme, YAGNI produces brittle, short-sighted code that can't evolve. But fear of this outcome leads to the over-engineering that YAGNI tries to prevent.
The resolution lies in understanding that YAGNI and extensibility aren't opposites—they're complementary when properly balanced. The goal isn't to never think about the future; it's to prepare for the future without building the future.
By the end of this page, you will understand the difference between speculative features and defensive architecture, how to leave options open without incurring cost, the role of SOLID principles in enabling YAGNI, strategic versus tactical technical decisions, and practical frameworks for when to invest in extensibility. You'll develop judgment for the nuanced middle ground between over-engineering and under-engineering.
Understanding where YAGNI applies requires recognizing that software decisions exist on a spectrum from tactical to strategic.
| Tactical Decisions | Strategic Decisions |
|---|---|
| Low switching cost | High switching cost |
| Localized impact | System-wide impact |
| Easily reversible | Difficult to reverse |
| Change scope: hours/days | Change scope: weeks/months |
| Examples: helper functions, local data structures, specific implementations | Examples: database choice, service boundaries, API contracts, core abstractions |
YAGNI applies most strongly to tactical decisions.
For tactical decisions, the cost of being wrong is low. If you implement a feature you don't need, you can delete it. If you implement a feature too simply, you can refactor it. The switching cost is bounded by the scope of the decision.
Strategic decisions warrant more foresight—but still not speculation.
For strategic decisions, the cost of being wrong is high. Changing your database from SQL to NoSQL can take months. Restructuring service boundaries affects every team. These decisions deserve careful thought.
But 'careful thought' isn't speculation. Even for strategic decisions, the goal isn't to predict and build for unknown futures. It's to:
The difference: building for hypotheticals (speculation) versus not building yourself into a corner (defensive architecture).
When facing a significant architectural decision, ask: (1) What options am I preserving? (2) What options am I foreclosing? YAGNI says don't build speculatively, but it doesn't say foreclose options carelessly. Preserving options is cheap; building for options is expensive.
The distinction between defensive architecture and speculative features is subtle but critical.
Defensive Architecture:
Speculative Features:
Defensive (YAGNI-Compliant):
// Interface-based design
interface UserRepository {
findById(id: string): User | null;
save(user: User): void;
}
// Simple, in-memory implementation
class InMemoryUserRepository
implements UserRepository {
private users = new Map<string, User>();
findById(id: string) {
return this.users.get(id) ?? null;
}
save(user: User) {
this.users.set(user.id, user);
}
}
Cost: ~10 extra lines for the interface. Benefit: Can swap to database later without changing clients.
Speculative (YAGNI-Violation):
// Building for hypothetical multi-database future
class UniversalUserRepository {
constructor(
private primaryDb: DatabaseAdapter,
private replicaDb?: DatabaseAdapter,
private cache?: CacheAdapter,
private auditLog?: AuditAdapter
) {}
async findById(id: string) {
// 50 lines of multi-layer lookup
// None of which is used yet
}
}
interface DatabaseAdapter { /*...*/ }
interface CacheAdapter { /*...*/ }
interface AuditAdapter { /*...*/ }
Cost: 10x more code, complexity, testing. Benefit: Handles scenarios that may never occur.
The Cost-Benefit Lens:
Defensive architecture has a high ratio of future optionality to present cost. An interface that takes 5 minutes to write enables swapping implementations forever.
Speculative features have a low ratio. Hours of implementation for futures that may not materialize.
Rule of Thumb: If the 'future-proofing' costs less than 10% additional effort and doesn't add complexity that interferes with today's development, it's likely defensive architecture. If it's a significant undertaking that delays current work, it's likely speculation.
SOLID principles—often seen as 'enterprise' or 'complex'—are actually enablers of YAGNI. They make code easy to change, which means you can confidently defer features knowing you can add them later.
The SOLID-YAGNI Connection:
| SOLID Principle | How It Enables YAGNI |
|---|---|
| Single Responsibility (SRP) | Small, focused classes can be modified without ripple effects. You can add functionality later without rewriting existing code. |
| Open/Closed (OCP) | New behavior through extension (new implementations) rather than modification. Adding features doesn't require changing working code. |
| Liskov Substitution (LSP) | Implementations can be swapped without breaking clients. Today's simple implementation can be replaced with complex one later. |
| Interface Segregation (ISP) | Clients depend only on what they use. New capabilities can be added to interfaces without affecting existing clients. |
| Dependency Inversion (DIP) | Depending on abstractions, not concretions. Enables swapping implementations as requirements evolve. |
The Paradox Resolved:
Following SOLID doesn't mean building for the future—it means building in a way that doesn't preclude the future. SOLID code is easy to extend because of its structure, not because extensions were speculatively built.
Consider the contrast:
Without SOLID + Without YAGNI: Messy code with speculative features bolted on. Hard to understand, hard to change, and the speculative features are probably wrong.
With SOLID + With YAGNI: Clean code that implements only what's needed, structured so new capabilities can be added when requirements are known. Best of both worlds.
SOLID principles aren't 'nice to have' when practicing YAGNI—they're essential. Without SOLID's structural qualities, deferring features becomes risky because adding them later is too hard. SOLID-compliant code gives you the confidence to say 'we'll add that when needed.'
Specific patterns help preserve options without building speculatively. These are 'defensive architecture' in pattern form.
The Pattern: Provide a simplified interface to a complex subsystem—or an interface to a subsystem that doesn't exist yet.
YAGNI Application: Create a facade with a simple implementation. When complexity is needed, replace the implementation without changing clients.
Example:
// Facade to notification capability
class NotificationService {
sendNotification(userId: string, message: string): void {
// V1: Simple console log
console.log(`TODO: Notify ${userId}: ${message}`);
}
}
// Later, when email is needed:
class NotificationService {
constructor(private emailClient: EmailClient) {}
sendNotification(userId: string, message: string): void {
this.emailClient.send(userId, message);
}
}
Benefit: Clients call NotificationService from day one. Implementation evolves without client changes.
Cost: Virtually zero. The facade is just a function that does the simple thing today.
A key insight: some decisions are cheap to reverse, others are expensive. YAGNI applies most strongly to cheap-to-reverse decisions.
Cheap to Reverse (Strong YAGNI):
Expensive to Reverse (Careful Thought Warranted):
For expensive-to-reverse decisions, invest time in understanding constraints and options—but still don't build speculative functionality. The investment is in understanding, not building.
Speculative approach:
'Let's use PostgreSQL for ACID compliance, add Redis for caching, set up a separate Elasticsearch instance for search, and consider adding Cassandra if we need to scale writes.'
Result: Multi-database complexity from day one, for requirements that aren't validated.YAGNI approach:
'Let's start with PostgreSQL—it handles our current needs. We'll add a repository interface so database access is centralized. If we discover caching is needed, Redis can be added behind the interface. If search becomes a bottleneck, Elasticsearch is an option.'
Result: Simple architecture. The decision about additional databases is deferred until there's evidence it's needed.
Note: The interface (defensive architecture) is cheap. The multi-database setup (speculation) is expensive.Michael Feathers introduced the concept of 'seams'—places in code where behavior can be altered without modifying the code itself. Seams are the engineering of reversibility.
Types of Seams:
Creating Seams Is YAGNI-Compliant:
Creating seams is defensive architecture—it costs little but enables future change. The seam itself doesn't implement speculative functionality; it just ensures that if new functionality is needed, there's a place to introduce it.
Where to Place Seams:
Not Every Line Needs a Seam:
Seams have small cost but non-zero cost (cognitive, code, indirection). Place them strategically at likely change points, not everywhere.
For any significant component, ask: 'If this needed to work differently, where would I make the change?' If the answer involves extensive modification or rewrite, consider adding a seam. If the answer is 'swap this implementation for another,' you have a seam.
YAGNI is a strong principle, but not absolute. There are domains and situations where more upfront investment is warranted.
| Situation | Why YAGNI Is Relaxed | Guideline |
|---|---|---|
| Safety-critical systems | Failure costs are catastrophic; speculation may be necessary for redundancy | Invest in proven approaches, not novel speculation |
| Published APIs | External consumers depend on stability; changes break them | Think carefully about API design; versioning is expensive |
| Foundational infrastructure | Everything else builds on this; changes ripple everywhere | More design thought warranted, but still validate before building |
| Regulatory requirements | Future regulations may be known or predictable | Build to known upcoming requirements, not hypothetical ones |
| Physical system interfaces | Hardware changes are expensive; software must accommodate | Understand hardware constraints and plan accordingly |
| Performance-critical paths | Fundamental architecture limits performance; refactoring may not help | Benchmark and design for actual performance requirements |
The Common Thread:
Notice that even in these situations, the advice isn't 'speculate freely.' It's 'invest in understanding' and 'build for known constraints.' The situations that relax YAGNI are those with:
Even then, the investment is in thoughtful design, not comprehensive features for hypothetical futures.
It's easy to rationalize speculation by claiming 'this is a safety-critical path' or 'this is foundational infrastructure.' Be honest: Is this actually a high-cost-to-change decision, or is the fear of future difficulty driving speculation? Most code is more malleable than we assume.
Applying YAGNI requires judgment. Here's a framework for making these decisions systematically.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
# YAGNI Decision Framework ## Step 1: Classify the Decision┌────────────────────────────────────────────────────────┐│ Is this a TACTICAL or STRATEGIC decision? ││ ││ Tactical: Local impact, easy to change ││ Strategic: System-wide impact, expensive to change │└────────────────────────────────────────────────────────┘ If TACTICAL: Strong YAGNI. Build minimum for current needs.If STRATEGIC: Proceed to Step 2. ## Step 2: Assess Certainty┌────────────────────────────────────────────────────────┐│ How certain is the future need? ││ ││ HIGH: Confirmed requirement, contractual obligation ││ MEDIUM: Likely based on patterns, stakeholder hints ││ LOW: Possible, but based on intuition or precedent │└────────────────────────────────────────────────────────┘ If HIGH: Consider building, but validate scope.If MEDIUM: Build structure (seams), not features.If LOW: Apply YAGNI strictly. Don't build. ## Step 3: Assess Switching Cost┌────────────────────────────────────────────────────────┐│ What's the cost to add/change this later? ││ ││ LOW: Hours to days of refactoring ││ MEDIUM: Weeks of work, some coordination needed ││ HIGH: Months of work, cross-team impact │└────────────────────────────────────────────────────────┘ If LOW: YAGNI. Add when needed.If MEDIUM: Add seams and abstractions. Don't build features.If HIGH: Invest in understanding. Still minimize speculation. ## Decision Matrix┌─────────────┬───────────────────────────────────────────┐│ │ SWITCHING COST ││ CERTAINTY ├─────────────┬─────────────┬───────────────┤│ │ LOW │ MEDIUM │ HIGH │├─────────────┼─────────────┼─────────────┼───────────────┤│ LOW │ YAGNI │ YAGNI │ Add seams only│├─────────────┼─────────────┼─────────────┼───────────────┤│ MEDIUM │ YAGNI │ Add seams │ Add seams, ││ │ │ │ some structure│├─────────────┼─────────────┼─────────────┼───────────────┤│ HIGH │ YAGNI │ Consider │ Consider ││ │ │ building │ building │└─────────────┴─────────────┴─────────────┴───────────────┘ ## Final CheckBefore building anything not immediately needed:□ Have I articulated why this is needed?□ Could a seam suffice instead of implementation?□ What's the cost if I'm wrong about this need?□ What could I build instead with this time?Let's apply the framework to realistic scenarios.
Question: Should we build multi-tenancy from day one?
Analysis:
- Decision type: Strategic (affects data model fundamentally)
- Certainty: HIGH (multi-tenancy is definitely needed for SaaS)
- Switching cost: HIGH (retrofitting tenant isolation is painful)Recommendation: This falls in the 'consider building' zone.
However, 'building multi-tenancy' can range from:
- Adding tenant_id to every table (low cost)
- Building a complete tenant provisioning, isolation, and administration system (high cost)
YAGNI-aligned approach:
- Add tenant_id columns now (cheap, prevents painful migration)
- Build tenant admin UI when there are multiple tenants
- Build tenant isolation features as security requirements materialize
Result: Defensive architecture (tenant_id) without speculative features (admin UI).Question: Should we build internationalization support now?
Analysis:
- Decision type: Tactical to Strategic (depends on depth)
- Certainty: MEDIUM (international expansion is possible but not confirmed)
- Switching cost: MEDIUM (can be retrofitted with effort)Recommendation: This falls in the 'add seams' zone.
YAGNI-aligned approach:
- Use a translation function consistently: t('welcomeMessage')
- For now, t() just returns the key (English text)
- When internationalization is real, plug in actual translation logic
Don't build:
- Translation management system
- Multi-language database schema
- Locale-specific UI layouts
- RTL support
Result: ~10 minutes to wrap strings in t(). When i18n is real, the seam is there. If it never happens, t() is transparent overhead.Notice that both cases benefit from some preparation—but bounded, low-cost preparation. The goal is to do just enough to avoid painting yourself into a corner, without building for scenarios that may never materialize.
We've navigated the apparent tension between YAGNI and responsible architecture. The resolution is nuanced but actionable.
The Master Skill:
The ultimate skill is judgment—knowing when to apply strict YAGNI, when to invest in defensive architecture, and when genuine future-building is warranted. This judgment develops through experience, reflection, and learning from both over-engineering and under-engineering mistakes.
YAGNI isn't a rule to follow blindly. It's a bias towards simplicity, tempered by awareness of costly irreversibility. The goal is not minimal code at all costs—it's maximum value delivery with minimal waste.
You've completed the YAGNI module. You now understand why to avoid speculative features, the real costs of unused code, how iterative design supports minimal building, and how to balance YAGNI with responsible architecture. Apply this judgment in every feature, every design decision, and every code review. Build what matters, when it matters.