Clean Code Principles for 2026
Documented, named critiques of Robert C. Martin's Clean Code lineage - the Muratori/Martin performance debate, the Ousterhout/Martin design debate on function length, comments, and TDD - with decision rules for when each side is actually right.
Assume the reader already knows the textbook rules (small functions, intention-revealing names, few comments, one level of abstraction per function). This document is the part that isn't in the textbook: two documented, named, still-live disputes between Clean Code's author and two of its sharpest critics, and the decision rules that fall out of them.
Dispute 1: Function length and "shallow" decomposition (Ousterhout vs Martin)
John Ousterhout (A Philosophy of Software Design) and Robert Martin held a direct, public, written debate, archived at github.com/johnousterhout/aposd-vs-clean-code. It covers three concrete topics - method length, comments, TDD - and neither side backed down. Treat this as two named experts disagreeing, not as a settled question.
Ousterhout's claim: Clean Code's push toward very short methods produces shallow functions - a function whose interface (name, parameters, the fact that a reader must now go find it) costs more attention than the small amount of logic it hides saves. Splitting a function does not reduce complexity, it relocates and multiplies interfaces. His rule, stated directly against Martin's: "first make functions deep, then try to make them short enough to be easily read - don't sacrifice depth for length." A deep function (small interface, substantial functionality behind it, e.g. a well-designed read() syscall) beats five shallow ones that force the reader to jump around to reconstruct one coherent operation.
Decision rule: Before extracting a function, ask whether the extracted piece is independently meaningful (has its own name-worthy concept, will be reused, or is independently testable) or whether it's just relocating five lines behind a new name to satisfy a line-count target. Only the former is a net win. A 25-line function that reads top-to-bottom as one coherent paragraph beats five 5-line functions with names like validateAndPrepare, doInnerCalc, finalizeStep that only ever call each other once, in order.
Dispute 2: Comments (Ousterhout vs Martin)
Same debate thread, unresolved: Clean Code's position is that a comment is a failure to name something well enough - good naming should make most comments unnecessary. Ousterhout's APOSD position is that structured comments carry information code cannot express at all (why a value was chosen, what invariant a caller must uphold, what the interface promises independent of the current implementation) and that a codebase relying purely on naming under-documents its own contracts.
Decision rule: Naming solves "what does this do." It cannot solve "why does this exist," "what did I already rule out," or "what must remain true across all future implementations of this interface." If the information is the second kind, write the comment - do not keep renaming things trying to make the name carry it.
Dispute 3: TDD as a design method (Ousterhout vs Martin)
Ousterhout's argument, from the same debate: TDD's natural unit of work is one test, which is smaller than a natural design unit (a class, a module boundary). Writing the next test that makes you write the next few lines of code optimizes locally and can produce a design nobody actually thought through end-to-end, because no step in the loop asks "does this whole class hang together." This is a critique of TDD as a design technique, not of having tests.
Decision rule: Use TDD for well-understood, narrow units (a pure function, a parser, a bug-fix regression). For anything where the module boundary itself is undecided (a new subsystem, a new tool's data model), sketch the interface first, then backfill tests - do not let the red-green-refactor loop be the mechanism that decides the shape of the boundary.
Dispute 4: Performance (Muratori vs Martin)
Casey Muratori's "Clean Code, Horrible Performance" (computerenhance.com/p/clean-code-horrible-performance, 2023) benchmarked a shape-area calculation two ways: Clean-Code-style polymorphic dispatch (virtual functions / vtable calls) versus a flat switch/union approach. Result: roughly 1.5x slower (24 cycles/shape flat vs 35 cycles/shape polymorphic) in his base case, and he argues the real cost isn't the vtable indirection itself but that the compiler cannot optimize across a virtual call boundary because it doesn't know the concrete type - the loss compounds with every optimization the compiler is blocked from making, not just the dispatch overhead.
Robert Martin responded directly and at length on GitHub (github.com/unclebob/cmuratori-discussion/blob/main/cleancodeqa.md), conceding the specific Clean Code examples "are not efficient at the nanosecond level," and drew this explicit boundary: "If you are trying to squeeze every nanosecond from a battery of GPUs, then clean code may not be for you... if you are trying to squeeze every man-hour of productivity from a software development team, then clean code can be an effective strategy."
A third party, Evan Teran (blog.codef00.com/2023/04/13/casey-muratori-is-wrong-about-clean-code), adds the load-bearing nuance both sides under-state: Muratori's own benchmark code already follows most Clean Code conventions (small functions, clear naming) - the actual variable is specifically virtual-dispatch-based polymorphism, not "clean code" as a whole. Teran's own benchmarks with std::variant show the direction of the effect is compiler- and pattern-dependent (Clang and GCC disagree on which approach wins), which argues for profiling over ideology in either direction.
Decision rule:
| Context | Guidance |
|---|---|
| Hot loop, profiler shows time in a virtual/interface call | Flatten it: switch/tagged-union/std::variant-style dispatch, monomorphize, or devirtualize. Isolate the fast path behind a clearly named boundary. |
| Everything else (the large majority of business logic) | Default to Martin's boundary: optimize for the team's throughput, not for cycles nobody is measuring. Do not pre-flatten abstractions "for speed" without a profiler run backing it up. |
| Reviewing a PR that removes an abstraction "for performance" | Demand the benchmark. Muratori's own point-of-agreement with Martin is that this is an empirical question, not a stylistic one. |
Code Smell Reference
A smell is cheap to fix the day it appears, expensive once other code depends on its shape.
| Smell | Symptom | Usual Fix |
|---|---|---|
| Shallow function | Extracted piece has no independent meaning, is called once, only exists to hit a line-count target | Inline it back; see Dispute 1 |
| Long parameter list | 5+ positional parameters | Parameter object / Builder |
| Shotgun surgery | One conceptual change touches many files | Consolidate responsibility - see SOLID |
| Feature envy | Method uses another object's data more than its own | Move method to the object it envies |
| Primitive obsession | Business concepts represented as raw str/int | Value objects / typed wrappers |
| Comment restating code | # increment i above i += 1 | Delete it; if it needed explaining, rename instead |
| Naming-only documentation of a contract | A public interface with no comment on preconditions/invariants, name alone is expected to carry the contract | Add a structured comment per Dispute 2 - this is not "clutter," it's the part naming cannot do |
When Not to Apply These Rules
| Situation | Recommendation |
|---|---|
| Hot numerical loop with a measured (not assumed) performance requirement | Prioritize the profiler's verdict; isolate and comment the exception |
| Auto-generated code | Do not hand-clean it; fix the generator |
| A module boundary that is still genuinely unknown | Sketch the interface first; don't let TDD's next-test loop decide the architecture (Dispute 3) |
| A public interface whose contract can't be inferred from its signature | Write the structured comment; don't keep renaming to avoid it (Dispute 2) |
Related Documentation
Sources
- Ousterhout/Martin debate transcript -
github.com/johnousterhout/aposd-vs-clean-code- used for Disputes 1, 2, 3 (method length, comments, TDD), including the direct "deep vs shallow" quote. - Casey Muratori, "'Clean' Code, Horrible Performance" -
computerenhance.com/p/clean-code-horrible-performance- benchmark numbers and the compiler-optimization-boundary argument (Dispute 4). - Robert Martin, response to Muratori -
github.com/unclebob/cmuratori-discussion/blob/main/cleancodeqa.md- the "nanoseconds vs man-hours" quote and Martin's concession. - Evan Teran, "Casey Muratori is wrong about clean code (but he's also right)" -
blog.codef00.com/2023/04/13/casey-muratori-is-wrong-about-clean-code- nuance that the villain is virtual dispatch specifically, and compiler-dependentstd::variantbenchmark numbers.