Skip to main content

YAGNI — You Aren't Gonna Need It

Editorial

Introduction

A developer is building a notification system. The current requirement is to send emails when orders are placed. But the developer thinks ahead: "We will probably need SMS notifications, push notifications, Slack integrations, and maybe webhook callbacks too."

So instead of building an email sender, they build a full notification framework — abstract message channels, a routing engine, a priority queue, configuration-driven channel selection, and retry logic. It takes three weeks instead of two days. Six months later, the product still only sends emails. The SMS feature was never requested. The Slack integration was deprioritized. The webhook system has zero users.

Three weeks of engineering effort — wasted. Three weeks of code that now needs to be maintained, tested on every deploy, and understood by every new team member — for zero value.

This is the exact scenario YAGNI — You Aren't Gonna Need It — warns against. Originating from Extreme Programming (XP), YAGNI states: do not add functionality until it is necessary. Not until you think it might be necessary. Not until a manager suggests it might be nice. Until there is a concrete, immediate requirement for it.

YAGNI is the natural companion to KISS (previous tutorial). KISS says keep the solution simple. YAGNI says keep the scope simple — do not solve problems you do not have.

YAGNI principle. Build things when you actually need them, not when you foresee needing them. Violation: for a PDF-report requirement, a speculative plugin system — ReportFormatter interface, PdfFormatter (the only one needed), CsvFormatter (never used), ExcelFormatter (raises NotImplementedError 'coming soon'), and a FormatterRegistry. Fix: one SalesReportGenerator.generate_pdf() method; add CSV later only if actually needed. Unused code carries four costs: build, maintenance, cognitive, and opportunity. The prediction problem: roughly two-thirds of features built are never or rarely used (Standish 45% never + 19% rarely), and adding later is usually easier than working around a wrong guess. YAGNI joins DRY and KISS as the trinity against unnecessary complexity.

The Real Cost of Unused Code

Speculative features are not free, even if they "only" cost developer time to write. Every piece of code has four ongoing costs:

  1. Build cost — The time spent writing, testing, and reviewing the code. This is the most visible cost but often the smallest.

  2. Maintenance cost — Every line of code can break. Every class must be updated when dependencies change. Every interface constrains future design choices. Speculative code incurs maintenance cost on every deploy forever.

  3. Cognitive cost — Every developer who reads the codebase must understand (or skip past) the speculative code. Abstractions built for hypothetical requirements add mental overhead to understanding real requirements.

  4. Opportunity cost — The time spent on speculative features is time not spent on features users actually need. This is the most expensive and most invisible cost.

Ron Jeffries, one of the founders of XP, summarized it perfectly: "Always implement things when you actually need them, never when you just foresee that you need them."

The twist is that speculative code often makes the real future requirement harder to implement. When the actual need arrives, it rarely matches the developer's prediction exactly. Now you must work around the speculative abstraction — which was designed for the imagined requirement — to implement the actual one.

YAGNI Violation — The Speculative Plugin System

The requirement: build a report generator that creates PDF sales reports. Here is a YAGNI violation — the developer builds a pluggable report system anticipating formats that were never requested.

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


# YAGNI violation: plugin system for one format

class ReportFormatter(ABC):
    @abstractmethod
    def format(self, data: List[Dict[str, Any]]) -> bytes:
        pass

    @abstractmethod
    def file_extension(self) -> str:
        pass


class PdfFormatter(ReportFormatter):
    def format(self, data: List[Dict[str, Any]]) -> bytes:
        # Actual PDF generation logic
        content = "PDF Sales Report\n"
        for row in data:
            content += f"{row['product']}: ${row['revenue']:.2f}\n"
        return content.encode()

    def file_extension(self) -> str:
        return ".pdf"


class CsvFormatter(ReportFormatter):
    """Never requested. Never used."""
    def format(self, data: List[Dict[str, Any]]) -> bytes:
        header = ",".join(data[0].keys()) if data else ""
        rows = [header] + [",".join(str(v) for v in row.values()) for row in data]
        return "\n".join(rows).encode()

    def file_extension(self) -> str:
        return ".csv"


class ExcelFormatter(ReportFormatter):
    """Never requested. Never used."""
    def format(self, data: List[Dict[str, Any]]) -> bytes:
        raise NotImplementedError("Excel support coming soon")  # It never came

    def file_extension(self) -> str:
        return ".xlsx"


class FormatterRegistry:
    """Plugin registry — over-engineering for 1 format."""
    _formatters: Dict[str, ReportFormatter] = {}

    @classmethod
    def register(cls, name: str, formatter: ReportFormatter) -> None:
        cls._formatters[name] = formatter

    @classmethod
    def get(cls, name: str) -> ReportFormatter:
        if name not in cls._formatters:
            raise ValueError(f"Unknown format: {name}")
        return cls._formatters[name]


# Register formatters
FormatterRegistry.register("pdf", PdfFormatter())
FormatterRegistry.register("csv", CsvFormatter())
FormatterRegistry.register("excel", ExcelFormatter())


class ReportGenerator:
    def __init__(self, formatter_name: str = "pdf") -> None:
        self._formatter = FormatterRegistry.get(formatter_name)

    def generate(self, data: List[Dict[str, Any]]) -> bytes:
        return self._formatter.format(data)
import java.util.*;

// YAGNI violation: plugin system for one format

abstract class ReportFormatter {
    public abstract byte[] format(List<Map<String, Object>> data);
    public abstract String fileExtension();
}

class PdfFormatter extends ReportFormatter {
    @Override
    public byte[] format(List<Map<String, Object>> data) {
        StringBuilder content = new StringBuilder("PDF Sales Report\n");
        for (Map<String, Object> row : data) {
            content.append(row.get("product")).append(": $")
                   .append(row.get("revenue")).append("\n");
        }
        return content.toString().getBytes();
    }

    @Override
    public String fileExtension() { return ".pdf"; }
}

class CsvFormatter extends ReportFormatter {
    // Never requested. Never used.
    @Override
    public byte[] format(List<Map<String, Object>> data) {
        StringBuilder sb = new StringBuilder();
        if (!data.isEmpty()) {
            sb.append(String.join(",", data.get(0).keySet())).append("\n");
        }
        for (Map<String, Object> row : data) {
            sb.append(String.join(",",
                row.values().stream().map(String::valueOf).toArray(String[]::new)
            )).append("\n");
        }
        return sb.toString().getBytes();
    }

    @Override
    public String fileExtension() { return ".csv"; }
}

class ExcelFormatter extends ReportFormatter {
    // Never requested. Never used.
    @Override
    public byte[] format(List<Map<String, Object>> data) {
        throw new UnsupportedOperationException("Excel support coming soon");
    }

    @Override
    public String fileExtension() { return ".xlsx"; }
}

class FormatterRegistry {
    private static final Map<String, ReportFormatter> formatters = new HashMap<>();

    public static void register(String name, ReportFormatter formatter) {
        formatters.put(name, formatter);
    }

    public static ReportFormatter get(String name) {
        ReportFormatter f = formatters.get(name);
        if (f == null) throw new IllegalArgumentException("Unknown format: " + name);
        return f;
    }
}

public class ReportGenerator {
    private final ReportFormatter formatter;

    public ReportGenerator(String formatterName) {
        this.formatter = FormatterRegistry.get(formatterName);
    }

    public byte[] generate(List<Map<String, Object>> data) {
        return formatter.format(data);
    }
}

Five classes and a registry — and the system only ever generates PDF reports. The CsvFormatter was never used by any caller. The ExcelFormatter throws NotImplementedError — a placeholder for a feature that never arrived. The FormatterRegistry adds indirection that makes debugging harder.

The developer spent time writing, testing, and maintaining three classes that deliver zero value. And if CSV support is eventually needed, the business requirements might look completely different from what the developer imagined — perhaps they need a specific column ordering, filters, or aggregation logic that the speculative CsvFormatter does not support.

Applying YAGNI — Build Only What Is Needed

The YAGNI-compliant version does exactly what is required: generate PDF sales reports.

from typing import Dict, List, Any


class SalesReportGenerator:
    """Generates PDF sales reports. That's the requirement. That's what it does."""

    def generate_pdf(self, data: List[Dict[str, Any]]) -> bytes:
        content = "PDF Sales Report\n"
        content += "-" * 40 + "\n"
        total_revenue = 0.0

        for row in data:
            product = row["product"]
            revenue = row["revenue"]
            total_revenue += revenue
            content += f"{product}: ${revenue:.2f}\n"

        content += "-" * 40 + "\n"
        content += f"Total: ${total_revenue:.2f}\n"
        return content.encode()


# Usage — direct and clear
report_gen = SalesReportGenerator()
data = [
    {"product": "Widget A", "revenue": 15000.00},
    {"product": "Widget B", "revenue": 23500.50},
    {"product": "Widget C", "revenue": 8750.25},
]
pdf_bytes = report_gen.generate_pdf(data)
print(pdf_bytes.decode())
import java.util.List;
import java.util.Map;

public class SalesReportGenerator {

    public byte[] generatePdf(List<Map<String, Object>> data) {
        StringBuilder content = new StringBuilder("PDF Sales Report\n");
        content.append("-".repeat(40)).append("\n");
        double totalRevenue = 0;

        for (Map<String, Object> row : data) {
            String product = (String) row.get("product");
            double revenue = (Double) row.get("revenue");
            totalRevenue += revenue;
            content.append(String.format("%s: $%.2f%n", product, revenue));
        }

        content.append("-".repeat(40)).append("\n");
        content.append(String.format("Total: $%.2f%n", totalRevenue));
        return content.toString().getBytes();
    }

    public static void main(String[] args) {
        SalesReportGenerator gen = new SalesReportGenerator();
        List<Map<String, Object>> data = List.of(
            Map.of("product", "Widget A", "revenue", 15000.00),
            Map.of("product", "Widget B", "revenue", 23500.50),
            Map.of("product", "Widget C", "revenue", 8750.25)
        );
        byte[] pdf = gen.generatePdf(data);
        System.out.println(new String(pdf));
    }
}

One class. One method. It does exactly what the requirement says — generates a PDF sales report.

Notice what happened to the code's volume: we went from 5 classes and a registry to 1 class and 1 method. If CSV is needed later, the refactoring is straightforward. The code is simple enough to evolve cleanly when real requirements appear — far easier than working around a speculative abstraction that does not match the actual need.

Visualization

Triage a feature backlog for a PDF-report task. For each request — CSV export, an Excel stub, a pluggable FormatterRegistry, configurable themes — decide Build now or Defer, and watch the effort, maintenance and wasted-effort meters climb while value delivered lags. Then run the 6-months-later reality check: every speculative feature you built is stamped 'never used' (matching the ~two-thirds that go unused), and the real new need — emailing the PDF — turns out to be something none of the speculative formats predicted.

The Prediction Problem

The core argument against YAGNI is: "But what if we need it later? Won't it be harder to add then?"

This sounds reasonable, but it rests on two false assumptions:

False Assumption 1: You Can Accurately Predict Future Requirements

Product requirements change based on user feedback, market shifts, competitor moves, and business strategy. The feature you predict today has a low probability of matching what is actually needed months from now. Experienced developers learn this repeatedly — the features they anticipated rarely match the features that were actually requested.

A study by the Standish Group found that 45% of features built in software products are never used, and another 19% are rarely used. That means roughly two-thirds of all code written was unnecessary.

False Assumption 2: Adding It Later Is Harder Than Adding It Now

This is usually the opposite of reality. Simple, focused code is easier to extend than code burdened with speculative abstractions. When you build an abstraction for an imagined requirement, and the real requirement differs from your prediction, you must work around your own abstraction — which is harder than building from scratch.

The code written today for a requirement that exists today benefits from complete context: you know the exact needs, the exact data shapes, and the exact user expectations. Speculative code lacks this context and is therefore more likely to be wrong.

Common Mistakes

Mistake 1: Using YAGNI to Skip Good Architecture

❌ Wrong: "YAGNI says not to over-engineer, so I'll hardcode the database connection string in every file and skip any modularity."

✅ Right: YAGNI applies to features and abstractions, not to basic engineering practices. Separating configuration from code, writing testable functions, and following SRP (Section 6) are not speculative features — they are foundational practices that make the code workable at all. YAGNI says do not add a caching layer until you have a performance problem. It does not say skip error handling.

Mistake 2: Confusing YAGNI with "Never Refactor"

❌ Wrong: "We built a simple solution. Now the requirements have changed and it is getting messy, but YAGNI says not to add abstractions."

✅ Right: YAGNI says do not add abstractions before the need exists. Once the need is real and present — a second payment provider, a third report format, a fourth notification channel — the time to add the abstraction is now. YAGNI is about timing, not about avoiding good design forever.

Mistake 3: Building Infrastructure "While You're In There"

❌ Wrong: "I'm already touching the payment module, so I'll add support for cryptocurrency payments even though nobody has asked for it."

✅ Right: Scope creep disguised as efficiency. The fact that you are in a file does not mean you should add unrelated capabilities. Finish the task at hand, ship it, and add crypto support when it moves from a hypothetical idea to a business priority.

YAGNI, KISS, and DRY — The Trinity

DRY (previous tutorial), KISS (previous tutorial), and YAGNI form a cohesive trio that addresses different dimensions of the same problem: unnecessary complexity.

  • DRY asks: "Is this knowledge represented in exactly one place?" — It fights duplication.
  • KISS asks: "Is this the simplest solution that works?" — It fights unnecessary complexity in the implementation.
  • YAGNI asks: "Is this solving a problem I actually have right now?" — It fights unnecessary complexity in scope.

Together, they guide you toward code that does what is needed, does it in the most direct way, and avoids redundancy — while remaining easy to extend when real requirements arrive.

These three principles provide the philosophical foundation for the refactoring techniques you will learn in Section 9 and the pattern application judgment you will develop in Sections 10–12. A pattern applied because YAGNI says you need it now, built with KISS simplicity, without DRY violations — that is well-designed code.

Key Takeaways

  • YAGNI means do not build features, abstractions, or infrastructure until a concrete requirement demands it — not when you predict it might be needed
  • Speculative code has four costs: build, maintenance, cognitive, and opportunity — and most speculative features are never used
  • Future requirements rarely match developer predictions — optimizing for an imagined future usually makes the actual future harder to build
  • Simple code is easier to extend than code burdened with speculative abstractions designed for imagined requirements
  • YAGNI does not mean skipping good engineering practices like modularity, testability, and error handling — those serve current needs, not future speculation
  • DRY, KISS, and YAGNI form a complementary trio: fight duplication (DRY), fight complexity (KISS), and fight unnecessary scope (YAGNI)