AlgoMaster Logo

Clean Code Principles

Medium Priority29 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

Clean code isn't about following style rules until the linter goes quiet. It's about writing code that the next person, often a future version of the author, can read, change, and trust. The principles below are language-agnostic, but the examples are pure Java, and they all sit in the same online-store context used throughout the course. Sibling lessons cover coding standards (the formatting rules), Effective Java tips (the Java-specific design wisdom), common pitfalls, performance, and testing. This lesson is about the everyday craft that makes the rest of those decisions easier.

Meaningful Names

A name's job is to answer three questions in one breath: what is this thing, why does it exist, and how is it used. If a reader has to scroll back to the declaration to remember, the name is too vague.

Reveal intent

A name should communicate why the value exists, not just its type.

Bad:

p is "something with a length and elements," t accumulates "something." A reader has to study the loop to understand it's adding up prices. The variable names carry zero information.

Good:

The names now answer the question by themselves. itemPrices says what the array holds, cartTotal says what the sum represents, and the loop reads like a sentence. The body of the code didn't change. The intent did.

Avoid disinformation

A name disinforms when it suggests something the value doesn't have. customerList for a Set<Customer> lies, because a set doesn't preserve order. accountInfo is vague; accountBalance is precise. Two names that differ by a single letter (l vs 1, O vs 0) hurt the same way.

When the type changes later, change the name with it. A List<Customer> that becomes a Set<Customer> and keeps the customerList name is a small lie that grows into a debugging detour.

Pronounceable and searchable

Code gets discussed aloud. Code gets grepped. Names that fail either test slow the work down.

Single-letter names work for loop indices or short-lived locals (for (int i = 0; i < n; i++)), where the scope is small enough that the name doesn't need to carry meaning. Anywhere else, the cost compounds every time someone reads the code.

Class names are nouns, method names are verbs

A class represents a thing, so its name reads as a noun: Cart, Order, ShippingAddress, WishlistItem. A method does something, so its name reads as a verb or verb phrase: addToCart, calculateTotal, markAsShipped. Boolean methods read as questions: isEmpty, hasDiscount, canShipTo. The convention is small but useful; no need to read the body to know whether cart.total is a getter or an action.

Functions Should Be Small and Do One Thing

A function that doesn't fit on a single screen is probably doing too much. The classic measure is even tighter: a function should do one thing, do it well, and do it only. The reader's brain holds a fixed number of moving parts; a 200-line method burns through that budget before the reader gets to the part that matters.

Bad:

placeOrder does five jobs: validate the cart, compute the subtotal, apply the coupon, apply tax, print the receipt. If a tax rate changes, this method gets edited. If the receipt format changes, this method gets edited. If a coupon code becomes a flat dollar amount instead of a percentage, this method gets edited. Every reason the code might change pulls back to the same 25 lines.

Good:

placeOrder is now five lines, and each line reads as a step in the workflow. Each helper is short, named for what it does, and changes for one reason. A new tax rate touches applyTax alone. A new coupon rule touches applyCoupon alone.

The Single Level of Abstraction rule

A function should mix only one level of detail. The "after" version's placeOrder is at the workflow level: validate, compute, apply, print. None of those lines reach down into loop variables or arithmetic. The helpers operate one level lower, in the world of cart items and prices. Don't blend the two; if the code is about steps in a checkout, it shouldn't also be about for-loops over array indices.

The orchestrator sits at the top, and every helper sits one level down. Each box has one reason to change.

Function Arguments

Fewer arguments make a function easier to call, easier to test, and easier to read. The cost of an argument is high: every caller has to know what to pass, and every test has to construct it.

Argument countVerdictExamples
0 (niladic)Easiest to call and testcart.isEmpty()
1 (monadic)Common and finecart.contains(productId)
2 (dyadic)Acceptable when both feel naturalcart.add(productId, quantity)
3 (triadic)Treat as a smell; refactor when you canorder.ship(address, carrier, options)
4+ (polyadic)Almost always wrong; group into an objectcreateOrder(customer, product, qty, price, ...)

When a function naturally needs more than a few arguments, the arguments probably belong together in their own object.

Bad:

Eight arguments, half of them strings. Two of those strings swapped at the call site and the compiler won't notice. The signature is also fragile: adding a phone number means changing every caller.

Good:

Three arguments, each one a meaningful concept. Adding a phone number is a change to Customer, not a change to every caller of createOrder.

No flag arguments

A boolean argument that switches behaviour inside the function is a sign the function is two functions wearing one signature.

Bad:

Reading printReceipt(order, true) at the call site is a riddle. What does true mean? The only way to know is to open the method.

Good:

Two methods, two intent-revealing names. The call site reads as a sentence: printGiftReceipt(order) instead of printReceipt(order, true). The flag isn't hidden in a boolean; it's hoisted into the method name where the reader can see it.

Avoid output arguments

An "output argument" is one a function modifies to return a result through, instead of returning the result properly. The classic shape: void computeTotal(Cart cart, double[] result). The caller doesn't expect their argument to come back changed, and the bug report is "the cart is corrupted after totalling it."

Both versions produce the same answer. The second one is honest: input goes in, result comes out, the cart is unchanged. The first one mutates a caller-supplied object, and the only way to know is to read the body.

Comments

The best comment is the one nobody has to write because the code already says what the comment would have said. The second-best comment is one that explains why, not what. Bad comments lie, drift, or substitute for clear code.

Comments that explain "what" usually mean the code is unclear

Bad:

The comment exists because the variable names don't. Fix the names and the comment becomes redundant.

Good:

No comment needed. The loop's intent is in the names.

Comments that explain "why"

Some context isn't in the code. The reason for an unusual choice, a workaround for an external system's quirk, a non-obvious business rule: these are worth a sentence.

The comment isn't explaining what the if does; it's explaining why the magic number 200 is there. A future developer wondering "can this drop to $150?" now knows where to look for the policy.

Other comments that add value

  • Legal headers. Copyright notices, license tags at the top of source files.
  • Intent. "Sorted by total descending so the top spenders appear first."
  • Warnings. "Caller must hold the cart lock; this method does not synchronise."
  • TODO / FIXME. Short notes about known-incomplete work, scoped to a specific task.

Comments that add no value

  • Redundant. i++; // increment i
  • Misleading. A comment that's diverged from the code it describes.
  • Out-of-date. Drift is the most common comment failure mode; if a comment can be wrong, it eventually will be.
  • Commented-out code. Version control already keeps history. Old code in the file just confuses readers.
  • Journal entries. "Changed by Alex on 2024-03-12 to fix bug 8421." Use the commit message and the ticket system; the file is for code, not changelogs.

The honest rule is: every comment is a small failure of the code to speak for itself. Sometimes that failure is unavoidable and the comment is worth writing. Often, the comment is a sign that a name should change or a method should split.

Formatting: Vertical and Horizontal Density

Code is read top to bottom. A file should read like a newspaper article: the most important concept high on the page, the detail further down. Related lines stick together; unrelated lines have blank lines between them.

Bad:

Everything is jammed together. The eye can't find the structure: where do the fields end and the constructors start? Where does one method end and the next begin?

Good:

Fields at the top. A blank line between each method. The public API (computeFinalTotal) sits next to the private helper it calls (applyDiscount). The eye lands on each method as a unit.

Variables should be declared close to where they're used. If a method has a local that's only touched in the last five lines, declare it on line six, not line one. The reader's eyes shouldn't bounce up and down to find what a name refers to.

subtotal is declared right before the loop that builds it. shipping is declared on the line that needs it. No scrolling required.

Horizontal density

A line that wraps past 100 characters is a line that fights the reader. The Coding Standards chapter covers the formatting rules in detail; the clean-code principle is just that the reader's working memory is finite. Long expressions belong on multiple lines, with intermediate names if needed.

The second form is a hair longer in line count and dramatically easier to read. Each intermediate name documents what the calculation just produced. The reader doesn't have to re-parse the long expression to verify the math.

Error Handling Touches

The exception-handling section covered the details. For clean code purposes, two principles sit at the surface.

Return values vs exceptions for predictable cases

If a value can legitimately be absent (a coupon code that doesn't exist, a wishlist that's empty), an Optional return type expresses that in the type system. Throwing an exception for a routine "not found" turns regular logic into error handling and forces the caller to wrap every call in a try block.

The second style reads as one logical flow. The first style requires a control-flow gymnastics to recover from a normal outcome.

Don't return null, and don't pass null

null is the universal "no value" sentinel, and every caller has to remember to check for it. Forget once and a NullPointerException appears ten frames downstream. Returning an empty collection, an Optional, or a domain object that represents "no result" is almost always cleaner.

The for-each loop over the empty list does nothing, which is exactly the desired outcome. No null check, no edge case, no NPE. The wishlistOrNull version forces every caller to write the null check, and one missed check is a bug.

Symmetrically, don't pass null into a method that doesn't expect it. A null argument is a programmer error; validate at the boundary with Objects.requireNonNull and fail fast.

DRY: Don't Repeat Yourself

Every piece of knowledge in the system should have one place to live. Two copies of the same logic eventually drift apart, and the bug that follows is "we fixed the tax rate over here but not over there."

Bad:

Three copies of subtotal * 0.08. When the tax rate changes to 8.5%, all three need updating. Miss one and the cart says $129.60 while the order confirmation says $130.20.

Good:

One named constant for the tax rate. One helper for the calculation. Changing the rate is a one-line edit; the three call sites stay correct by construction.

What DRY isn't

Don't get tricked into deduplicating things that only look alike. Two methods that happen to have the same five lines but represent unrelated concepts will eventually diverge, and forcing them to share an abstraction now makes that future change painful. The DRY rule is about repeated knowledge, not repeated typing. Coincidental code repetition is fine; conceptual repetition is the target.

Single Responsibility at the Class Level

A class should have one reason to change. Restated: a class should have one job, and the people asking for changes to that class should all be asking for the same kind of change.

Bad:

Cart here is the cart, the persistence layer, the mailer, and the view layer. Four reasons to change. A database schema migration, a new email template, a new HTML design, or a new pricing rule each pull back to the same class.

Good:

Each class has one job. Cart holds items. CartTotaller computes totals. CartHtmlRenderer renders HTML. Persistence and email would each move into their own classes too. A change to the HTML format touches CartHtmlRenderer and nothing else.

The split isn't more code overall; it's the same code reorganised so each class can change for one reason.

Cohesion and Coupling

These two words name the same idea from opposite sides: cohesion is how strongly the things inside a class belong together; coupling is how tightly a class is tied to other classes. High cohesion is good. Low coupling is good.

Cohesion

A class is cohesive when its methods and fields all serve the same purpose. The "god object" cart above had low cohesion: half its methods touched the items list, half touched a database connection and an SMTP server. After the split, each smaller class is highly cohesive, since every method speaks to its one job.

A practical check: pick any pair of methods in the class. Do they touch the same fields? If yes (most of the time), cohesion is high. If methods cluster into groups that touch disjoint sets of fields, the class is probably two classes in one.

Coupling

A class is loosely coupled when it depends on as little of the outside world as possible. The "after" CartTotaller depends only on Cart and CartItem. It doesn't know about the database, the renderer, or the email service. Replacing Cart's internal storage from an ArrayList to a LinkedHashSet wouldn't affect CartTotaller as long as getItems() still returns an Iterable.

The first picture is a knot: every change risks breaking three other things. The second is a graph of small parts that each do one thing and talk to as little of the rest as they have to.

The Boy Scout Rule

"Leave the campground cleaner than you found it." Applied to code: when a file gets touched, make it slightly cleaner before committing. A confusing name renamed in passing. A 30-line method whose first five lines get extracted into a well-named helper. A magic number turned into a constant. None of these are big enough to justify a separate cleanup task; all of them are easy to do while already in the file with full context.

Two tiny improvements in a single commit. Repeat across a team and a codebase, and after a year the code is meaningfully better without anyone running a "cleanup project."

A note on judgment: the boy scout rule only works when the change is small enough to be obvious. A 50-file rename mixed with a bug fix doesn't help the reviewer; it hides the bug fix. Keep cleanups small, keep them adjacent to the original change, and split them into a separate commit when they grow beyond a handful of lines.

Code Smells to Watch For

A code smell is a surface symptom that points at a deeper problem. None of these are bugs on their own. All of them mean "look closer."

SmellWhat it looks likeWhat it usually means
Long methodA method that scrolls past one screenDoing more than one thing; needs to split
Large classA class with dozens of methods or fieldsMultiple responsibilities; split by job
Primitive obsessionStrings and ints used everywhere instead of typesA missing domain object
Feature envyA method that mostly reads another class's fieldsThe method belongs on the other class
Data clumpsThe same three or four parameters travelling togetherThey belong in their own class
Long parameter listSix-plus parameters on a methodGroup related ones into an object
Shotgun surgeryOne change requires edits in many filesA concept is scattered; pull it together
Switch on typeA switch on a type code instead of polymorphismSubtype the type; let dispatch happen by type

Primitive obsession

When String and int show up where the domain has a concept, the code loses information.

Bad:

The signature can't tell a UK postal code from a US ZIP, and it can't catch passing the customer's name where the city goes (both are strings). At the call site, every argument is a String, and the compiler is no help.

Good:

The types now carry the meaning. A PostalCode can't be accidentally passed where an OrderId belongs. The records are a few lines each and they pay off every time a method takes one of them as an argument. Without records, plain classes with private fields work the same way.

Feature envy

A method has feature envy when it spends more time reading another class's fields than its own.

Bad:

totalForCart reads getItems(), then price and quantity from each item. Three nested reaches into Cart and CartItem. The method clearly wants to live closer to the data.

Good:

The total calculation moved to Cart, where the items live. The per-item arithmetic moved to CartItem, where the price and quantity live. Each method touches only its own fields. Feature envy gone.

Data clumps

When the same group of parameters travels together through method after method, the group is a missing class.

Once the clump becomes a class, methods can also move onto it: address.format(), address.isUkAddress(). The class collects what was scattered.

Putting It Together

These principles aren't independent rules; they reinforce each other. Good names make small functions readable. Small functions make single-responsibility classes natural. Single-responsibility classes are cohesive and loosely coupled. The whole stack stands on each layer below it.

Writing clean code isn't a checklist exercise. The code gets written, something feels off, and the principles provide the vocabulary to name the issue. "This method is doing two things." "This name lies about the type." "These three parameters are a data clump." Once the problem has a name, the fix is usually obvious.

The real measure of clean code is the next person's reaction. If they can read the file, find the change they need to make, make it, and commit without rereading half the codebase, the code is clean.

Quiz

Clean Code Principles Quiz

10 quizzes