Clean Code
Overview
Clean Code is the set of practices for writing readable code — clear names, focused functions, absence of surprise.
The central principle is sound: code is read many more times than it is written, and optimizing for reading pays more than optimizing for writing.
Several of the movement's specific rules, however, produce worse code when followed literally. This document covers both things.
Problem
Readability is treated as aesthetic preference, which prevents productive discussion. "I find this more readable" against "I find that more readable" has no resolution.
The way out is to reframe the question: readability is not a property of the code, it is a relationship between the code and whoever reads it. The operational question becomes how much effort does someone need to understand this well enough to change it safely?
That is observable. One new person reading the code and narrating what they understand reveals more than any debate about style.
Core Concepts
Names
The highest-return item, by a wide margin.
A good name makes the comment unnecessary. A bad name forces the reader to keep a translation in mind throughout the reading.
Three rules that hold up:
- The name reveals intent, not implementation.
delinquentCustomersrather thanfilteredList. - Length follows scope. A loop index can be
i; a class field used in twenty places cannot. - The vocabulary is the domain's. If the business says "policy", the code says
policy. See ubiquitous language.
Functions
A function should operate at a single level of abstraction. Mixing "calculate the amount due" with "format the date for the file's standard" forces the reader to switch levels midway.
The popular rule — "functions should be at most five lines" — does not hold up. Extracting aggressively produces the opposite problem: to understand a flow, the reader jumps through ten three-line functions, and the logic ends up living nowhere.
The useful criterion is not size. It is whether the function tells a coherent story at a single level.
Comments
Clean Code's strong formulation — "a comment is a sign of failure" — is partly wrong.
Comments that explain what the code does are indeed redundancy that ages. But comments that explain why carry information the code cannot carry:
❌ // increment the counter
counter++;
✅ // The provider returns 429 with no Retry-After on this endpoint.
// 800 ms was determined empirically; below that the rejection
// rate starts climbing again.
wait(800);
The second is not a failure. It is the only way to record that.
Surprise is the real cost
Code that does something beyond what its name promises is the most expensive of
all, because the reader does not know they need to investigate. A validateOrder
function that also writes to the database breaks trust in every other name in the
system.
Mental Model
Write for whoever will read it a year from now with no context. That person is frequently you.
When to Use
- Where reading is frequent and change is expensive — business rules, tests, code other people will maintain.
- With more rigour where the business logic lives — that is where reading is most frequent and error most expensive.
When Not to Use
As a literal rule about size. Extracting until everything is three lines makes readability worse.
When it conflicts with demonstrably critical performance. On hot paths, less elegant and faster code can be the right choice — with a comment explaining why and the measurement that justified it.
In generated or throwaway code. A one-off migration, an analysis script, a prototype.
As an appeal to authority in review. "That's not clean code" is not a critique until you point at what becomes harder to understand or change.
When the pursuit of purity becomes the work. Refactoring names indefinitely in stable code is cost with no return.
Alternatives
- Tests as documentation — a well-named test communicates intent better than many comments.
- Expressive types — a
NationalIdtype communicates more than a well-namedString. - Design heuristics — more structural criteria, less open to interpretation.
Trade-offs
| More extraction and ceremony | Less |
|---|---|
| Each piece is simple | Bigger pieces |
| Names document the flow | Flow explicit and linear |
| Many jumps to follow the logic | Logic in one place |
| Easy to test in parts | Coarser tests |
Failure Modes
Excessive fragmentation. Tiny functions always called in sequence, understanding which requires opening all of them.
A name that lies. Describes something other than what the code does. The worst case.
Outdated comment. Describes the code from two years ago and is believed.
Abstraction for readability's sake. Extracting a class to "improve the name" when a well-named variable would do.
Common Mistakes
Treating the rules as literal. Especially the one about function size.
Eliminating all comments. The "why" ones are irreplaceable.
Discussing style instead of reading effort. The question is what becomes harder to understand, not what is more pleasing.
Applying the same rigour everywhere. Domain code deserves more than configuration code.
Real-World Example
A code review asked for a 40-line function to be extracted into eight smaller ones. The author disagreed; the discussion stalled on preference.
The criterion that resolved it: they asked someone who did not know the code to read each version and explain what it did.
On the 40-line version, they took three minutes and got it right. On the extracted version, they took seven and got the order of two steps wrong, because the function names did not indicate sequence.
The final version ended up with three functions, not eight and not one — separating the three levels of abstraction that genuinely existed: fetch the data, apply the rule, persist the result.
What resolved it was not the rule. It was measuring the reading effort of someone with no context.
The rigour varies with the code
Applying the same readability standard across the whole system is waste on one side and negligence on the other.
| Kind of code | Rigour | Why |
|---|---|---|
| Business rules | Maximum | Read frequently, changed by many people, error expensive |
| Infrastructure adapter | Medium | Changes little once stabilized |
| Configuration and wiring | Low | Rarely read, changed mechanically |
| Tests | High | Executable documentation; an unreadable test is not used as reference |
| Migration, one-off script | None | Short life, one consumer |
| Performance-critical path | Special | Clarity yields to measurement, with a comment justifying it |
The test row tends to surprise. Teams invest in production readability and accept tests with duplicated setup and generic names — when the test is precisely what the next person reads to understand the code's intent.
Related Concepts
- Code Smells — the signs that something needs attention.
- Refactoring — how to change without breaking.
- Design Heuristics — more structural criteria.
- Abstraction — when extracting pays off.
Practical Exercise
Pick a file in your system and ask someone who does not know it to read it and narrate what they understand.
Note where the person hesitates, backtracks, or asks. Those points are the real readability problems — and they rarely coincide with what a style review would flag.
Interview Questions
- How do you assess readability in a way two people would agree on?
- What kind of comment is worth keeping?
- When does extracting a function make the code worse?
Further Exploration
- Martin, Robert C. Clean Code. Prentice Hall, 2008.
- Ousterhout, John. A Philosophy of Software Design. Yaknyam Press, 2018 — disagrees with Clean Code on several points, and is worth reading alongside.
- Beck, Kent. Implementation Patterns. Addison-Wesley, 2007.