YAGNI, “You Aren’t Gonna Need It,” has been a core XP principle since the late 90s. The idea is simple: don’t write code for requirements that don’t exist yet. Most of us know it. Most of us violate it anyway.
That was never entirely irrational. Before agentic coding, the cost of retrofitting a poorly-planned system was high, so we hedged. We added nullable fields “just in case,” scaffolded abstractions for hypothetical future use cases, and left TODO stubs that lived in codebases for years. I’ve done plenty of this myself. It was a bet that our own foresight would be cheaper than a future rewrite.
That calculus has changed. In 2026, with agents writing the bulk of implementation code, the cost of adding something later has collapsed. The cost of ambiguity has gone way up.
What Agents Are Bad At
Agents are fast. Given a clear, well-scoped task with clean invariants, a capable agent can produce solid, working code in minutes. Where they struggle is reading intent behind ambiguity.
A nullable field is a contract that says: “this might or might not exist, handle both cases.” An agent reading that contract will, rationally, defend against both. It will add null checks, optional chaining, fallback defaults, and guard clauses throughout the call stack. Not because those are needed today, but because the type signature implies they might be.
This cascades. One Optional<T> at the boundary becomes null checks five layers deep. The agent isn’t confused, it’s responding logically to the signal you gave it. The defensive code isn’t wrong, exactly. It’s the agent being faithful to the contract you encoded. The problem is that speculative nullability spreads across the codebase.
I hit a textbook case of this recently on a client system. A generic Case model had a nullable foreign key to a work order, because some case types have one and some don’t. The actual invariant, that maintenance cases always have a work order, wasn’t enforced anywhere. So every consumer, human or agent, had to null-check the entire chain: case to work order, work order to vendor, vendor to contact. One nullable hop at the boundary, defensive code in every layer below it. The fix wasn’t more null checks. It was modeling the case types explicitly so the invariant lived in the type.
Clean Invariants Are the Lever
The shift I’ve made is being more aggressive about encoding what is actually true right now, not what might be true later.
If a field is always present, make it non-nullable. If a state can’t occur, don’t model it. If a concept doesn’t exist in the system today, don’t create an interface for it. The type system should describe the current world, not hedge against a future one.
This is YAGNI applied at the type level. It’s not a new idea, but it has new consequences when an agent is writing most of the code.
When invariants are clean, agents work from a narrower, more accurate solution space. They don’t generate code paths for states that don’t exist. They don’t build abstractions around possibilities. They solve the actual problem in front of them, which is exactly what I want.
A few concrete patterns that have made a real difference for me:
Replace nullable fields with required ones. If a value isn’t always present, make it always present or model the two states explicitly with a discriminated union or enum. Don’t leave null as a sentinel for “we’ll figure this out later.”
Delete scaffolding with no current consumer. Unused interfaces, abstract base classes with one concrete subclass, generic utility functions solving a problem you don’t have yet. Remove them. When you need them, an agent can regenerate them in seconds.
Use strict types over flexible ones. A string that actually represents a status should be an enum. A Map<string, any> with known keys should be a typed struct. And when a function has more than one expected business outcome, return a union of named variants like CaseCreated | CaseDuplicate | IngestionDisabled instead of a boolean, a None, or a loosely shaped dict. Put fields only on the variants where they’re valid. Flexibility in type signatures is ambiguity to an agent.
Avoid premature layering. An agent handed a three-tier abstraction for a feature that could be a single function will fill all three layers with code. Keep the architecture flat until the complexity earns the layers.
Wait for the second case. The generic abstraction isn’t justified until the second concrete use exists. I recently scoped a refactor that would have introduced a tidy wrapper type across fifteen-plus call sites. Concrete cost, speculative benefit. We wrote the idea down and moved on. If the second case ever shows up, an agent can do that migration in an afternoon.
The New Trade-off
The classic argument against strict YAGNI: “What if we need it later? Refactoring is expensive.” That was true when refactoring meant a developer spending days untangling the system.
With agents, refactoring is cheap. Adding a field, changing a type, introducing an abstraction when it’s actually needed. These are fast operations when the task is well-scoped and the codebase is clean. An agent can handle them. The catch is that last part: when the codebase is clean.
A codebase full of speculative structure is harder to reason about, not easier to extend. It gives agents more surface area to misinterpret, more implicit contracts to honor, more ambiguity to defend against. The hedging that was supposed to make the future easier actually makes it harder.
The right move now is to trust that future-you, working with an agent, can add what future-you actually needs. Current-you should only model what current-you knows to be true.
What This Looks Like in Practice
I’ve written before about encoding business invariants as runnable checks, and this is the same discipline applied one layer down. Types carry the invariants that are true by construction, and the harness verifies the ones that have to happen at runtime.
The signal I look for before an agent session: can I describe the invariants of this system in a few sentences without caveats? If I find myself saying “well, this field is null when X hasn’t happened yet” or “this interface is there for a future integration we haven’t built,” that’s a flag. The codebase is carrying future intent that will cost cycles in the present.
Cleaning that up before the session makes a measurable difference. Making the types honest, deleting the scaffolding. I consistently get tighter, more focused output. Less defensive code. Fewer hallucinated code paths. More of what I actually asked for.
The work is small relative to the payoff. A few hours of cleanup before handing a codebase to an agent pays for itself in the first session, and keeps paying dividends after that.
The Principle Holds
YAGNI was always right. We just had structural incentives to violate it. Agentic coding removes those incentives and adds a new penalty for ignoring it. The principle hasn’t changed, the environment around it has. Pure YAGNI, enforced at the type level, is one of the highest-leverage things I’ve found for improving agent output quality.