Senior 7 min · March 06, 2026

Clean Code — When Stale Comments Cause $0.01 Errors

A Stripe rounding comment stayed after the API changed, causing monthly reconciliation drift.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Clean code communicates intent so clearly the next reader can understand it without comments.
  • Meaningful names: a name should answer why it exists, what it does, and how it's used.
  • Functions do one thing: if you can't name it without 'and', split it.
  • Comments explain WHY, not WHAT. A comment that explains the code is a code failure.
  • Consistent formatting enforced by tooling removes team friction and speeds up reviews.
  • Production insight: dirty code multiplies bug-fix time by 3x on average.
Plain-English First

Imagine you're building a LEGO set and you just dump all the pieces into one giant pile — it works, but finding the piece you need takes forever and knocking one thing over ruins everything. Clean code is like sorting those pieces into labelled trays, each one containing exactly what the label says. Someone else can walk up, read the labels, grab what they need, and build confidently. The code still does the same job — it just doesn't make the next person want to quit.

Every developer eventually inherits a codebase that makes their stomach drop. Functions that are 300 lines long, variables named 'temp2', comments that say 'don't touch this — no idea why it works', and logic so tangled that fixing one bug plants three more. This isn't a skills problem — it's a clean code problem. And it costs real money: studies consistently show that developers spend roughly 70% of their time reading code, not writing it. Code that's hard to read is code that's expensive to maintain.

Clean code principles exist to solve a specific, painful problem: code written for machines is read by humans. A compiler doesn't care if your function is named 'doStuff' or 'calculateMonthlySubscriptionTotal' — but your teammate at 2am debugging a production outage cares enormously. Clean code is the discipline of writing software that communicates its intent so clearly that the next reader — which is almost always future-you — can understand, trust, and safely change it.

By the end of this article you'll understand the core principles of clean code — meaningful naming, focused functions, honest comments, and clear structure — and you'll see exactly what they look like in real Java code. You'll know not just the rules but the reasoning behind them, so you can apply good judgment even in situations no rulebook covers.

Meaningful Names: The Single Biggest Lever You Have

A name is a promise. When you name a variable 'days', you're promising it holds a count of days. When you name a method 'processData', you're promising nothing — that name could mean literally anything. Bad names are the leading cause of confusion in codebases because they force the reader to hold two things in their head at once: what the code actually does AND what the author meant it to do.

The rule is simple: names should reveal intent. A name should answer three questions — why this thing exists, what it does, and how it's used. If you need a comment to explain a variable name, the name has already failed its job.

Avoid single-letter names outside of short loop counters. Avoid abbreviations that aren't universally known. Avoid names that mislead — a list named 'accountList' that's actually a Map is worse than no name at all. Use searchable names for constants. Name booleans as yes/no questions: 'isEligibleForDiscount' reads naturally in an if-statement; 'eligibleDiscount' doesn't.

This isn't pedantry. A clean name means the reader's brain can stay focused on logic instead of decoding vocabulary. That multiplied across thousands of names in a real project is the difference between a codebase teams enjoy working in and one they dread opening.

SubscriptionRenewalService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class SubscriptionRenewalService {

    // BAD: what is 'd'? What is 'uts'? What does 86400 mean?
    // public boolean chk(int d, long uts) {
    //     return (System.currentTimeMillis() / 1000) - uts > d * 86400;
    // }

    // GOOD: the intent is crystal clear before you read a single line of logic
    private static final int SECONDS_PER_DAY = 86_400; // underscore improves readability of large numbers

    /**
     * Returns true if the subscription has been inactive longer than the allowed grace period.
     * Used to determine whether to send a renewal reminder or suspend the account.
     */
    public boolean hasGracePeriodExpired(int gracePeriodInDays, long lastActiveTimestampSeconds) {
        long currentTimeSeconds = System.currentTimeMillis() / 1_000;
        long secondsInactive = currentTimeSeconds - lastActiveTimestampSeconds;
        long gracePeriodInSeconds = (long) gracePeriodInDays * SECONDS_PER_DAY;

        // We compare seconds directly to avoid floating-point errors from day conversion
        return secondsInactive > gracePeriodInSeconds;
    }

    public static void main(String[] args) {
        SubscriptionRenewalService service = new SubscriptionRenewalService();

        // Simulate a user who last logged in 8 days ago
        long eightDaysAgoInSeconds = (System.currentTimeMillis() / 1_000) - (8L * 86_400);

        boolean expired = service.hasGracePeriodExpired(7, eightDaysAgoInSeconds);
        System.out.println("Grace period expired: " + expired); // Should print true

        boolean stillActive = service.hasGracePeriodExpired(30, eightDaysAgoInSeconds);
        System.out.println("Grace period expired (30-day plan): " + stillActive); // Should print false
    }
}
Output
Grace period expired: true
Grace period expired (30-day plan): false
Pro Tip: Name It Like You're Explaining It to a Colleague
Before committing a name, ask: 'If I said this name out loud in a code review, would my teammate immediately know what it holds?' If you'd feel the need to add 'so basically it's the...' then the name isn't done yet. Rename first, comment second.
Production Insight
In a real incident, a variable named 'time' was reused for two different units (seconds and milliseconds) across a 50-line method. The author knew the context and used it correctly, but a refactoring 6 months later assumed 'time' was always milliseconds. A 30-minute payment deadlock followed.
Rule: a name should be precise enough that the next reader can't misinterpret it. If time is in seconds, name it 'timeInSeconds' or 'durationSeconds'.
Key Takeaway
A name that needs a comment has already failed.
If you can't explain a variable's purpose in its name alone, rename it before reaching for a comment.
Name by role, not by type — 'activeSubscribers' not 'userList'.

Functions That Do One Thing — The Single Responsibility Rule in Practice

A function that does one thing is a function you can name clearly, test completely, and reuse confidently. A function that does several things is a function you can do none of those things with.

The classic sign of a function doing too much is that you struggle to name it without using 'and': 'validateInputAndSaveUserAndSendWelcomeEmail'. That 'and' is a red flag — you've got three functions trapped inside one.

The practical rule: a function should operate at a single level of abstraction. If your function contains both high-level orchestration logic (call the validator, call the repository, call the mailer) AND low-level detail (trim whitespace, check regex, format SQL), it's doing two jobs. Pull the detail down into helper functions. Let the top-level function read like a table of contents.

Function length is a symptom, not a rule. Short functions aren't the goal — focused functions are. A well-written 25-line function beats a messy 5-line one. That said, if you find yourself needing to scroll to read a single function, it's almost always doing too much.

Arguments are part of this too. A function with more than three parameters is a hint that some of those arguments belong together in an object. It also makes call sites harder to read — positional arguments with no labels are a silent bug factory.

UserRegistrationService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import java.util.regex.Pattern;

public class UserRegistrationService {

    private static final Pattern EMAIL_PATTERN =
        Pattern.compile("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$");

    private static final int MINIMUM_PASSWORD_LENGTH = 8;

    // BAD: this function does validation, transformation, AND persistence — three jobs
    // public void registerUser(String email, String password, String rawName) {
    //     if (!email.contains("@")) throw new IllegalArgumentException("bad email");
    //     if (password.length() < 8) throw new IllegalArgumentException("short password");
    //     String name = rawName.trim();
    //     // ... then 40 more lines of saving to DB and sending email
    // }

    // GOOD: each method has exactly one job and a name that proves it

    /** Orchestrates the full registration flow. Reads like a checklist. */
    public void registerNewUser(String email, String rawPassword, String rawDisplayName) {
        validateEmailFormat(email);
        validatePasswordStrength(rawPassword);
        String sanitizedDisplayName = sanitizeDisplayName(rawDisplayName);
        UserRecord newUser = buildUserRecord(email, rawPassword, sanitizedDisplayName);
        saveUserToDatabase(newUser);
        sendWelcomeEmail(newUser);
    }

    /** Validates format only — does NOT check if email already exists (separate concern) */
    private void validateEmailFormat(String email) {
        if (email == null || !EMAIL_PATTERN.matcher(email).matches()) {
            throw new IllegalArgumentException(
                "Invalid email format: '" + email + "'");
        }
    }

    /** Enforces minimum security rules for passwords */
    private void validatePasswordStrength(String password) {
        if (password == null || password.length() < MINIMUM_PASSWORD_LENGTH) {
            throw new IllegalArgumentException(
                "Password must be at least " + MINIMUM_PASSWORD_LENGTH + " characters.");
        }
    }

    /** Removes dangerous whitespace — does NOT validate content */
    private String sanitizeDisplayName(String rawDisplayName) {
        return rawDisplayName == null ? "" : rawDisplayName.trim();
    }

    private UserRecord buildUserRecord(String email, String password, String displayName) {
        // In production: hash the password here — never store plaintext
        return new UserRecord(email, password, displayName);
    }

    private void saveUserToDatabase(UserRecord user) {
        // Simulated — would call your repository layer in a real system
        System.out.println("[DB] Saving user: " + user.email());
    }

    private void sendWelcomeEmail(UserRecord user) {
        // Simulated — would call your email service in a real system
        System.out.println("[Email] Welcome email sent to: " + user.email());
    }

    // Simple record to group related user data instead of passing 4 loose parameters
    record UserRecord(String email, String password, String displayName) {}

    public static void main(String[] args) {
        UserRegistrationService service = new UserRegistrationService();

        try {
            service.registerNewUser("alice@example.com", "securePass99", "  Alice  ");
        } catch (IllegalArgumentException e) {
            System.out.println("Registration failed: " + e.getMessage());
        }

        System.out.println("---");

        try {
            // This should fail validation
            service.registerNewUser("not-an-email", "short", "Bob");
        } catch (IllegalArgumentException e) {
            System.out.println("Registration failed: " + e.getMessage());
        }
    }
}
Output
[DB] Saving user: alice@example.com
[Email] Welcome email sent to: alice@example.com
---
Registration failed: Invalid email format: 'not-an-email'
Interview Gold: The Newspaper Metaphor
Robert Martin's 'newspaper metaphor' says code should read top-to-bottom like a news article: headline first (the public method), then supporting detail (private helpers below). Interviewers love candidates who can explain why public methods should appear before private ones in a class — it's about the reading experience, not convention.
Production Insight
A team inherited a 'processBatch' method that was 450 lines — it validated, transformed, enriched, persisted, and notified. A single configuration change that should have taken 2 hours took 3 days because adding a new field required understanding the entire flow.
Rule: if you need to scroll more than once to read a single method, it's doing too much. Extract aggressively.
Key Takeaway
If you can't name a function without the word 'and', it's doing two jobs.
Split it, name each piece clearly, and test them independently.
A focused function is a testable function.

Comments That Help vs. Comments That Lie — Knowing the Difference

Here's a clean code truth that surprises a lot of developers: a comment is a failure to express something clearly in code. That's not a reason to never write comments — it's a reason to write fewer, better ones.

Bad comments are the ones that explain what the code is doing. If you need a comment to explain what a line of code does, the code itself isn't clear enough. Rewrite the code first. The worst comments are the ones that lie — logic that's been changed but the comment wasn't updated. A misleading comment is worse than no comment at all because it actively points you in the wrong direction.

Good comments explain why — the business decision, the non-obvious tradeoff, the 'this looks wrong but here's why it isn't'. They capture information that can't live in the code itself: regulatory constraints, hardware quirks, the reason a seemingly-obvious optimization was deliberately not applied.

TODO comments are acceptable only if they're tracked — a TODO buried in a file that nobody reads is just deferred guilt. Use your issue tracker instead. Legal comments (copyright headers) are necessary but should live in a single file, not repeated in every class.

The deepest clean code principle about comments: if you feel the urge to write a comment, first ask whether a better name, a smaller function, or a well-named constant could say the same thing. Usually it can.

PaymentProcessor.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class PaymentProcessor {

    // BAD COMMENTS — these add noise, not signal
    //
    // // increment i by 1
    // i++;
    //
    // // check if amount is greater than zero
    // if (amount > 0) { ... }
    //
    // // This is the payment method
    // public void pay() { ... }

    private static final double STRIPE_MINIMUM_CHARGE_DOLLARS = 0.50;

    // GOOD COMMENT — explains WHY, not WHAT. The code is clear; the business rule is not.
    /**
     * Stripe rejects any charge below $0.50 regardless of currency.
     * We validate here rather than letting the API call fail to avoid
     * unnecessary network round-trips and confusing Stripe error codes.
     * See: https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts
     */
    public void chargeCustomer(String customerId, double amountInDollars) {
        if (amountInDollars < STRIPE_MINIMUM_CHARGE_DOLLARS) {
            throw new IllegalArgumentException(
                "Charge amount $" + amountInDollars +
                " is below the Stripe minimum of $" + STRIPE_MINIMUM_CHARGE_DOLLARS);
        }

        // GOOD COMMENT — flags a non-obvious gotcha future developers will thank you for
        // We deliberately do NOT round amountInDollars here.
        // Stripe accepts fractional cents and rounds on their side.
        // Rounding here caused a $0.01 discrepancy in reconciliation reports (see issue #4821).
        submitChargeToStripe(customerId, amountInDollars);
    }

    private void submitChargeToStripe(String customerId, double amount) {
        // Simulated payment gateway call
        System.out.printf("[Stripe] Charging customer '%s' — $%.2f%n", customerId, amount);
    }

    public static void main(String[] args) {
        PaymentProcessor processor = new PaymentProcessor();

        // Valid charge
        processor.chargeCustomer("cus_abc123", 29.99);

        // Below Stripe minimum
        try {
            processor.chargeCustomer("cus_abc123", 0.25);
        } catch (IllegalArgumentException e) {
            System.out.println("Charge rejected: " + e.getMessage());
        }
    }
}
Output
[Stripe] Charging customer 'cus_abc123' — $29.99
Charge rejected: Charge amount $0.25 is below the Stripe minimum of $0.5
Watch Out: Commented-Out Code Is a Silent Codebase Rot
Never commit commented-out code. It accumulates, nobody knows if it's safe to delete, and it breeds uncertainty. If you need it back, that's what version control is for — git checkout is infinitely more reliable than a block comment from 2019 that nobody dares touch.
Production Insight
A startup lost a $200K deal during due diligence when the auditor found a comment saying '// this is a hack, will fix later' in the payment module. The comment was four years old. The auditor flagged it as a risk indicator.
Rule: every TODO or 'fix later' comment is a liability. Track it in your issue tracker or delete it. If it's not important enough to be tracked, it's not important enough to be in the code.
Key Takeaway
Comments that explain WHAT are noise; comments that explain WHY are gold.
A misleading comment is worse than no comment at all.
If you need a comment to explain what the code does, rename the code first.

Code Structure and Formatting — The Respect You Show Future Readers

Formatting isn't cosmetic. The layout of your code communicates structure the same way paragraph breaks communicate structure in prose. A wall of unformatted text is hard to read even if every word is perfect — the same is true of code.

The most important formatting rule is one you can automate: be consistent. Whether you use 2 spaces or 4, whether braces go on the same line or the next — it barely matters. What matters enormously is that the entire codebase looks like one person wrote it. This is why teams adopt linters and formatters (Checkstyle, Spotless, Prettier) and enforce them in CI. It removes style arguments entirely and keeps code review focused on logic.

Vertical space tells a story. Group related lines together. Separate unrelated concepts with a blank line. A function that has no breathing room makes every line feel equally important, which means nothing feels important. Use vertical distance to signal: 'these things belong together; this next block is a new idea'.

Horizontal formatting is about keeping lines readable without scrolling. A widely accepted modern limit is around 120 characters. Anything beyond that usually means a function is doing too much or a line is trying to express too complex a thought at once.

Dependency ordering matters too: in a class, keep high-level functions near the top and low-level helpers below. Callers should appear before callees. This creates a natural reading path — you see the big picture before the details, just like a well-structured document.

InvoiceFormatter.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import java.util.List;

public class InvoiceFormatter {

    // Vertical grouping: constants together, logically separated from the constructor
    private static final String CURRENCY_SYMBOL = "$";
    private static final String LINE_SEPARATOR = "-".repeat(40);

    private final String businessName;
    private final String customerName;

    public InvoiceFormatter(String businessName, String customerName) {
        this.businessName = businessName;
        this.customerName = customerName;
    }

    // High-level orchestration at the TOP — you see the big picture first
    public String formatInvoice(List<LineItem> lineItems) {
        StringBuilder invoice = new StringBuilder();

        appendHeader(invoice);
        appendLineItems(invoice, lineItems);
        appendTotal(invoice, calculateTotal(lineItems));

        return invoice.toString();
    }

    // Lower-level detail follows BELOW the caller — the newspaper structure
    private void appendHeader(StringBuilder invoice) {
        invoice.append(LINE_SEPARATOR).append("\n");
        invoice.append("Invoice from: ").append(businessName).append("\n");
        invoice.append("Bill to:      ").append(customerName).append("\n");
        invoice.append(LINE_SEPARATOR).append("\n");
    }

    private void appendLineItems(StringBuilder invoice, List<LineItem> lineItems) {
        for (LineItem item : lineItems) {
            // Left-align description, right-align amount — clear visual hierarchy
            invoice.append(String.format("%-25s %s%.2f%n",
                item.description(), CURRENCY_SYMBOL, item.unitPrice()));
        }
        invoice.append(LINE_SEPARATOR).append("\n");
    }

    private void appendTotal(StringBuilder invoice, double total) {
        invoice.append(String.format("%-25s %s%.2f%n", "TOTAL", CURRENCY_SYMBOL, total));
        invoice.append(LINE_SEPARATOR).append("\n");
    }

    private double calculateTotal(List<LineItem> lineItems) {
        return lineItems.stream()
            .mapToDouble(LineItem::unitPrice)
            .sum();
    }

    // Related data grouped into a record — no loose parallel arrays or parameter lists
    record LineItem(String description, double unitPrice) {}

    public static void main(String[] args) {
        InvoiceFormatter formatter = new InvoiceFormatter("TheCodeForge Ltd", "Alice Nguyen");

        List<LineItem> items = List.of(
            new LineItem("Annual Pro Subscription", 99.00),
            new LineItem("Priority Support Add-on", 29.99),
            new LineItem("Setup Fee", 0.00)
        );

        System.out.println(formatter.formatInvoice(items));
    }
}
Output
----------------------------------------
Invoice from: TheCodeForge Ltd
Bill to: Alice Nguyen
----------------------------------------
Annual Pro Subscription $99.00
Priority Support Add-on $29.99
Setup Fee $0.00
----------------------------------------
TOTAL $128.99
----------------------------------------
Pro Tip: Let Your Formatter Do the Arguing
Add a code formatter (like Spotless for Java, or Prettier for JS/TS) to your CI pipeline and configure it to fail the build on unformatted code. You'll never have another style debate in a PR again — and every contributor's code looks identical regardless of their IDE settings.
Production Insight
A team spent 3 months with 5 different style conventions across 10 microservices. Each PR had at least 5 comments about brace placement. The friction slowed deployment velocity by 40%.
Rule: any style debate that can be automated should be automated. Human attention is better spent on logic, not formatting. Adopt a formatter in CI on day one.
Key Takeaway
Consistent formatting enforced by tooling removes an entire category of team friction.
Automate it once, stop debating it forever.
Vertical structure signals meaning — group related, separate unrelated.

Error Handling: Keeping Code Clean When Things Go Wrong

Error handling is where clean code often breaks down. The same developer who writes beautiful, well-named, single-responsibility functions will sometimes glue on a 'catch (Exception e)' block that swallows everything or a chain of try-catch blocks that obscure the happy path.

Clean error handling follows the same principle as the rest of clean code: separate concerns. The error-handling logic should be separated from the business logic. A function that catches, logs, and recovers in a single block is doing three jobs. Extract each job.

Use exceptions for exceptional conditions only. Don't use exceptions for control flow — that's a pattern that makes code hard to follow and surprisingly slow. In production systems, the happy path should be the most readable path. Try-catch blocks should not dominate the visual layout of a method.

Resource cleanup is another common mess. Nested try-finally blocks, forgetting to close streams, or spreading close() calls across multiple methods. The solution is the Try-With-Resources pattern in Java (or 'using' in C#) — the language handles cleanup for you, and the code stays linear.

Return meaningful error types or custom exceptions that carry context. Returning 'null' on failure or a generic 'false' forces the caller to guess what went wrong. A custom exception with a message, a cause, and perhaps an error code is always preferable.

Don't silently ignore exceptions. An empty catch block or one that only logs and continues as if nothing happened is a time bomb. If you can't handle the exception at this level, let it propagate. The decision to handle or not handle should be explicit, not accidental.

DataExportService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import java.io.*;
import java.nio.file.*;
import java.util.List;

public class DataExportService {

    private static final int CHUNK_SIZE = 4096;

    // BAD: error handling mixed with business logic, resource leak risk
    // public void exportData(List<String> records, File target) throws IOException {
    //     try {
    //         FileOutputStream fos = new FileOutputStream(target);
    //         BufferedOutputStream bos = new BufferedOutputStream(fos);
    //         for (String record : records) {
    //             bos.write(record.getBytes());
    //         }
    //         bos.close();
    //     } catch (IOException e) {
    //         logger.error("export failed", e);
    //         throw e;  // rethrow? It's unclear what the caller should do.
    //     }
    // }

    // GOOD: explicit exception, clean resource management, single responsibility

    private static final class ExportException extends RuntimeException {
        public ExportException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    /**
     * Exports records to a file in chunks. If the export fails, the file may be
     * incomplete — the caller should check the return value or use a transactional
     * approach (write to temp file, then rename).
     *
     * @throws ExportException if the export cannot complete due to an I/O error
     */
    public static void exportRecords(List<String> records, Path targetPath) {
        try (BufferedWriter writer = Files.newBufferedWriter(targetPath)) {
            for (String record : records) {
                writer.write(record);
                writer.newLine();
            }
        } catch (IOException e) {
            throw new ExportException(
                "Failed to export " + records.size() + " records to " + targetPath, e);
        }
    }

    public static void main(String[] args) {
        List<String> data = List.of("alice,100", "bob,200");

        try {
            exportRecords(data, Paths.get("/tmp/export.csv"));
            System.out.println("Export completed successfully.");
        } catch (ExportException e) {
            System.err.println("Export failed: " + e.getMessage());
            // In production, this might trigger a retry or alert
        }
    }
}
Output
Export completed successfully.
Watch Out: Empty Catch Blocks Are a Silent Production Bomb
An empty catch block that swallows an exception and continues as if nothing happened is one of the most dangerous things you can write. It turns a transient I/O error into silent data corruption days later. If you can't handle the exception, don't catch it. Use the 'fail fast' principle: let the error propagate to a boundary where it can be logged and surfaced properly.
Production Insight
A team at a fintech company had a 'catch (Exception e) { log.warn('ignoring exception'); }' in a scheduled batch job. A persistent database connection timeout was silently ignored for weeks, causing 15% of daily transactions to be silently dropped. The first sign of trouble was a customer complaint about missing funds.
Rule: never catch an exception unless you can actually handle it. If you catch, either recover, wrap, or rethrow. Logging and continuing is almost always wrong.
Key Takeaway
Error handling should be separated from business logic.
An empty catch block is a time bomb — either handle the exception or let it propagate.
Use custom exceptions with context: message, cause, and actionable information.
● Production incidentPOST-MORTEMseverity: high

The $0.01 Reconciliation Nightmare — When a Comment Convinced Engineers to Round Off

Symptom
Monthly reconciliation reports showed a consistent $0.01 difference between the sum of Stripe charges and internal revenue records. No single transaction was off by $0.01, but the cumulative error grew each month.
Assumption
The team assumed the payment processor code was buggy. They spent days debugging the request serialization, network retry logic, and database writes, all because a comment at the top of the charge method said: 'round amount to 2 decimal places before sending to Stripe'.
Root cause
The rounding was intentional in an older version of the Stripe API that required exact cents. When Stripe updated to accept fractional cents, the code changed but the comment stayed. The comment misled every engineer who touched the code after the upgrade, and the rounding introduced a cumulative discrepancy that matched the pattern of Stripe's internal rounding.
Fix
Removed the rounding step entirely. Added a new comment explaining: 'We deliberately do NOT round amountInDollars here. Stripe accepts fractional cents and rounds on their side. Rounding here caused a $0.01 discrepancy in reconciliation reports (see issue #4821).' The reference to the issue ensures anyone reading the code can find the full context.
Key lesson
  • Comments that describe what the code does become lies the moment the code changes.
  • Always verify comments when debugging production issues — especially ones that say 'we do X because Y'.
  • When a comment explains a business rule or non-obvious constraint, link it to an external source (documentation, issue tracker) so it doesn't become stale.
  • If you find yourself about to add a comment that describes what the code does, rename the code instead.
Production debug guideWhen you inherit a codebase that makes you cry, here's how to systematically unknot it without breaking everything.5 entries
Symptom · 01
You can't tell what a function does from its name
Fix
Rename it to describe its actual behaviour. If the name still contains 'and', split the function into two. Track down all call sites and update them with modern IDE refactoring tools.
Symptom · 02
Variables named 'data', 'temp', 'obj' everywhere
Fix
Use grep to find all occurrences (git grep -n 'temp' -- '*.java'). Rename each to a meaningful name that describes the role, not the type. If the variable's role is unclear because it's reused for multiple purposes, restructure the code so each variable has one purpose.
Symptom · 03
Comments that explain what every line does
Fix
Delete every comment that repeats the code. If the code is unclear without the comment, refactor the code until it's self-explanatory. Keep only comments that explain WHY (business rules, workarounds, performance traps).
Symptom · 04
A 200-line method with multiple blank-line-separated sections
Fix
Extract each section into its own private method with a clear name. The original method becomes a readable sequence of calls. This is the 'composed method' pattern from Refactoring.
Symptom · 05
Inconsistent formatting throughout the codebase
Fix
Run a code formatter (e.g., Spotless for Java, Prettier for JS) on the entire codebase and commit the formatting change in a single, isolated commit. Then add the formatter to your CI pipeline so it's enforced going forward.
★ Code Smell Quick Fix — Identify the Smell and Nuke ItWhen you spot these smell signals in a PR or legacy code, apply the fix immediately. Each entry gives you a symptom, a command to confirm it, and the exact change to make.
Variable named 'temp' or 'data'
Immediate action
Find all occurrences with `git grep -n '\btemp\b' -- '*.java'`. Rename each to describe its purpose using IDE rename refactoring (Shift+F6 in IntelliJ).
Commands
git grep -n '\btemp\b' -- '*.java'
git mv ./src/io/thecodeforge/legacy/TempUsage.java ./src/io/thecodeforge/clean/NamedProperly.java (only after renaming inside the file)
Fix now
Rename the variable to something like 'cachedInvoiceList' or 'pendingTransaction'. If the same variable is reused for different purposes, split it into two separate variables.
Function with 'And' in the name (e.g., validateAndSaveAndSendEmail)+
Immediate action
Identify the three distinct responsibilities. Extract each into its own method using Extract Method (Ctrl+Alt+M in IntelliJ). The original method becomes a sequence of calls: validate(...); save(...); sendEmail(...);
Commands
git log --oneline --all -S 'validateAndSaveAndSendEmail' -- '*.java'
wc -l <filename> to confirm the method shrinks by at least half
Fix now
Rename to orchestrateRegistrationFlow() that calls three separate private methods. Each new method name must avoid 'and'.
Comment that explains a simple line (e.g., // adds 1 to counter)+
Immediate action
Delete the comment. If the code is still unclear, rename the variable or extract a helper method. Use `git grep -n '// adds' -- '*.java'` to find all similar cases.
Commands
git grep -n '// adds' -- '*.java'
sed -i '/\/\/ adds/d' <filename> (be careful to only remove the exact line)
Fix now
Remove the comment. The code 'counter++' is self-explanatory. If 'counter' is a bad name, rename it to something like 'activeUserCount'.
Method with more than 20 lines of code+
Immediate action
Use IDE to measure line count. If > 20, look for blank-line-separated blocks. Extract each block into a private method. Aim for each method to fit on one screen (max ~25 lines).
Commands
awk 'END{print NR}' filename.java (count lines)
cloc --by-file filename.java (to see function boundaries)
Fix now
Extract each logical section into a method named after that section. The original method becomes a table of contents.
AspectDirty CodeClean Code
Variable naming'd', 'tmp2', 'data''gracePeriodInDays', 'cachedUserProfile'
Function size200-line 'doEverything' method10-25 line functions each doing one job
CommentsExplains what every line does; often outdatedExplains WHY — business rules, tradeoffs, surprises
TestabilityHard to test without mocking the entire worldEach small function is independently testable
Onboarding timeDays to understand a single moduleNew dev is productive within hours
Bug-fix riskHigh — changing one thing breaks three othersLow — isolated responsibility means isolated changes
Code review qualityReviews focus on decoding what it doesReviews focus on whether it does the right thing
Error handlingCatch (Exception e) {} — silent data corruptionCustom exceptions, explicit propagation, try-with-resources
Formatting consistencyMixed tabs/spaces, random brace placementFormatted automatically by CI, looks like one person wrote it

Key takeaways

1
A name that needs a comment has already failed
if you can't explain a variable's purpose in its name alone, rename it before reaching for a comment
2
The 'and' test
if you can't name a function without the word 'and', it's doing two jobs — split it, name each piece clearly, and test them independently
3
Comments that explain WHAT are noise; comments that explain WHY are gold
the only comments worth writing are the ones future-you will genuinely thank you for
4
Consistent formatting enforced by tooling removes an entire category of team friction
automate it once, stop debating it forever
5
Error handling should be clean too
separate concerns, use custom exceptions, and never leave an empty catch block

Common mistakes to avoid

4 patterns
×

Over-commenting obvious code

Symptom
You write '// increment i by 1' above 'count++'. Over time, the team becomes conditioned to skim or ignore comments, missing the one truly important comment that explains a business rule.
Fix
Delete any comment that re-states what the code already says. Reserve comments for the business rule or non-obvious decision the code can't express on its own. If the code isn't clear without the comment, rename the variable or extract a method.
×

Naming variables by their type instead of their role

Symptom
Variables like 'userList', 'stringArray', 'dateObject' tell you what the variable is in memory, not what it means in the domain. The reader has to carry the semantic meaning in their head. This leads to confusion when the type changes or when the same variable is reused for different purposes.
Fix
Name by purpose: 'activeSubscribers', 'sortedUploadDates', 'trialExpiryDate'. The type is already visible in a typed language — the name should add meaning, not repeat the type. Use IDE refactoring to rename all occurrences.
×

Creating one massive function 'to keep related code together'

Symptom
A 150-line function with a dozen concepts jammed in. Even the original author struggles to understand it six months later. Bugs are introduced because the reader misses an implicit dependency between sections separated by 80 lines of code.
Fix
When you find yourself writing a blank-line-separated 'section' inside a function, that section deserves its own named private method. The top-level function becomes a readable summary; the details live below. Use Extract Method repeatedly until each method fits on one screen.
×

Swallowing exceptions with empty catch blocks

Symptom
A 'catch (Exception e) {}' that logs nothing and does nothing. The application continues as if nothing happened, but data may be lost, transactions may be incomplete, and downstream systems may receive inconsistent state. The first sign of trouble is often a customer complaint.
Fix
Never use an empty catch block. At minimum, log the exception with context (what was happening, what data was involved). If you can't handle the exception meaningfully, don't catch it — let it propagate to an error boundary. Use custom exceptions to carry context.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Can you explain the difference between clean code and simply commented c...
Q02SENIOR
What is the Single Responsibility Principle at the function level, and c...
Q03SENIOR
You're reviewing a PR and a function has a very long and accurate commen...
Q04SENIOR
Describe a technique you use to name variables and methods that improves...
Q05SENIOR
How do you handle the tension between writing clean code and shipping fa...
Q01 of 05SENIOR

Can you explain the difference between clean code and simply commented code? Why isn't adding more comments always the answer?

ANSWER
Clean code is code that communicates its intent without needing comments. Commented code is a symptom that the code itself isn't clear. Writing more comments to explain unclear code is like adding footnotes to a poorly written article — you're compensating for a readability problem rather than fixing it. Clean code prefers meaningful names, small functions, and clear structure over comments. Comments are reserved for 'why' information that can't live in the code: business rules, tradeoffs, regulatory constraints. The litmus test: if a developer can understand the code without reading any comment, that's clean code.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Is clean code just about making code look pretty?
02
Does writing clean code slow you down?
03
How do I convince my team to write cleaner code without it turning into arguments?
04
Should I refactor a messy codebase all at once or incrementally?
05
What's the single most impactful clean code change a team can make in a week?
🔥

That's Software Engineering. Mark it forged?

7 min read · try the examples if you haven't

Previous
Design Patterns Overview: Creational, Structural and Behavioural
5 / 16 · Software Engineering
Next
Code Review Best Practices