Summary: Clean-code advice has a credibility problem: much of it is repeated because it is repeatable, not because it survived contact with production. These ten rules are offered as the residue — the ones the author broke and paid for. What makes them durable is that each has a reviewable test attached, a question you can ask at a pull request rather than a principle you can only nod at.

The framing matters. This is explicitly a follow-up to the observation that some clean-code rules do more harm than good; the question it answers is if those rules fail, what do you keep? That is the same spirit as Become a better Developer with 10 principles, but pitched at the level of a single diff.

1. A name should explain why the value exists

The type already says what a variable holds. A list of users is a list of users. The name's remaining job is to say why this one matters here.

List<User> list = repo.findAll();
for (User u : list) {
    if (u.getD() != null && u.getS() == 1) {
        send(u);
    }
}

You cannot review that without opening the User class to learn what getS() == 1 means. Move the meaning into the names, and hide the condition behind a method that states what it checks:

List<User> activeSubscribers = repo.findActiveSubscribers();
for (User subscriber : activeSubscribers) {
    notifyRenewal(subscriber);
}

The test: if a name needs a comment, the name is wrong.

2. Return early and keep the main path flat

Deep nesting is not a style preference. Each level is one more condition the reader must hold in their head simultaneously. By the fourth, nobody knows which else belongs to which if — the author has watched senior engineers count braces with a finger.

Guard clauses that exit on the exceptional cases leave the main path at one indentation level, where it reads as a sequence rather than a maze.

3. One method should work at one level of detail

The tiring thing is not length, it is the altitude change. A method that runs a summation loop and then calls a payment gateway forces the reader to switch scales mid-sentence.

public void checkout(Cart cart) {
    Money total = priceOf(cart);
    chargeCustomer(cart, total);
    sendConfirmation(cart, total);
}

Three lines, three steps, all at the same level. The details still exist one level down, available when you want them.

The test: read the method aloud. If you say "and then inside that loop", split it.

4. Build objects that cannot hold broken data

Validation scattered across the codebase drifts apart. One service checks the email format; a batch job written next year does not. Put the check in the single place the object is constructed, and no caller can skip it. An object that can only exist in a valid state removes an entire category of defensive checks from everywhere else — and removes the question "has this been validated yet?" from every review.

5. Comments should explain why, not what

A comment restating the code is a second thing to keep in sync, and it is always the one that rots. Comments earn their place when they record the reason a non-obvious decision was made — the constraint, the bug this works around, the thing the next reader will otherwise try to "fix."

6. An empty catch block is a bug you have not found yet

Swallowing an exception buys quiet now and costs a long debugging session later. The system keeps running, the data is wrong, and nothing in the logs says so.

try {
    updateInventory(order);
} catch (Exception e) {
    // ignore
}

That block turns a failed stock update into a successful checkout, and the customer buys an item that does not exist. The fix catches the exception you actually expect, logs the identifier you will search for, and passes the original along so the stack trace survives:

} catch (InventoryUnavailableException e) {
    log.error("Inventory update failed for order {}", order.id(), e);
    throw new CheckoutFailedException(order.id(), e);
}

The test: search a new codebase for empty catch blocks. It shows you the fragile parts in ten minutes.

7. A boolean parameter hides two methods in one

save(user, true) tells you nothing at the call site; you have to open the method to learn what true means. Worse, the flag splits the body into two paths that often share almost no logic.

public void save(User user) { repository.save(user); }

public void register(User user) {
    save(user);
    emailClient.sendWelcome(user);
}

register(user) says more than save(user, true) ever will.

The test: every boolean argument gets one question in review — what are the two methods here?

8. Wait for the third copy before you extract

Two similar blocks look like duplication. Often they are two separate rules that happen to agree this quarter, and the shared abstraction becomes the thing that has to be torn apart when they diverge. Waiting for the third occurrence gives you enough evidence to know what the abstraction actually is.

This is the restraint that The Boy Scouting Rule — A Simple Yet Effective Programming Principle needs to stay safe: leave the campsite cleaner, but not by inventing structure the code has not yet earned. The same premature-commitment error appears in the data layer as denormalizing before there is a measured problem — see Database Concepts Every Backend Developer Must Understand.

9. Keep database and network calls out of your rules

Business rules mixed with I/O cannot be tested without standing up the world, and they fail for reasons that have nothing to do with the rule. Pull the calls to the edges and leave the decision logic pure. The rule then becomes something you can exercise exhaustively in milliseconds — which is what makes it reviewable at all.

This is the practitioner-level version of the layering discipline DDD Model Thinking Made Easy and Practical Using Common-Sense Architectural Notations argues for at the model level.

10. Your test name should tell me what broke

A failing test called testUpdate tells you a test failed. A test called rejects_withdrawal_when_balance_below_minimum tells you what the system promised and stopped doing — usually enough to locate the defect before opening the file. The test name is the error message you will read at 3am.

Why this doesn't need a process

None of these require a two-hour meeting, a new standard, or a tooling rollout. Each is a question one reviewer can ask about one diff, which is what makes them stick where heavier schemes do not. Encoding Team Standards is the argument for what to do when a rule does need to outlive the reviewer who remembers it — encode it as something executable rather than something recalled.

The caveat worth carrying: these are rules for code a human will read and maintain. The Senior Review Bottleneck describes what happens when the volume of generated code outruns the review capacity that rules like these depend on.