What is YAGNI?

YAGNI stands for “You Aren’t Gonna Need It” — an Extreme Programming (XP) principle that says: don’t implement something until it is actually needed. Build what you need now, not what you think you might need in the future. Speculative code adds complexity, maintenance burden, and confusion — without providing value.

Explanation

Real-World Analogy

  • Imagine building a house 🏠. The client wants a 3-bedroom house. Do you also pre-build a swimming pool “just in case”? Add a helicopter pad “for future expansion”? Pre-wire for a server room “they might want someday”?
  • No. You build what they need now. Adding futures wastes time, money, and clutters the building. When they actually need a pool, you add it then — with the right design for what they actually want at that point.

Why YAGNI Matters

Problem with Speculative CodeImpact
Extra complexityMore code = more bugs = harder to understand
Wrong assumptionsFuture requirements rarely match current speculation
Wasted timeTime spent on unused features = time stolen from real features
Dead codeCode that’s never used but still needs to be maintained/tested
Over-engineeringSystems too complex for their actual needs
  • "The best code is no code at all." — Every line you write must be maintained, tested, and understood by future developers (including future you).

YAGNI vs DRY

DRY PrincipleYAGNI
FocusEliminate existing duplicationDon’t add future features prematurely
Trigger”I’m copy-pasting this again""I might need this someday”
ActionRefactor current codeDon’t write the speculative code
TimingApplied to existing codebaseApplied to new work decisions

YAGNI — Before & After

# ❌ YAGNI Violation — Adding features that don't exist in requirements
class UserService:
    def __init__(self):
        # Speculative: "we might support multiple databases later"
        self.primary_db = None
        self.replica_db = None
        self.db_pool = []
        self.cache = {}  # "might need caching"
        self.audit_log = []  # "might need audit trails"
        self.rate_limiter = None  # "might add rate limiting"
 
    def get_user(self, user_id: int):
        # Speculative: complex fallback logic "just in case"
        try:
            if self.cache.get(user_id):
                return self.cache[user_id]
            user = self.primary_db.find(user_id) if self.primary_db else None
            if not user and self.replica_db:
                user = self.replica_db.find(user_id)
            self.cache[user_id] = user
            return user
        except Exception:
            return None
 
# ✅ YAGNI — Build only what's needed NOW
class UserService:
    def __init__(self, db):  # Dependency injection — use what you have
        self.db = db
 
    def get_user(self, user_id: int):
        return self.db.find(user_id)
 
    # Add caching, replicas, rate limiting WHEN the requirement arises

Implementation — Spotting YAGNI Violations

  • Real-world patterns where YAGNI is violated and their pragmatic alternatives. Python · Java

    Languages:

# ─── Python — Common YAGNI violations and fixes ───────────────────────
 
# ── Pattern 1: Unnecessary Strategy Pattern "just in case" ────
# ❌ YAGNI Violation
class SortStrategy:
    def sort(self, data): raise NotImplementedError
class BubbleSort(SortStrategy):
    def sort(self, data): return sorted(data)  # "might swap algorithms later"
class Sorter:
    def __init__(self, strategy: SortStrategy): self.strategy = strategy
    def sort(self, data): return self.strategy.sort(data)
 
# ✅ YAGNI: Just sort it! Add strategy when you actually have 2+ strategies
def sort_data(data: list) -> list:
    return sorted(data)
 
 
# ── Pattern 2: Over-parameterized functions ────────────────────
# ❌ YAGNI Violation
def send_email(to, subject, body,
               cc=None, bcc=None,
               priority="normal",
               format="html",
               retry_count=3,
               schedule_time=None,
               template_id=None,
               analytics_tag=None):  # Most of these never used
    pass
 
# ✅ YAGNI: Start simple, add params when needed
def send_email(to: str, subject: str, body: str) -> bool:
    """Send basic email. Extend params when actual requirements arise."""
    print(f"Email to {to}: {subject}")
    return True
 
 
# ── Pattern 3: Config for non-existent scenarios ───────────────
# ❌ YAGNI Violation
class AppConfig:
    def __init__(self):
        self.multi_tenant = False     # "might need multi-tenancy"
        self.feature_flags = {}       # "might need feature flags"
        self.plugin_system = []       # "might need plugins"
        self.i18n_locales = ["en"]    # "might go international"
        self.dark_mode = False        # current scope: internal tool, no UI
 
# ✅ YAGNI: Only what's needed today
class AppConfig:
    def __init__(self, debug: bool = False, db_url: str = "localhost"):
        self.debug = debug
        self.db_url = db_url
 
 
# ── Pattern 4: YAGNI done right — incremental growth ──────────
# Week 1: just need to create a user
def create_user(name: str, email: str) -> dict:
    return {"name": name, "email": email}
 
# Week 3: requirement: users need roles
def create_user(name: str, email: str, role: str = "user") -> dict:
    return {"name": name, "email": email, "role": role}
 
# Week 7: requirement: users need to be validated
class User:
    def __init__(self, name: str, email: str, role: str = "user"):
        if "@" not in email: raise ValueError("Invalid email")
        self.name = name
        self.email = email
        self.role = role
# Class introduced ONLY when complexity warranted it
// ─── Java — YAGNI in API Design ──────────────────────────────────────
 
// ❌ YAGNI Violation — Interface with methods no one needs yet
interface UserRepository {
    User findById(int id);
    User findByEmail(String email);
    List<User> findAll();          // No use case for this yet
    List<User> findByRole(String role); // Not in requirements
    void bulkImport(List<User> users);  // Future feature not needed yet
    User findByExternalId(String id);   // "might integrate OAuth later"
}
 
// ✅ YAGNI — Only what's currently needed
interface UserRepository {
    User findById(int id);
    User findByEmail(String email);
    void save(User user);
    // Add findByRole, bulkImport WHEN they are actually required
}
 
 
// ❌ YAGNI — Generic framework nobody asked for
abstract class GenericProcessor<T, R, C extends ProcessingContext> {
    abstract R process(T input, C context);
    abstract void validate(T input);
    abstract void preProcess(T input, C context);
    abstract void postProcess(R result, C context);
    // 400 lines of framework for one use case...
}
 
// ✅ YAGNI — Just a simple class
class OrderProcessor {
    public Invoice process(Order order) {
        validate(order);
        return createInvoice(order);
    }
    private void validate(Order o) { /* just check what's needed */ }
    private Invoice createInvoice(Order o) { return new Invoice(o); }
    // Refactor to generic WHEN you have 2+ processors that need it
}

Key Takeaways

  • Build now, for now — implement what the current requirement demands, nothing more.
  • Speculative code is technical debt that was never useful — it costs maintenance without providing value.
  • Rule: If there’s no test for it, no requirement for it, and no current use — don’t write it.
  • YAGNI works best with refactoring — when you do need the feature, add it then using the knowledge of what you actually need.
  • Related: DRY Principle, Law of Demeter, Single Responsibility Principle (SRP)

More Learn

GitHub & Webs