Clean Code Principles
"Clean code" has become a vague phrase that means different things to different people. The useful version is concrete: code is clean when the next person to read it can understand what it does and modify it without surprise. Most "clean code" advice that survives contact with real codebases reduces to that test.
This page is about the principles that actually move the needle, the trade-offs that come up, and the rules-of-thumb that sound good in books but hurt in practice.
What clean code actually optimizes for
Three orthogonal dimensions:
1. **Readability** — can a new reader understand it without context that lives only in the author's head?
2. **Modifiability** — can the next change be made locally, without cascading effects?
3. **Discoverability** — can someone find the right place to make a change?
A piece of code that scores well on all three is "clean." Optimizing one at the expense of the others — say, extreme function-extraction for readability that hurts modifiability — is not.
The principles that work
Names that describe behavior
A function named `processData` is opaque. A function named `validateAndStoreOrder` says what it does. Naming is the highest-leverage form of documentation; it is the thing every reader sees first.
Specific rules:
- **Functions name the action**: `validateOrder`, not `orderValidator`
- **Variables name the data**: `pendingOrders`, not `data`
- **Booleans phrase as questions**: `isReady`, `hasPermission`
- **No abbreviations** except universally-known ones (`url`, `id`)
Functions do one thing
The "do one thing" rule is correct but often misapplied. The right form: a function should operate at one level of abstraction. A function that orchestrates ("validate, then store, then notify") is one thing at that level. A function that does that orchestration *and* the validation logic inline is two things.
The test: can you describe what this function does in one sentence without the word "and"? If not, split.
Magic numbers and strings get names
`if (status === 3)` is opaque. `if (status === STATUS_APPROVED)` is documenting itself. The constant has a name that explains the meaning.
This applies to URLs, configuration values, error codes, and any other "literal that means something."
Local consistency over global consistency
When a codebase has a pattern, follow it, even if it is not your preferred pattern. Mixing styles within a file is worse than either style consistently applied. Save style fights for new code, not existing code.
Errors are not exceptional cases handled at the bottom
Errors are part of the function's contract. Where errors come from, what they mean, what to do with them — these are first-class design decisions. Burying error handling in a try/catch at the bottom of a function defeats the purpose of typed errors and structured responses.
The principles that have aged badly
"Functions should be no more than 5 lines"
Sometimes correct, often arbitrary. A 5-line function that consists entirely of well-named single-step calls is fine. A 30-line function that does one coherent thing at one level of abstraction is also fine. The line count is not the right metric.
"Comments are a sign of bad code"
False at the extremes. Comments that explain *why* (a hidden constraint, a tricky invariant, a non-obvious design decision) are exactly the comments that age well. Comments that explain *what* (restating the code in English) usually indicate the code itself could be clearer.
"Don't Repeat Yourself" applied to anything that looks similar
The DRY principle gets misapplied. Extracting a "shared" abstraction from two pieces of code that incidentally look similar but represent different concepts couples them in a way that hurts both. The right test for shared abstraction is shared *meaning*, not shared *form*.
"Use design patterns wherever possible"
Patterns are vocabulary, not goals. Using a Visitor pattern when a switch statement would be clearer is the bad outcome. Apply patterns when the problem they solve actually exists.
Specific patterns that earn their place
Early returns instead of nested conditions
```
// less clean
function approve(order) {
if (order.valid) {
if (!order.locked) {
// ... actual logic
}
}
}
// cleaner
function approve(order) {
if (!order.valid) return;
if (order.locked) return;
// ... actual logic
}
```
The early-return version is flatter and easier to read.
Replacing booleans with named enums
A function with three boolean parameters has 8 possible callsite combinations, most of them meaningless. Named enums or distinct method names make the valid combinations obvious.
Pure functions where practical
A function that takes inputs and returns outputs without side effects is dramatically easier to test, reason about, and refactor. Where you can write a function as pure, it is almost always better than the side-effecting alternative.
The trade-offs that come up
Inline vs. extracted
Extract too much and the code becomes a tangle of one-line functions calling other one-line functions. Extract too little and complexity accumulates in single places. The right answer is local: extract when the named function adds clarity, inline when the inline form is plainly visible.
Abstraction now vs. later
Premature abstraction is more harmful than too-late abstraction. Three duplicated lines is fine. Five very-similar functions probably shares an abstraction. Wait for the third instance to abstract; the first two often turn out to be different in ways that matter.
Defensive coding vs. trusting the contract
Validating every input at every level produces noise. Trusting the contract at internal boundaries produces clearer code. The line: validate at trust boundaries (user input, external APIs); trust within the system.
Common failure patterns
- **"Clean" as cargo cult.** Following rules without understanding what they optimize for.
- **Refactoring for cleanliness without changing behavior.** A real win is when refactoring uncovers and fixes a latent bug; pure cosmetic changes have lower value.
- **Treating clean code as the primary goal.** Code is clean *for a reason* — to enable the next change. Cleanliness in service of nothing is decoration.
- **Premature DRY.** Extracting "shared" abstractions from things that turn out to be different.
- **Over-using inheritance for "shared" behavior.** Composition is usually clearer; inheritance is rarely the right answer outside specific cases.
Further Reading
- [RefactoringStrategies](RefactoringStrategies) — How to make clean-code-style changes safely
- [TechnicalDebtManagement](TechnicalDebtManagement) — Where uncleanliness compounds
- [CodeDocumentationBestPractices](CodeDocumentationBestPractices) — Comments that earn their place
- [DebuggingStrategies](DebuggingStrategies) — When unclean code makes debugging harder
- [LegacyCodeModernization](LegacyCodeModernization) — Cleaning up code you did not write
- [SoftwareEngineeringPractices Hub](SoftwareEngineeringPracticesHub) — Cluster index