If you are a Software Developer of some form or another, chances are that you follow what are considered best practices for "Clean Code"or "Clean Architecture". It's considered generally best practice according to these books to keep functions down to a few lines, ensure classes have exactly one reason to change, and wrap implementation details behind abstract interfaces. It’s an approach designed to isolate responsibilities and keep the long-term cost of software modifications flat.
Yet, as codebases grow under this paradigm, engineers frequently encounter a subtle friction. In the drive to decouple every moving part, applications often accumulate a massive web of boilerplate and multi-layered abstractions. This raises a fundamental question: does hyper-decomposing code actually reduce complexity, or does it simply scatter it across dozens of shallow files, making a single linear operation difficult to follow?
This article revisits the baseline assumptions of Clean Architecture by examining a growing yet subtly different software design philosophy championed by systems engineers and computer science pragmatists. We will explore how different software environments define code quality, look at actual case studies of algorithmic decomposition, and map out alternative patterns like John Ousterhout's "Deep Modules." Along the way, we will examine how our design choices interact with mathematical correctness proofs, functional programming paradigms, and a modern toolchain increasingly driven by automated AI agents.
The bubbles that shape your opinions
The frameworks championed by the "Clean" movement were largely forged in the world of large-scale corporate IT consulting. They were explicitly designed to manage risk in massive organizations where hundreds of engineers with varying levels of experience write code against a single, shared repository.
In a setting like a sprawling insurance platform or a legacy banking app with shifting corporate rules, Clean Architecture serves a useful corporate purpose. It standardizes the file system layout. If every team uses the exact same Controller -> UseCase -> Repository pipeline, developers can move between squads and immediately know where files live.
However, this consulting-driven approach has created an architectural bubble. In major technology companies like Google or Meta, or fast-moving startups scaling to millions of users, Clean Architecture is rarely used. High-performing tech organizations do not scale software systems by adding layers of abstraction inside a single app. They scale by splitting systems into separate, highly focused services. Within those services, engineers write flat, direct code that prioritizes execution speed, clear data paths, and low cognitive overhead over abstract structural purity.
This fundamental disagreement about code layout was spotlighted in a written debate on GitHub between Stanford computer science professor John Ousterhout and Robert C. Martin ("Uncle Bob"). The entire unedited dialogue can be read directly at the official repository: johnousterhout/aposd-vs-clean-code. Ousterhout, the creator of the Tcl/Tk language and log-structured file systems, argued that cutting code into micro-functions does not eliminate complexity—it simply relocates it to the connections between those pieces:
"You recommend decomposing code into much smaller units than I do. You believe that the additional decomposition you recommend makes code easier to understand; I believe that it goes too far and actually makes code more difficult to understand."
— **John Ousterhout, APOSD vs. Clean Code Debate**
The Core Disagreements: Shipped Code vs. Dogmatic Rules
The crux of the GitHub debate centers on a few specific heuristics from Clean Code that have become deeply embedded in developer culture. When pinned down on the practical consequences of these rules, Uncle Bob's defenses highlight the exact points where the "Clean" philosophy slips into over-engineering.
1. The Hostility Toward Comments
Perhaps the most glaring friction point in the debate is the treatment of documentation. Clean Code asserts a highly controversial stance: "Comments are always an apology for unclear code." Uncle Bob argues that if you need a comment, you have failed to express yourself in code, and you should instead refactor and lengthen variable names until the code is completely self-documenting.
Ousterhout countered this by showing that code structure alone cannot explain the "why" behind design decisions. Code can show you what an engine is doing, but it cannot convey the developer's underlying intent, performance constraints, or edge-case reasoning. By treating comments as a failure, Clean Architecture forces teams to write incredibly verbose, winding variable and method names that clutter the screen while still leaving the actual architectural context completely invisible.
2. The Trap of Over-Decomposition: The Prime Number Generator
To see how these styles conflict in practice, look at a classic coding problem discussed extensively in Section 4 of the GitHub debate: a program that generates prime numbers using the Sieve of Eratosthenes.
This example has a famous history. Donald Knuth originally wrote a direct, mathematical implementation. Later, Uncle Bob rewrote it in Clean Code to showcase his decomposition methodology. Finally, Ousterhout dissected both to show where the "Clean" paradigm broke down.
The Uncle Bob Variant: Hyper-Decomposition
To satisfy the rule that every function should do "one thing," Uncle Bob split the core algorithm into a dedicated class (PrimeGenerator) containing a web of fifteen separate private methods. Rather than passing variables explicitly down a stack, these tiny methods operated primarily by updating and reading shared, class-level state variables.
Ousterhout explicitly described this result as "awful" (emphasis his), pointing out that the hyper-decomposed code became highly entangled. Because the math was shattered into micro-methods like crossOutMultiples, determineIterationLimit, and notCrossed, a reader could no longer look at the algorithm in one continuous stream.
Consider this actual code snippet from Uncle Bob's implementation discussed in the repository:
private static boolean isMultipleOfNthPrimeFactor(int candidate, int n) {
return candidate == smallestOddNthMultipleNotLessThanCandidate(candidate, n);
}
Ousterhout pointed out that this type of extreme splitting results in shallow interfaces. The method name smallestOddNthMultipleNotLessThanCandidate is incredibly long, taking up valuable space and cognitive effort to parse, yet the method body does almost no actual work. It is a wrapper around a wrapper. You have to flip constantly between fifteen different functions to trace how a single index pointer is mutated, meaning the structural layout obscures the actual math.
The Ousterhout Approach: The Deep Module
Ousterhout's counter-version consolidates the algorithm back down into a few cohesive, well-documented methods inside a clean interface. Rather than creating a new method for every loop or conditional step, Ousterhout keeps the mathematical sequence unified in a single block.
Complexity is managed not by cutting the file into pieces, but by using Information Hiding: keeping the array filtering hidden inside the class and placing clear, contextual comments above the loops to explain why the iteration limits are bounded by the square root of the target number. The user of the class sees a simple generatePrimes(max) interface, while the developer reads a unified, easily scannable calculation block.
The Mathematical View: Algorithmic Correctness and Local Reasoning
The conflict between Ousterhout and Uncle Bob is not just a matter of aesthetic preference. It mirrors a foundational concept in theoretical computer science: formal verification and correctness proofs.
When pioneers like Edsger Dijkstra and Donald Knuth designed algorithms, they evaluated code based on how reliably a human could prove it mathematically correct. In Hoare logic, proving correctness relies on checking triples written as $P { S } Q$, where $P$ is the precondition, $S$ is the program statement, and $Q$ is the postcondition. For loops, this requires establishing a loop invariant—a logical assertion that remains true before, during, and after every iteration.
To successfully verify a loop invariant, an engineer needs local reasoning. You must be able to look at the variables running through the loop and verify that their transformations preserve the mathematical invariant.
LOCAL REASONING (Knuth / Ousterhout)
[ Explicit Inputs ] ───> [ Unified Functional Block ] ───> [ Explicit Output ]
└─ Loops & Invariants Visible ─┘
EXPLODED STATE SPACE (Uncle Bob)
Method 1 ──> Method 2 ──> Method 3 ──> Method 4 ──> Method 5 ──> Method 6
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
[────────────────────── Shared Class-Level State ──────────────────────────]
This is where Uncle Bob’s hyper-decomposition model fails standard computer science rigor. By splitting the Sieve of Eratosthenes into fifteen separate private methods that interact by mutating shared, class-level variables, he explodes the state space of the program.
Dijkstra famously fought against hidden side effects and implicit global states because they destroy local reasoning. When a loop's conditional logic is fragmented into distinct methods like smallestOddNthMultipleNotLessThanCandidate, the loop invariant is no longer localized within a clear block of code. Instead, the mathematical state is scattered across the entire object container. To prove that the code is correct, you can no longer analyze a single loop sequentially; you have to trace and mathematically verify the state transitions across fifteen separate method boundaries.
By prioritizing a stylistic rule (making functions tiny) over mathematical visibility, Clean Code trades away the exact structural clarity required to verify that an algorithm works correctly. Knuth’s and Ousterhout’s preference for localized, well-commented blocks keeps the execution state visible, allowing developers to reason about invariants without leaving the immediate context.
Bridging the Gap: Functional Core, Imperative Shell
This loss of local reasoning highlights a deeper gap in the "Clean" ideology: an ongoing reliance on 1990s-style, mutable Object-Oriented paradigms. Uncle Bob's method of breaking down functions often assumes that passing arguments down a stack is messy, so he shifts variables into class-level state fields. This choice reveals an aversion to pure functional programming and modern, immutable data structures.
If you want to maintain decoupled architectures in massive enterprise applications without paying Uncle Bob's over-decomposition tax, the modern alternative is the Functional Core / Imperative Shell pattern.
┌────────────────────────────────────────────────────────┐
│ IMPERATIVE SHELL │
│ (Handles Side Effects: HTTP Routers, DB I/O, Logging) │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ FUNCTIONAL CORE │ │
│ │ (Pure Business Logic, Immutable Data) │ │
│ │ [ Inputs ] ───> [ Outputs ] │ │
│ └────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
Instead of scattering business logic across multiple directories of UseCases and Interactors, this approach splits code based on side effects:
- The Functional Core (The Deep Module): This contains your core corporate logic, written entirely as pure, deterministic functions using immutable data structures. Data goes in, calculations happen, and new data comes out. Because there is no internal state mutation, it behaves exactly like Ousterhout's Deep Module—a concentrated block of complex computation hidden behind a predictable interface that is trivially easy to unit test.
- The Imperative Shell: An thin outer wrapper that deals with the messy outside world. It reads from the database, passes raw data into the Functional Core, collects the immutable result, and writes it back to storage.
By separating logic based on mutability rather than folder structures, enterprise systems can remain highly robust and completely isolated from framework changes. You achieve all the testing advantages promised by Clean Architecture, but your business rules stay flat, clear, and highly localized within functional cores that fit easily inside a single file.
What Systems Architects Prioritize
Engineers responsible for building software that runs at global scale generally share Ousterhout's aversion to speculative abstraction. Their design choices are shaped by hardware boundaries and human working memory limits.
Linus Torvalds on the Fragility of Object Models
The creator of Linux and Git places structural focus on data layout rather than trying to hide operations inside layers of polymorphic interfaces:
"Bad programmers worry about the code. Good programmers worry about data structures and their relationships... Inefficient abstracted programming models [mean] two years down the road you notice that some abstraction wasn't very efficient, but now all your code depends on all the nice object models around it, and you cannot fix it without rewriting your app."
John Carmack on the Illusion of Code Cleanliness
The lead architect behind Doom and Quake argues that separating sequential operations into an extensive chain of tiny functions introduces latency and obscures the actual program state:
"If everything is just run out in a 2000-line function, it is obvious which part happens first... It is very easy for frames of operational latency to creep in when operations are done deeply nested in various subsystems... Sometimes, a style gets applied as a matter of course where a performance benefit is negligible, but we still eat the bugs."
The Google Approach to YAGNI
At Google, systems built by engineers like Jeff Dean value simplicity and empirical validation. Creating an extra abstraction layer to protect against a hypothetical future change is viewed as dead weight. Code must be justified by current, verified requirements and performance benchmarks, not speculative future proofing.
Shifting Focus: Deep Modules and Targeted DDD
Moving away from a layered template means shifting focus toward creating Deep Modules. Ousterhout defines a deep module as a component that provides significant functionality behind a very simple, compact interface. A classic file system utility or an image processing library are deep modules: you call a single method like read() or compress(), and the internal code manages the complex performance mechanics without forcing you to interact with the underlying machinery.
CLEAN ARCHITECTURE (Shallow Modules)
Interface ──> UseCase ──> Interactor ──> RepositoryInterface ──> Database
[ High structural complexity, tiny amount of actual logic per file ]
OUSTERHOUT'S IDEAL (Deep Modules)
Simple Interface Surface Area ──────────────────> [ Internal Complex Engine ]
[ A clear entry point hiding a concentrated, concrete implementation ]
Even within Domain-Driven Design (DDD)—a framework frequently cited by advocates of complex design—the core philosophy is highly practical. Eric Evans’ foundational concept is the Bounded Context. He argues that you must choose an architectural style based on the specific problem a given module solves.
If you are writing a core financial ledger where business rules are highly volatile, a multi-layered decoupled approach is justifiable. But if you are writing a high-volume telemetry ingestion worker, you want flat, unencumbered performance. Evans cautioned against building models that are more complex than the actual business problem being solved.
Code That Fits in Your Head
When software is over-decomposed, it places a heavy cognitive burden on the developer. You shouldn't have to open six separate files across four directories just to see how a simple data payload is updated.
The primary goal of software architecture should be Simplicity—writing code that comfortably fits into a developer's working memory. Interfaces and structural layers are useful tools, but they must earn their place by hiding real complexity, not simply because an acronym dictates their existence.
As the tools we use to write and run code continue to advance, we have to look critically at how our design choices should adapt:
- Language Paradigms: Does it make sense to force a rigid, interface-heavy style originally optimized for languages like Java or C# onto dynamic or expressive languages like Python, Go, or TypeScript?
- Modern Toolchain: Many traditional "Clean Code" metrics were created when developers worked in basic text editors. With modern IDEs, instant static analysis, and automated refactoring, do strict limits on file structure and line counts still offer real utility?
- The AI Workspace: As engineering teams integrate AI coding assistants like Claude Code 4.6+ that can instantly scan and modify large context windows, how does our understanding of readability change? Should humans spend less time maintaining boilerplate abstraction layers and focus instead on writing direct, predictable execution paths?
The next time you face pressure to add multiple layers of structural abstraction to a working, readable component, look at how the core systems of the internet are constructed. Avoid the complexity tax. Keep your modules deep, your interfaces simple, and don't build a bridge until you've actually found water.
To listen to both software authors unpack this structural debate in their own words, watch the full John Ousterhout and Robert "Uncle Bob" Martin Discuss Their Software Philosophies video. This follow-up interview offers excellent perspective on the history of their respective careers, how the GitHub repository came together, and what each learned from challenging the other's architectural models.
Top comments (0)