You Aren't Gonna Need It: costs of building speculative features, the difference between speculative features and keeping options open, and reversible versus irreversible decisions.
YAGNI (You Aren't Gonna Need It, from Extreme Programming) says do not build a capability until you actually need it. Speculative features cost time to build, delay what is needed now, add code to maintain, and are often wrong when the real requirement arrives. YAGNI does not mean skipping tests or refactoring; it is those practices that make adding the feature later cheap. I make an exception for decisions that are expensive to reverse, such as public API contracts, persisted data formats and security, where some upfront design pays off.
"YAGNI ('You Aren't Gonna Need It') comes from Extreme Programming: don't build a capability until you actually need it. Martin Fowler's breakdown of why is the clearest. A presumptive feature has a cost of build (time spent now), a cost of delay (the feature customers need now ships later), a cost of carry (extra code and concepts everyone works around), and often a cost of repair, because when the real requirement arrives it's usually different from the guess. Two clarifications make YAGNI work rather than becoming an excuse. First, it applies to presumptive features and generality, never to tests, refactoring and clean modular code: those are exactly what make adding the feature later cheap. Second, I scale it by reversibility. Internal class design is cheap to change, so I apply YAGNI aggressively. Public API contracts, event schemas, persisted data formats, security and privacy decisions are expensive to reverse once others depend on them, so they get deliberate upfront design and versioning from day one."
| Cost | Meaning | Example |
|---|---|---|
| Build | Effort spent now | 6 weeks on a theming engine |
| Delay | Value lost on what's needed now | The reporting feature slips a quarter |
| Carry | Ongoing complexity tax | Every change works around tenant-theme hooks |
| Repair | Rework when reality differs | Customer two needs SSO/white-label domains, not themes |
Even if the guess is right, you pay build + delay + carry earlier than necessary.
| Applies to | Does not apply to |
|---|---|
| Features nobody has asked for ("we might need multi-currency") | Tests |
| Extension points/plug-in frameworks for one variant | Refactoring and clean naming |
| Generic base classes for one use | Clear module boundaries |
| Configuration options nobody sets | Known, scheduled requirements |
| Parameters "in case" | Security, privacy and compliance basics |
| Premature scaling infrastructure | Observability needed to operate today |
| Decision | Reversibility | Approach |
|---|---|---|
| Internal class/module design | Easy | YAGNI strongly; refactor when needed |
| Feature flags, configuration | Easy | Add when a real variant exists |
| Database schema for new data | Medium (migrations help; data backfills don't come free) | Model what you know; plan migrations |
| Public HTTP API shape | Hard | Design carefully; version from day one |
| Event/message schemas | Hard (consumers you don't control) | Make additive change possible; version |
| Persisted file/data formats | Hard | Design upfront |
| Data retention, privacy, security model | Very hard | Decide upfront |
| Choice of primary database/cloud | Hard | Decide deliberately; keep boundaries |
| Cheap option | Why it helps later |
|---|---|
| Small, well-named modules | A new variant has an obvious home |
| Tests around behaviour | Refactoring to add an extension point is safe |
| DI and configuration | Behaviour can be swapped without rewrites |
| Ports at volatile boundaries (vendors, I/O) | Integrations can change |
| Additive-change-friendly contracts | New fields don't break consumers |
Our [SaaS product] had one customer when the team spent [six weeks] building a multi-tenant theming engine "for when we have more tenants". The reporting feature that customer was actually asking for slipped [a quarter]. When customer two signed [about a year] later, their needs were white-label domains and SSO, not themes; the engine didn't fit and was eventually deleted. Since then I push for building only against a real customer need or ticket, while keeping the code modular and tested so that adding a genuine variant later takes days, not weeks. The exception I argue for is anything that's hard to reverse: on the same product we did invest upfront in a versioned public API and in a tenant-aware data model, because changing those later would have meant migrations across every customer.
Checks judgement about speculative work.
Green flags
Red flags
YAGNI emerged from Extreme Programming (Kent Beck, Ron Jeffries, late 1990s). Jeffries' version: "Always implement things when you actually need them, never when you just foresee that you need them." Martin Fowler's Yagni article (2015) added the cost-of-build/delay/carry/repair analysis and stressed that YAGNI depends on refactoring, tests and continuous integration.
| Principle | Says | Reconciliation |
|---|---|---|
| OCP | Design so variants can be added without modification | Add the extension point when the second real variant arrives |
| YAGNI | Don't add extension points speculatively | Keep code clean so that refactoring to the extension point is cheap |
The combination: first variant as a plain class; second variant triggers a quick, safe refactor to an interface/strategy.
| Question | If no |
|---|---|
| Is there a customer need, ticket or committed roadmap item? | Don't build it |
| Would retrofitting it later be disproportionately expensive? | Don't build it now |
| Is it a contract or format others will depend on? | Lighter design is fine |
| Would building it now block or delay current priorities? | (If yes, definitely don't) |
| Sign | Example |
|---|---|
| Interfaces with one implementation and no test reason | IReportExporter with only CsvReportExporter |
| Configuration nobody sets | EnableLegacyMode defaulted everywhere |
| Unused parameters or overloads | Export(report, format: "csv", options: null) |
| Generic engines | Rule engine with one hard-coded rule |
| Infrastructure ahead of load | Kafka + sharding for 50 requests/minute |
// Speculative: a pluggable export framework for one format.
// public interface IExportFormatter { string Format { get; } byte[] Export(Report r); }
// public sealed class ExportFormatterRegistry { /* reflection-based plug-in loading */ }
// YAGNI: the one format we need, written cleanly so a second is easy later.
using System.Globalization;
using System.Text;
var report = new Report("October sales", [new("Sydney", 120_500m), new("Perth", 64_250m)]);
Console.WriteLine(CsvExport.Export(report));
public sealed record ReportRow(string Region, decimal Revenue);
public sealed record Report(string Title, IReadOnlyList<ReportRow> Rows);
public static class CsvExport
{
public static string Export(Report report)
{
var sb = new StringBuilder("Region,Revenue\n");
foreach (var row in report.Rows)
sb.Append(row.Region).Append(',')
.Append(row.Revenue.ToString(CultureInfo.InvariantCulture)).Append('\n');
return sb.ToString();
}
}
If an Excel export is requested, extracting an interface from CsvExport takes minutes because the code is small and tested.
What happens: multi-tenant, multi-currency, multi-region features for a single customer. Why it is wrong: real needs differ; work is discarded or reworked. Do instead: build for today's customer; keep code easy to change.
What happens: "we don't need tests yet". Why it is wrong: those practices are what make deferring features safe. Do instead: keep quality high so later additions are cheap.
What happens: a public API or event schema designed ad hoc with no versioning. Why it is wrong: changing it later breaks consumers you don't control. Do instead: invest design time where reversal is expensive.
What happens: unused overloads, flags and settings. Why it is wrong: they must be understood, tested and preserved. Do instead: add them when a caller needs them.
What happens: message brokers, sharding, microservices ahead of real load. Why it is wrong: operational cost and complexity now for a maybe-later need. Do instead: scale when measurements show the need; keep boundaries clean.
What happens: entity-attribute-value tables to avoid future migrations. Why it is wrong: queries, validation and performance suffer immediately. Do instead: model known data properly and use migrations.
What happens: no thought about structure at all. Why it is wrong: change becomes expensive, which defeats YAGNI's premise. Do instead: design for today's needs, cleanly.
What happens: a feature on next sprint's plan is ignored "because YAGNI". Why it is wrong: known needs aren't speculative. Do instead: plan for committed requirements.
What happens: unused frameworks linger "in case". Why it is wrong: carry costs continue forever. Do instead: delete it; version control remembers.
What happens: arguments about future-proofing are opinion against opinion. Why it is wrong: decisions become political. Do instead: discuss build/delay/carry/repair costs and reversibility explicitly.
Seniors use YAGNI selectively: aggressively for internal code, carefully for contracts. The skill is classifying decisions by reversibility and keeping the codebase cheap to change.
| Decision type | My default |
|---|---|
| Internal class or module shape | Build only what is needed |
| Feature flags and configuration | Add when a real variant exists |
| Public HTTP API shape | Design carefully, add versioning |
| Event schemas | Make additive changes possible from day one |
| Security and compliance | Decide upfront |
The Open/Closed Principle says modules should be open for extension but closed for modification: you add new behaviour by adding code, not by editing tested code. In C# this usually means a strategy interface with one implementation per variant, registered in DI, so adding a new payment method is a new class rather than another case in a growing switch. I apply it where change actually happens repeatedly. A small, stable switch (or a switch expression over a closed set) is fine; building extension points for variants that never arrive is speculative generality.
KISS (Keep It Simple) means choosing the simplest design that meets the actual requirements, so the code is easy to read, test and change. The useful distinction is between essential complexity (the problem really is hard, such as tax rules) and accidental complexity (layers, patterns and frameworks we added). I also separate simple (few interleaved concepts) from easy (familiar or quick to write): copying a framework template is easy but may not be simple. In practice it means a plain class before an abstraction, a direct EF Core query before a generic repository, and readable code over clever LINQ.
DRY (from The Pragmatic Programmer) says every piece of knowledge should have one authoritative representation in the system. It is about rules, not text: the GST rate, the definition of an overdue invoice or a validation rule should live in one place. Two blocks of code that look the same but change for different reasons are accidental duplication, and merging them couples unrelated features. I remove true duplication of business knowledge quickly, and I tolerate similar-looking code until a real shared concept appears, usually around the third repetition.
How to tell it (STAR)
We spent [six] weeks on a theming engine for customers we did not have yet. When the second customer arrived they needed something different, and we deleted it. Since then I push for building only for a real ticket, while keeping the code clean enough that adding a variant later is cheap.
Last updated 31 Oct 2026.