YAGNI Is Not a Law: The Hidden Cost of Being Too Simple

YAGNI Is Not a Law: The Hidden Cost of Being Too Simple

The YAGNI debate has swung too far. Discover why over-engineering and under-engineering both hurt, and how to find the balance in modern architecture.

There’s a special kind of irony in watching a team spend more time debating whether they’ll need a feature than it would take to build the feature twice. The YAGNI principle, You Aren’t Gonna Need It, has become a shield against over-engineering, but it’s increasingly being used as a cudgel against actual thought. Somewhere between “build the smallest thing” and “plan for everything”, most teams have lost the plot entirely.

The dirty secret? Both extremes are expensive. The team that refuses to build anything until a customer demands it is just as likely to hit a wall as the team that spins up four microservices for a todo app.

The Definition Nobody Reads

Let’s start with what YAGNI actually means. The principle, which originated with Extreme Programming, says you shouldn’t add functionality until it’s actually needed. The Design Encyclopedia’s definition of overengineering gets at the same idea: designing products with “more complexity, features, or components than are necessary for their intended purpose.”

But here’s the nuance that gets lost in every agile ceremony: “necessary for the intended purpose” is doing a lot of heavy lifting. What’s the intended purpose? If you’re building an app that might need high availability in six months, is it over-engineering to design for that now? Or prudent planning?

The answer is: it depends on things you probably don’t know yet.

The Real Cost of Over-Engineering

When teams violate YAGNI, the pattern is almost always the same. Someone reads a blog post about microservices. Someone else watched a conference talk about event-driven architecture. Suddenly the team is building a distributed system with message queues, saga patterns, and eventual consistency for an application that could run fine on a single server.

The Clean Architecture Solution Template for .NET post makes a telling observation about this. Milan Jovanović, who’s built thousands of production .NET applications, notes that four projects is the common baseline: Domain, Application, Infrastructure, and a Presentation project like an API. The key quote?

“More than that is usually over-engineering for a single deployable.”

The “single deployable” part matters. A five-project solution with Domain, Application, Infrastructure, Persistence, and Api projects has a specific project reference graph that must be maintained:

If you’re building a true multi-deployable system with actual scaling requirements, the complexity might be justified. But the moment you’re adding abstractions “just in case” you need them, you’re not architecting, you’re decorating.

Look at the reference graph for a Clean Architecture solution:

Project reference graph for the solution: Api references Application, Infrastructure, and Persistence, Infrastructure and Persistence reference Application, Application references Domain, Domain references nothing
The dependency graph for a Clean Architecture solution, showing how each project references only the layers below it.

That’s already a significant amount of indirection. Every additional project isn’t just a folder on disk, it’s a compile-time dependency, a namespace boundary, a set of conventions developers must learn, and a place for bugs to hide.

The Other Side Nobody Discusses

Here’s where the YAGNI conversation gets uncomfortable: Larry Garfield’s recent post on under-engineering makes a devastating case against the “smallest thing that works” philosophy. His MiDy project went through eight rewrites because he kept trying to avoid complexity that was actually necessary.

The pattern was predictable. He started with a basic file-to-URL mapping because anything more seemed like over-engineering. Then he needed search functionality, which required an index. Then the index required a database. Then the raw PDO approach required a query builder. Each “Keep It Simple” decision made the next iteration more complex because he was building on a foundation that couldn’t support the actual requirements.

His conclusion is worth quoting: “I had to realize far earlier that an indexable cache was mandatory (and that SQLite was the obvious choice), and that a query builder could not be avoided. Every attempt to Keep It Simple made it more complex.”

The next sentence is the devastating one: “Months of coding saved me hours of planning.”

The Inconvenient Truth About YAGNI

Here’s the thing nobody wants to say: YAGNI works when you’re operating in a domain you partially understand, building for requirements that are still emerging, and you can iterate quickly. It fails catastrophically when you’re working in a domain you understand well and the requirements are knowable.

The problem isn’t the principle, it’s the application. Too many teams treat YAGNI as a moral law rather than a risk management tool. They confuse “we don’t know what we need” with “we don’t need to plan.”

The 7 Architectural Sins talk that’s been circulating touches on why this keeps happening. The sin isn’t adding abstractions, it’s adding abstractions without understanding the tradeoffs.

A Pattern for Actually Getting This Right

Let’s get practical about this. The clean architecture template’s dependency rule is actually a good model for thinking about YAGNI, not because it’s the only architecture, but because it forces explicit decisions about boundaries.

Here’s a framework that actually works:

First, know your domain. If you’re building a CMS, you already know you’ll need search capability. That’s not a “you aren’t gonna need it” situation, that’s a “build it before you need it” situation. Garfield’s mistake wasn’t following YAGNI, it was ignoring what the domain required.

Second, distinguish between speculative features and domain fundamentals. A microservices architecture for a small team is speculative infrastructure. A proper abstraction boundary between business logic and infrastructure is a domain fundamental. One is YAGNI violation, the other is architecture.

Third, think about replacement cost. Here’s the real test: if you don’t build X now, and it turns out you need it, how much does it cost to add later? If it takes you eight rewrites like it did for Garfield, that’s not YAGNI, that’s denial.

The teams that navigate this well establish a feedback loop where architectural decisions get re-evaluated as the system evolves, rather than treated as one-time choices carved in stone. They treat the infrastructure (and the related documentation) as something that can and should change.

Where the Real Complexity Hides

Part of the problem is that our industry has an abstraction fetish. We’ve convinced ourselves that more layers means better design, when often it means more indirection for no purpose. What started as a noble goal, separating concerns, has become an arms race of dependencies where full-stack engineers spend more time in configuration files than in business logic.

Take a look at the actual Clean Architecture template implementation. The base Entity and AggregateRoot classes show a pattern that most teams need:

public abstract class Entity
{
    public Guid Id { get; protected init; }
}

public abstract class AggregateRoot : Entity
{
    private readonly List<IDomainEvent> _domainEvents = [];

    public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents;

    protected void RaiseDomainEvent(IDomainEvent domainEvent) =>
        _domainEvents.Add(domainEvent);

    public void ClearDomainEvents() => _domainEvents.Clear();
}

public interface IDomainEvent;

The IDomainEvent interface is a plain marker, no MediatR dependency in the Domain project. That’s a pragmatic YAGNI decision. It could have been coupled to MediatR’s notification interface, but that creates a dependency on a library in the innermost layer. It could have been a DDD AggregateRoot with all the event-sourcing machinery, but that would be speculative infrastructure.

The same pragmatic thinking applies to the application layer’s DI registration:

public static IServiceCollection AddApplication(
    this IServiceCollection services)
{
    var assembly = typeof(DependencyInjection).Assembly;

    services.AddMediatR(config =>
    {
        config.RegisterServicesFromAssembly(assembly);
        config.AddOpenBehavior(typeof(ValidationBehavior<,>));
        config.AddOpenBehavior(typeof(LoggingBehavior<,>));
    });

    services.AddValidatorsFromAssembly(assembly);

    return services;
}

That’s it. One extension method per project, clear registration, no magic. The application layer handles use cases and validation, it doesn’t care where the data comes from.

The Anti-Patterns That Keep Happening

Let me check off the ones I still see in production codebases:

The Distributed Monolith That Thinks It’s Microservices. You see a deployment diagram with six boxes and the team says “we’re resilient.” The reality is they’ve created what is effectively a monolithic application that requires six separate deployments with orchestration, message brokers, and distributed traces, just to handle the same request flow that would be one database transaction in a modular monolith.

The Full DDD Onboarding for a CRUD App. The team creates aggregate roots, value objects, and domain events for operations that are straightforward database reads and writes. The domain complexity doesn’t exist yet, and might never exist, but the infrastructural complexity is already there, slowing down every single feature.

The Framework-Driven Architecture. The tooling decisions come before the problem statement. Teams build around an event bus, an orchestrator, a streaming platform, or a work queue because it’s trendy, not because the requirements demand it.

The “Everything Has to Be Eventually Consistent” team. This one masks itself as “planning for scale.” The team embraces distributed transactions, outboxes, and eventual consistency even though their peak load is 100 concurrent requests on one database that’s barely using 2% of its capacity.

The Behavior Analogy That Actually Works

Think about it like this: YAGNI is like trying to write a perfect grocery list for the next month. If you’ve never cooked before, you don’t know what you’ll need, so you buy a few basics and make multiple trips. That’s YAGNI. But if you’ve been cooking for years, you know you always use olive oil and garlic. Not buying them because “maybe I won’t need them” isn’t simplicity, it’s negligence.

The problem is that too many teams treat all requirements as unknowable, when in reality many requirements are quite predictable. A CMS will need search. An e-commerce platform will need inventory management. A payments system will need audit trails. These aren’t speculative, they’re fundamentals of the domain.

The Path Forward

The way out of this mess isn’t more principles, it’s more thinking.

When you’re about to reject an architectural decision using YAGNI, ask yourself: “Am I rejecting this because it’s genuinely unnecessary, or because I’m avoiding the effort of learning something that would help me understand the problem better?”

When you’re about to adopt a complex architecture, ask yourself: “What concrete requirement does this solve today?” If the answer is “none”, it’s probably over-engineering.

When you’re planning a project, spend the time to understand what the domain actually requires. That planning, the kind of architecture that thinking through a design before coding, is what saves you from both under- and over-engineering. It’s a skill worth honing, even if it doesn’t feel as productive as typing.

For teams worried about the long-term trajectory of their codebase, the long-term consequences of prioritizing speed over sustainable architecture are real and documented. And for anyone building documentation or looking at low-code workflow tools, the same pattern applies: introducing heavyweight tools too early creates documentation debt and complexity, just as much as it does in code.

At the end of the day, good architecture isn’t about following rules, it’s about making conscious trade-offs with your eyes open. YAGNI isn’t wrong. But neither is planning. The key is knowing which one you’re actually doing, and being honest about the costs of both.

Share:

Related Articles