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 Code | Impact |
|---|---|
| Extra complexity | More code = more bugs = harder to understand |
| Wrong assumptions | Future requirements rarely match current speculation |
| Wasted time | Time spent on unused features = time stolen from real features |
| Dead code | Code that’s never used but still needs to be maintained/tested |
| Over-engineering | Systems 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 Principle | YAGNI | |
|---|---|---|
| Focus | Eliminate existing duplication | Don’t add future features prematurely |
| Trigger | ”I’m copy-pasting this again" | "I might need this someday” |
| Action | Refactor current code | Don’t write the speculative code |
| Timing | Applied to existing codebase | Applied 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 arisesImplementation — Spotting YAGNI Violations
-
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)