ProgrammingIntermediate

Clean Code Principles That Actually Hold Up in Practice

Not every 'clean code' rule ages well. Here's what actually improves real codebases, and what to ignore.

DevFieldGuideJune 24, 2026 (updated July 25, 2026)7 min read
Share:

"Clean code" advice has a reputation problem — some of it is dogma that makes codebases worse. Here's what's actually held up after years of maintaining production systems.

Holds up: name things for the reader, not the writer

js
// Bad — clear to you right now, opaque in six months
const d = calc(u, 0.08);
 
// Good — clear to whoever reads this next, including future you
const totalWithTax = calculateTotal(unitPrice, taxRate);

Names are the cheapest form of documentation you'll ever write. Spend the extra ten seconds.

Holds up: functions should do one thing, but "one thing" is contextual

The rule "functions should be 5 lines long" doesn't hold up — plenty of correct, readable functions are 20-30 lines when they represent one coherent operation. What actually matters: a function shouldn't silently do two unrelated things (e.g., saveUser() that also sends an email as a side effect nobody asked for by name).

Doesn't hold up: avoid comments entirely

"Code should be self-documenting, comments are a code smell" is repeated a lot and is wrong in an important way. Comments explaining what the code does are often redundant. Comments explaining why — a workaround for a browser bug, a business rule that looks arbitrary, a decision that was deliberately made against the "obvious" approach — are some of the highest-value lines in a codebase.

js
// Intentionally not using Promise.all here — one slow request
// shouldn't block the others from completing (see incident #482).
const results = await Promise.allSettled(requests);

Doesn't hold up: DRY at all costs

Premature abstraction to avoid three lines of duplicated code often creates a worse problem: a shared function that two callers depend on for subtly different reasons, which then needs a boolean flag, then another, until it's harder to read than the duplication would have been. Three similar lines in two places is fine. The rule of thumb: wait for a third occurrence before abstracting, and even then, check whether the two use cases are actually the same concept or just visually similar code.

Holds up: consistent formatting via tooling, not review comments

Bikeshedding over tabs vs. spaces or where braces go is a waste of human review time. Configure Prettier/ESLint (or your language's equivalent) and stop having the conversation. This is one of the highest-leverage, lowest-effort "clean code" wins available — it's a one-time setup cost that removes an entire category of code review friction permanently.

Holds up: optimize for deletion, not just addition

Code that's easy to delete cleanly — isolated, without tendrils reaching into unrelated parts of the system — is underrated compared to code that's easy to extend. Features get removed and rewritten far more often than most initial designs account for; a codebase where removing a feature means confidently deleting a folder is in much better shape than one where removal means archaeology to find every place that quietly depends on it.

Doesn't hold up: one class/interface per file, always

Rigidly enforcing "one exported thing per file" as a blanket rule often scatters a handful of genuinely related, small types across many files that would be easier to understand read together. The actual principle worth keeping: group by what changes together and gets read together, not by an arbitrary one-per-file count.

Holds up: make invalid states unrepresentable where the type system allows it

Rather than a single object with several optional fields that only make sense in certain combinations, a type that structurally prevents the invalid combination catches a whole category of bug at compile time instead of relying on every caller remembering a rule:

ts
// Allows invalid combinations the type system can't catch
interface Request {
  status: "loading" | "success" | "error";
  data?: User;
  error?: string;
}
// Nothing stops { status: "success", error: "oops" } from compiling
 
// The same states, structurally impossible to combine wrong
type RequestState =
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; error: string };

This isn't "more clean code dogma" so much as a genuinely different category of technique — a discriminated union like the second version means a caller literally cannot construct the invalid state, rather than merely being told not to in a comment or a runtime check.

Doesn't hold up: premature interfaces for every implementation

"Always code to an interface, not an implementation" made more sense when swapping implementations was a common, anticipated need (multiple database backends, pluggable strategies). Applied reflexively to every class with exactly one implementation and no planned second one, it adds a layer of indirection — jump to definition now lands on an interface, not the actual logic — with no real flexibility gained, since nothing is actually plugging in a second implementation. Add the interface when a second implementation is real, not speculative.

Holds up: early returns over nested conditionals

js
// Nested — harder to hold the conditions in your head
function getDiscount(user) {
  if (user) {
    if (user.isActive) {
      if (user.orders > 10) {
        return 0.1;
      }
    }
  }
  return 0;
}
 
// Early returns — each condition is handled and dismissed
function getDiscount(user) {
  if (!user) return 0;
  if (!user.isActive) return 0;
  if (user.orders <= 10) return 0;
  return 0.1;
}

Both versions are correct. The second reads top-to-bottom as a list of disqualifying conditions, each fully resolved before moving to the next — the first requires holding three levels of nesting in your head simultaneously just to find the one line that matters.

Readable code and efficient code aren't in tension nearly as often as people assume — understanding Big O notation well enough to spot a needless nested loop is itself a form of the same discipline: naming and structuring code so the next reader (including future you) can actually reason about what it does and how it scales.

AspectHolds upDoesn't hold up
NamingName for the reader, not the writerAvoid comments entirely — "why" comments are genuinely valuable
Function sizeOne coherent thing, whatever length that takesA fixed line-count rule ("5 lines max")
DuplicationWait for a third occurrence before abstractingDRY at all costs, even for coincidental similarity
File structureGroup by what changes togetherRigid "one class per file", always

The actual throughline

Every rule above that holds up shares one property: it optimizes for the next person who reads the code, who is very often you in six months with no memory of why you did something. Every rule that doesn't hold up is one that optimizes for an aesthetic ideal instead of that reader's actual experience.

Advertisement

Frequently Asked Questions

Advertisement
DevFieldGuide
DevFieldGuide

Editorial Team

Practical tutorials and developer tools, written and maintained by the DevFieldGuide team.

Enjoyed this article?

Get the next one straight to your inbox, along with the best of what we publish each week.

Related Articles

More in Programming

View all