8.6 KiB
DRY, KISS, YAGNI
Three short principles. Often confused. Often applied wrong by AI agents (and humans).
Contents
- DRY: do not duplicate knowledge
- KISS: keep complexity low and local
- YAGNI: avoid speculative configurability
- Ranked list: where AI agents over-engineer
- Self-check for DRY, KISS, YAGNI
DRY — Don't Repeat Yourself
Definition (Hunt & Thomas, The Pragmatic Programmer, verbatim). "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system."
Source: pragprog official DRY excerpt PDF; Wikipedia summary; O'Reilly 97 Things Every Programmer Should Know, Ch. 30.
The misreading
"Don't have any duplicate code." No. Hunt and Thomas frame DRY as duplication "of knowledge, of intent... expressing the same thing in two different places, possibly in two totally different ways." Two functions that look alike but encode different rules are not a DRY violation. One rule expressed in code + database schema + documentation is.
Smells worth flagging
These are textual signals that probably indicate knowledge duplication, but verify the underlying meaning before refactoring:
- Identical token sequence of ≥5 non-trivial lines appearing in ≥2 functions.
- The same regex, SQL fragment, or URL literal repeated in ≥3 sites.
- The same magic number or string repeated ≥3 times outside a constants module.
- The same validation branch (for example, "if value is missing, raise") duplicated across siblings of one module.
The Rule of 3 — wait for the third occurrence
Don't extract an abstraction the first time you see duplication. Don't extract on the second. Wait for the third — by then you have enough signal about the real shape of the shared knowledge to abstract correctly. The Rule of 3 is folklore from refactoring practice; the underlying principle (don't abstract too early) is in Fowler's Refactoring (refactoring.com/catalog) and in the wrong-abstraction work below.
The Sandi Metz corollary — wrong abstraction is worse than duplication
From Sandi Metz, "The Wrong Abstraction" (Jan 2016): "duplication is far cheaper than the wrong abstraction."
If an abstraction has accumulated per-caller branches and special cases, it is the wrong abstraction. The remedy:
- Re-inline the abstraction back into each caller.
- Delete the per-caller dead branches.
- Live with honest duplication for a while.
- Re-abstract only when the real shared knowledge becomes obvious.
Rule. Do not introduce an abstraction to eliminate three lines of duplication unless you can name the underlying knowledge the lines represent. If you can't name it, leave the duplication.
KISS — Keep It Simple, Stupid
Origin. Coined by Clarence "Kelly" Johnson at Lockheed's Skunk Works (U-2, SR-71). His designers were handed a basic toolkit; the aircraft had to be repairable by an average mechanic in a combat field with only those tools. The "stupid" refers to the mismatch between break-conditions and repair sophistication — not to the engineer.
Original phrasing: "Keep it simple stupid" (no comma). First documented use by the U.S. Navy in 1960.
Source: Wikipedia, KISS principle; Braithwaite background story.
Operationalizing KISS for code review
KISS is fuzzy without numbers. Use these as defaults:
- Cognitive Complexity ≤10 per function. SonarSource's Cognitive Complexity metric measures how hard the code is to understand rather than the count of independent paths. It's the dominant 2026 metric — adopted by SonarQube, Biome, and ReSharper. Target <7 for greenfield code; <15 is the upper bound before refactor is mandatory.
- Cyclomatic complexity ≤10 per function. McCabe's original 1976 metric, still useful as a structural floor. 11–20 is moderate risk; 21–50 is high risk; >50 is untestable. Source: McCabe NIST 235r; JetBrains ReSharper threshold guidance. Use Cognitive Complexity for human-readability judgement; use cyclomatic for "is this testable" judgement. When they disagree, prefer Cognitive Complexity.
- Nesting depth ≤5. Source: Aivosto, Project Metrics: Complexity.
- Function length: no canonical absolute, but common static-analysis defaults (SonarQube, PMD) flag >50–60 lines. Pair with complexity ceiling rather than relying on LOC alone.
Self-check
When you see a function exceed cyclo 10 or nest depth 5, refactor before finishing — not "later." Extract a helper, replace nested if/else with early returns or a lookup table.
YAGNI — You Aren't Gonna Need It
Canonical reference. Martin Fowler, bliki: Yagni (May 2015). "A mantra from ExtremeProgramming... capabilities we presume our software needs in the future should not be built now because 'you aren't gonna need it.'"
Fowler's four cost categories
When you build a presumptive feature, you pay:
- Cost of build. Analysis, coding, testing of a feature that ends up unused.
- Cost of delay. Opportunity cost — revenue-generating work you didn't do instead.
- Cost of carry. Added complexity makes every future modification and debug slower.
- Cost of repair. When the presumed feature turns out to be wrong, you pay to rip it out plus the technical debt accumulated against it.
Source: martinfowler.com/bliki/Yagni.html; InfoQ summary.
AI-specific YAGNI traps
LLMs over-produce speculative surface area. These are the patterns to spot:
- Config flags / env vars nobody asked for.
enable_x_v2,legacy_mode, toggles for a single code path that has no second variant. - Plugin / strategy systems for 2 known cases. Registry + base class + 2 subclasses where a direct conditional is shorter and clearer.
- Generic helpers with one caller.
normalizeAnything(value, strict=false, mode="default")invoked from exactly one site. - Optional parameters never passed.
send(value, retries=3, backoff=null, jitter=false, logger=null)where every call site uses defaults. Delete them until a real caller exists. - Speculative async / batching / caching. Async wrappers, queues, and batch endpoints where current load is single-digit RPS.
- Premature interfaces/protocols with one implementation.
FooRepositorypaired with one concreteSqlFooRepository. Inline until you have a second implementation.
Self-check
For every parameter, class, file, or abstraction you introduce, answer: who calls this today? If the answer is "nobody yet," delete it.
Ranked list — where AI agents over-engineer
By frequency observed (engineering-blog-grade, not from a controlled study):
- Premature interfaces/protocols with one implementation.
- Factory classes for trivial constructors —
UserFactory.create(...)wrappingUser(...). - DI containers in small apps — wiring frameworks for 3–5 services where explicit construction in
main()is shorter and traceable. - Try/catch wrappers that change nothing — adds lines, hides tracebacks.
- Speculative config surface — settings objects with 15 fields where 3 are read.
- Plugin / registry scaffolding for two cases.
utils.py/common.pymodules — magnets for unrelated functions; violate DRY's "single authoritative representation" by location.- Re-implementing what the platform already gives you — custom retry loops, enums, or record-like types the standard library provides; hand-rolled validation a database/schema constraint, the type system, or a framework's declarative rule would enforce; a native platform feature replaced by hand-written app code. Prefer the cheapest existing solution: stdlib or native feature over new code, a declarative constraint over an imperative check. The flip side is mode 15 in ai-failure-modes.md — don't reach for a new dependency for what a few lines already cover either.
- Excessive layering (Controller → Service → Manager → Repository) for CRUD — four files to read one row.
- Wrapping libraries "to make them swappable" — thin pass-through adapters around an HTTP, database, or SDK client you will never swap.
Self-check for DRY, KISS, YAGNI
Before you ship code:
- (DRY) Did you eliminate duplication of knowledge, or just duplication of text? Can you name the underlying rule?
- (DRY/Metz) If you introduced an abstraction, are there at least two callers today whose code is structurally identical? Or is the abstraction speculative?
- (KISS) Any function over cyclomatic 10 or nest depth 5?
- (YAGNI) Any optional parameter, config flag, env var, interface, factory, or base class without a caller using it today?
- (YAGNI) Did you wrap a library to "make it swappable"? Delete the wrapper.