YAGNI — You Aren't Gonna Need It
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.

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:
-
Build cost — The time spent writing, testing, and reviewing the code. This is the most visible cost but often the smallest.
-
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.
-
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.
-
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

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)