HN Debrief

"Clean" Code, Horrible Performance (2023)

  • Programming
  • Performance
  • Developer Tools
  • Software Engineering

The post revisits Casey Muratori’s critique of Robert Martin’s Clean Code style by walking through a shapes example and showing how common OO advice can turn a straightforward data-and-switch design into code with extra indirection, worse cache behavior, and much lower throughput. The core claim is not just that virtual dispatch is slower, but that several popular habits often travel together: hiding data behind layers, splitting logic into tiny methods, preferring polymorphism over explicit branching, and structuring code around abstract responsibilities instead of the machine’s actual data flow. In the article’s framing, those choices can make code slower by construction, not just in edge cases.

Treat “clean code” rules as local heuristics, not architecture. If a path is hot, design around data layout and control flow early, then spend abstractions only where you have a real change boundary or multiple concrete implementations.

Discussion mood

Mostly negative toward Clean Code as a dogmatic style guide, with a lot of frustration directed at cargo-cult code review culture. At the same time, many commenters defended the underlying maintainability goal and saw the article as proving a tradeoff, not proving that OO or abstraction is inherently wrong.

Key insights

  1. 01

    Interfaces need real change points

    Interfaces are valuable at the seams where code must actually move, not as a default starting shape for every module. The useful framing was a door hinge metaphor: add flexibility where requirements are known to vary, keep the rest rigid, and avoid inventing abstractions before you have at least two concrete use cases because you will design the wrong API against imaginary future needs.

    Do not ask teams to introduce interfaces “for cleanliness” alone. Ask what concrete second implementation, plugin boundary, or deployment seam the interface is buying you today.

      Attribution:
    • josephg #1
    • calvinmorrison #1
  2. 02

    Data layout beats local elegance

    Fast systems are often won or lost by how data moves through memory, not by whether each method looks tidy on its own. Several comments tied the article’s point to cache locality, pointer chasing, and open-world design costs. Once code is split into many heap objects with hidden state, you lose the ability to batch data into arrays, apply whole-program assumptions, and optimize at the macro level.

    For hot paths, review object graphs and allocation patterns before debating naming or method length. If performance matters, prefer representations that keep related data contiguous and make control flow explicit.

      Attribution:
    • josephg #1
    • leecommamichael #1
    • narnarpapadaddy #1
  3. 03

    Inlining logic can improve change safety

    Very small helper functions are not automatically easier to maintain when they are only used once and exist only to satisfy a style rule. In those cases the reader mentally expands them back into one long sequence anyway, then also has to reason about whether the helpers are reused elsewhere and what side effects they hide. A long linear function can be safer to modify because the blast radius is obvious from local reading.

    Before extracting another helper, ask whether it creates a reusable concept or just offloads scrolling. For one-off sequential logic, keeping code together can reduce review and refactor risk.

      Attribution:
    • wren6991 #1
  4. 04

    Polymorphism does not remove complexity

    Replacing a switch with a class hierarchy often moves the same branching complexity around instead of eliminating it. The comment sharpened this with the expression problem. A fixed set of types and a growing set of operations favor one representation, while a growing set of types favors another. Once you need tasks like serialization, the supposedly cleaner object model often drags explicit case analysis back in.

    Match your representation to what changes more often in your product, data variants or operations. Do not sell a polymorphic design internally as “removing conditionals” when it is really redistributing them.

      Attribution:
    • wasmperson #1
  5. 05

    Clean Code examples hide mutation

    The strongest substantive criticism was not just about speed. It was that Martin’s sample style often relies on hidden side effects and stateful class methods where pure functions would be easier to reason about, easier to test, and often easier to optimize. That turns a book about readability into examples that are locally neat but globally hard to trust.

    When choosing between an object method and a pure function, default to the one with explicit inputs and outputs. Hidden mutation should trigger the same scrutiny as an obvious performance smell.

      Attribution:
    • josephg #1 #2

Against the grain

  1. 01

    The article attacks a teaching example

    The most credible pushback was that Muratori targets an illustrative chapter example as if it were a prescription for all production code. Martin’s text, as quoted in the comments, already contrasts procedural and OO representations and says mature programmers sometimes want simple data structures with procedures. That weakens the claim that the book literally mandates the slowest possible form everywhere.

    If you cite this article inside your team, use it to challenge blanket rules, not to claim that every Clean Code recommendation is invalid on its own terms.

      Attribution:
    • Jtsummers #1 #2 #3
  2. 02

    Maintainability often dominates real systems

    In many business systems the expensive part is not dispatch overhead but future change. If requirements mutate constantly, simpler-to-modify code can outperform a micro-optimized design at the company level because developers can ship and safely revise it faster. Several comments also stressed that readable code is what lets you find and optimize the true bottlenecks later, while throughput only matters when the workload is actually heavy enough to expose it.

    Do not generalize game-engine constraints to every service or internal tool. Make expected throughput and change frequency explicit before choosing a style.

      Attribution:
    • taybin #1
    • hyperbolablabla #1
    • TheCoelacanth #1
    • Jtsummers #1
  3. 03

    Modern languages change the cost model

    Some of the performance critique is tied to runtime polymorphism in classic OO environments, not to abstraction in the abstract. Comments pointed to Rust’s monomorphization, Java JIT devirtualization, sealed types, and exhaustive match patterns as ways modern languages recover structure without paying the same penalty in every case. That does not erase data-layout concerns, but it does narrow where this specific criticism lands hardest.

    Re-evaluate style guidance per language and runtime. Advice grounded in C++ or Java from one era may not transfer cleanly to Rust, Go, or a modern JVM.

      Attribution:
    • nylonstrung #1
    • unscaled #1

In plain english

cache locality
A property of code and data layout where related data is stored close together so the CPU can access it efficiently.
devirtualization
A compiler or runtime optimization that turns a virtual method call into a direct call when the concrete target is known.
expression problem
A programming design tradeoff about how to extend a system with new data types or new operations without modifying existing code.
JIT
Just-in-time compilation, where a runtime system compiles code while the program is running to optimize it for actual usage.
monomorphization
A compiler technique that generates specialized concrete versions of generic code so it can run without dynamic type overhead.
OO
Object-oriented programming, a style that organizes software around objects that combine data and behavior.
Rust
A systems programming language focused on performance and memory safety without a garbage collector.
sealed types
Types whose allowed subclasses or variants are explicitly limited, making exhaustive matching and some optimizations easier.
virtual dispatch
A runtime mechanism that picks which method implementation to call based on an object’s actual type.

Reference links

Prior discussions and followups

Referenced essays and technical resources

Books and talks