YAGNI Principle: You Aren't Gonna Need It

Every experienced developer has done it. You're building a simple user registration system, and suddenly you're designing an abstract factory pattern to support authentication providers you might...

Key Insights

  • YAGNI isn’t about writing bad code—it’s about writing the right code at the right time, avoiding speculative features that may never be needed
  • Premature features carry hidden costs: increased complexity, maintenance burden, and cognitive overhead that slows down actual development
  • The principle works hand-in-hand with good design; use abstractions when they solve today’s problems, not tomorrow’s hypotheticals

The Temptation to Over-Engineer

Every experienced developer has done it. You’re building a simple user registration system, and suddenly you’re designing an abstract factory pattern to support authentication providers you might need “someday.” You add plugin hooks for features nobody has requested. You build a configuration system flexible enough to handle scenarios that exist only in your imagination.

YAGNI—“You Aren’t Gonna Need It”—emerged from Extreme Programming (XP) in the late 1990s as a direct response to this tendency. Ron Jeffries, one of XP’s founders, put it bluntly: implement things when you actually need them, never when you just foresee that you might need them.

The principle sounds obvious, yet developers routinely ignore it. Why? Because we’re trained to anticipate problems. We’ve been burned by rigid systems that couldn’t adapt. We want to demonstrate architectural sophistication. And frankly, building elaborate abstractions is more intellectually satisfying than solving mundane business problems.

But that satisfaction comes at a cost.

The Real Cost of Premature Features

Unused code isn’t free. Every line you write must be:

  • Read and understood by every developer who touches that file
  • Maintained when dependencies update or APIs change
  • Tested (or worse, left untested and rotting)
  • Compiled and deployed with every release

Studies consistently show that 60-80% of software features are rarely or never used. When you build speculatively, you’re likely contributing to that waste.

Consider the maintenance burden. That clever plugin system you built for hypothetical extensibility? It now constrains how you can refactor the core logic. The abstract factory pattern you added “for flexibility”? It’s three extra files that every new team member must understand before making simple changes.

There’s also opportunity cost. Time spent building unused features is time not spent on features users actually need. In a 2019 analysis of enterprise projects, teams that practiced strict YAGNI delivered 30% more user-facing functionality in the same timeframe.

YAGNI in Practice: Before and After

Let’s examine a real example. Here’s a user service that’s been over-engineered for hypothetical requirements:

from abc import ABC, abstractmethod
from typing import Dict, Any, Optional, List

# Abstract factory for "future" user storage backends
class UserRepositoryFactory(ABC):
    @abstractmethod
    def create_repository(self, config: Dict[str, Any]) -> 'UserRepository':
        pass

class PostgresRepositoryFactory(UserRepositoryFactory):
    def create_repository(self, config: Dict[str, Any]) -> 'UserRepository':
        return PostgresUserRepository(config)

# Base repository with plugin hooks we "might need"
class UserRepository(ABC):
    def __init__(self):
        self._pre_save_hooks: List[callable] = []
        self._post_save_hooks: List[callable] = []
    
    def register_pre_save_hook(self, hook: callable):
        self._pre_save_hooks.append(hook)
    
    @abstractmethod
    def save(self, user: 'User') -> None:
        pass
    
    @abstractmethod
    def find_by_id(self, user_id: str) -> Optional['User']:
        pass
    
    @abstractmethod
    def find_by_email(self, email: str) -> Optional['User']:
        pass

# Concrete implementation
class PostgresUserRepository(UserRepository):
    def __init__(self, config: Dict[str, Any]):
        super().__init__()
        self._connection_string = config.get('connection_string')
        self._pool_size = config.get('pool_size', 10)
        self._retry_strategy = config.get('retry_strategy', 'exponential')
    
    def save(self, user: 'User') -> None:
        for hook in self._pre_save_hooks:
            hook(user)
        # Actual save logic
        for hook in self._post_save_hooks:
            hook(user)
    
    def find_by_id(self, user_id: str) -> Optional['User']:
        pass  # Implementation
    
    def find_by_email(self, email: str) -> Optional['User']:
        pass  # Implementation

# Service layer with "extensible" validation
class UserService:
    def __init__(self, repository_factory: UserRepositoryFactory, config: Dict[str, Any]):
        self._repository = repository_factory.create_repository(config)
        self._validators: List['UserValidator'] = []
    
    def register_validator(self, validator: 'UserValidator'):
        self._validators.append(validator)
    
    def create_user(self, email: str, name: str) -> 'User':
        user = User(email=email, name=name)
        for validator in self._validators:
            validator.validate(user)
        self._repository.save(user)
        return user

This code has abstract factories, plugin hooks, configurable retry strategies, and extensible validation—none of which are currently used. The actual requirement? Save and retrieve users from Postgres.

Here’s the YAGNI version:

from dataclasses import dataclass
from typing import Optional
import psycopg2

@dataclass
class User:
    id: Optional[str]
    email: str
    name: str

class UserRepository:
    def __init__(self, connection_string: str):
        self._connection_string = connection_string
    
    def save(self, user: User) -> User:
        with psycopg2.connect(self._connection_string) as conn:
            with conn.cursor() as cur:
                cur.execute(
                    "INSERT INTO users (email, name) VALUES (%s, %s) RETURNING id",
                    (user.email, user.name)
                )
                user.id = cur.fetchone()[0]
                conn.commit()
        return user
    
    def find_by_email(self, email: str) -> Optional[User]:
        with psycopg2.connect(self._connection_string) as conn:
            with conn.cursor() as cur:
                cur.execute("SELECT id, email, name FROM users WHERE email = %s", (email,))
                row = cur.fetchone()
                return User(id=row[0], email=row[1], name=row[2]) if row else None

class UserService:
    def __init__(self, repository: UserRepository):
        self._repository = repository
    
    def create_user(self, email: str, name: str) -> User:
        if self._repository.find_by_email(email):
            raise ValueError("Email already exists")
        return self._repository.save(User(id=None, email=email, name=name))

Same functionality. A third of the code. Dramatically easier to understand, test, and modify.

YAGNI vs. Good Architecture: Finding the Balance

YAGNI doesn’t mean abandoning design principles. It means applying them to solve actual problems, not imaginary ones.

Here’s the distinction. This is appropriate abstraction—using dependency injection for testability:

from abc import ABC, abstractmethod

class UserRepository(ABC):
    @abstractmethod
    def save(self, user: User) -> User:
        pass

class PostgresUserRepository(UserRepository):
    def save(self, user: User) -> User:
        # Real database logic
        pass

class UserService:
    def __init__(self, repository: UserRepository):
        self._repository = repository

The interface exists because we need it today—for testing. We can inject a mock repository in tests without touching a database.

This is unnecessary abstraction:

class UserRepositoryFactory(ABC):
    @abstractmethod
    def create(self, config: Dict) -> UserRepository:
        pass

class UserRepositoryFactoryProvider:
    def get_factory(self, database_type: str) -> UserRepositoryFactory:
        # Supporting MySQL, MongoDB, Cassandra "just in case"
        pass

Nobody asked for multiple database support. If they do, you can add it then.

YAGNI is also not an excuse to skip:

  • Security measures: Authentication, authorization, and input validation are current requirements
  • Testing: Tests solve today’s problem of ensuring correctness
  • Error handling: Users will encounter errors today, not hypothetically
  • Logging: You need observability from day one

When to Ignore YAGNI

YAGNI has legitimate exceptions. Ignore it when:

Regulatory requirements are documented. If compliance mandates audit logging, build it now—even if the feature it audits doesn’t exist yet.

API contracts exist with external consumers. Once you publish an API, changing it has real costs. Think carefully about versioning and extensibility before release.

Scaling requirements are known and imminent. If you’re launching to 100,000 users next month, don’t build a system that only handles 100.

The cost of retrofitting is genuinely prohibitive. Some architectural decisions—database choice, fundamental data models—are expensive to change. These deserve more upfront thought.

The key word is “known.” Not suspected, not feared—known. Document the requirement that justifies the complexity.

Practical Guidelines for Teams

Adopt these practices to keep YAGNI alive in your team:

The Three Strikes Rule. Don’t abstract until you’ve written similar code three times. The first time, just write it. The second time, note the duplication. The third time, you have enough examples to design a good abstraction.

Time-box spikes. When someone argues for a complex solution, time-box an investigation. “Let’s spend two hours exploring whether we actually need this.” Often, the simpler solution proves sufficient.

Document deferred decisions. Create a “future considerations” document. When someone suggests a feature you’re not building, add it there. This acknowledges the idea without committing code.

Ask these code review questions:

  • What current requirement does this code satisfy?
  • If we removed this abstraction, what would break today?
  • Can we ship without this and add it when needed?

Measure feature usage. Track which features users actually use. Data kills speculation faster than any principle.

Embrace Iterative Development

YAGNI isn’t about building fragile systems or ignoring the future. It’s about acknowledging a fundamental truth: we’re bad at predicting the future.

Requirements change. Business pivots. That plugin system you built for extensibility? The company got acquired before anyone used it. The multi-tenant architecture you designed? The product stayed single-tenant forever.

Build what you need now. Build it well. When new requirements emerge—real requirements, from real users—refactor. Modern development practices make refactoring safe: version control, automated testing, continuous integration.

The code you don’t write has no bugs, needs no maintenance, and confuses no one. That’s not laziness. That’s engineering discipline.

Ship the simple version. Learn from real usage. Iterate. You aren’t gonna need it—until you do. And when you do, you’ll know exactly what to build.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.