Chapter 4: Comments
clean-code comments documentation
Status: Notes complete
Difficulty: Easy
Time to complete: ~35 min read
Overview
Comments are not automatically good. At best they are a necessary evil — a compensation for our failure to express ourselves in code. The proper use of a comment is to explain what code cannot. But code changes and comments don’t always follow; they rot, they lie, and they mislead. Every comment you write is a failure of expression. Before writing one, try first to refactor the code so the comment is unnecessary. When you cannot, write the comment with care.
This chapter catalogs both legitimate comments (8 types) and the many forms of bad comments (15 types). The balance strongly favors the bad: most comments in the wild are noise or misinformation.
The Problem: What Bad Code Looks Like
// BAD — comment compensates for a poor name; if the name were better, no comment needed
// Returns the number of days since the user last logged in
int d; // elapsed time in days
// BAD — commented-out code that nobody dares delete, rotting silently
//int processedCount = 0;
//for (Order o : orders) {
// if (o.getStatus() == Status.PENDING) {
// processedCount++;
// process(o);
// }
//}
// BAD — mandated Javadoc that adds zero information
/**
* @param order The order
* @param customer The customer
* @return The result
*/
public Result processOrder(Order order, Customer customer) { ... }All three examples above share a root cause: the comment is a substitute for clarity in the code itself. The goal of this chapter is to make you reach for refactoring first and comments only as a last resort.
Good Comments
1. Legal Comments
Why they exist: Copyright notices, license identifiers, and authorship statements at the top of source files are often required by company policy or open-source licenses. They are legitimate — but keep them brief. Reference an external license file rather than copying all the terms inline.
// GOOD — brief legal header pointing to an external document
// Copyright (c) 2024 Acme Corp. All rights reserved.
// This source code is subject to the MIT License found in LICENSE.md
package com.acme.payments;2. Informative Comments
Why they exist: Some code produces results that are hard to read at a glance — a regular expression, a cryptic format string, or a timestamp format. A brief comment explaining the format can be informative.
// GOOD — regex is inherently hard to read; comment clarifies the pattern
// Matches ISO-8601 date: 2024-12-31 or 20241231
private static final Pattern ISO_DATE = Pattern.compile(
"^(\\d{4})[-]?(\\d{2})[-]?(\\d{2})$"
);But often, you can do better by extracting a well-named method or constant. If you can name the thing clearly in code, the comment becomes redundant.
3. Explanation of Intent
Why they exist: Sometimes code expresses how but not why. The implementation is clear but the reasoning behind a decision is not. A comment that explains intent preserves the author’s context for future maintainers.
// GOOD — explains WHY the thread is used, not just WHAT it does
// We spawn a new thread here to avoid blocking the HTTP response thread
// during the potentially slow fraud-scoring API call. Timeout is 2s.
Thread fraudCheckThread = new Thread(() -> fraudService.score(order));
fraudCheckThread.start();C++ equivalent:
// GOOD — explains a non-obvious threading decision
// Using a lock-free queue here to avoid priority inversion in the
// real-time audio callback. std::mutex is not safe on the audio thread.
lockFreeQueue.push(audioFrame);Python equivalent:
# GOOD — explains why an unusual approach was chosen
# We use os.fork() instead of multiprocessing.Process here because
# the child must inherit the open file descriptors from the parent.
# multiprocessing uses spawn on macOS by default (Python 3.8+), which doesn't.
pid = os.fork()4. Clarification
Why they exist: When you use a library method whose name is ambiguous, or when an argument’s meaning is not obvious from the call site, a brief inline comment can clarify.
// GOOD — clarifies what the magic values mean at the call site
assertTrue(a.compareTo(b) == -1); // a < b
assertTrue(a.compareTo(b) == 0); // a == b
assertTrue(a.compareTo(b) == 1); // a > b// GOOD — format string is not self-evident
// Format: kk:mm:ss EEE, MMM dd, yyyy
DateTimeFormatter orderFormatter = DateTimeFormatter.ofPattern("kk:mm:ss EEE, MMM dd, yyyy");As with informative comments, attempt renaming or extraction first. If the library is yours, rename the method.
5. Warning of Consequences
Why they exist: A comment that warns future developers about a non-obvious consequence — a slow test, a destructive operation, a thread-safety constraint — can prevent costly mistakes.
// GOOD — warns about a known performance issue so developers don't run carelessly
// WARNING: This test creates a full snapshot of the 50 GB production replica.
// Do NOT run unless you have 30+ minutes and have notified the DBA team.
@Test
public void fullSnapshotIntegrationTest() { ... }// GOOD — thread-safety warning that code alone doesn't convey
// NOT thread-safe: SimpleDateFormat is not synchronized.
// Use DateTimeFormatter (immutable) or a ThreadLocal if sharing across threads.
private static final SimpleDateFormat LEGACY_FORMAT = new SimpleDateFormat("yyyy-MM-dd");6. TODO Comments
Why they exist: TODO marks known work that cannot be done right now — a placeholder while a dependency is being built, a future optimization, a tracked issue. Most IDEs surface TODOs in a dedicated panel.
// GOOD — actionable, linked to a tracking ticket
// TODO: Replace with async Kafka publish once PLAT-4821 is released.
// For now, synchronous HTTP call is acceptable under current load.
orderEventService.publishSync(OrderEvent.from(order));// TODO: Remove this compatibility shim once all clients are on API v3 (target: Q3 2025)
@Deprecated
public Order getOrderLegacy(String orderId) {
return getOrder(orderId);
}Warning: TODO comments must not become excuses for leaving broken or bad code indefinitely. They should have an owner or a ticket. A TODO that has been there for 5 years is just commented-out intent.
7. Amplification
Why they exist: Sometimes a line of code looks inconsequential but is critically important. A comment that amplifies its importance prevents future maintainers from “cleaning it up” and introducing a bug.
// GOOD — amplifies a trim() call that looks trivial but is functionally critical
String listItemContent = match.group(3).trim();
// The trim is essential. Leading whitespace would cause the next regex stage
// to treat this as a continuation line, silently corrupting the parsed output.
new ListItemWidget(this, listItemContent, this.level + 1);8. Public API Documentation (Javadoc / Doxygen)
Why they exist: Well-documented public APIs are a courtesy to users of your library or module. When you publish a library — internal or external — Javadoc on public classes and methods is often essential. IDEs surface it as inline documentation during development.
// GOOD — Javadoc on a public API method with meaningful parameter and return docs
/**
* Processes a payment for the given order and returns a receipt.
*
* <p>This method is idempotent: calling it multiple times with the same
* {@code idempotencyKey} will not charge the customer more than once.</p>
*
* @param order the order to charge; must not be null
* @param idempotencyKey unique key for deduplication (UUID recommended)
* @return a {@link Receipt} containing the transaction ID and timestamp
* @throws PaymentDeclinedException if the payment method is declined
*/
public Receipt processPayment(Order order, String idempotencyKey) { ... }Javadoc on private or internal methods adds noise. Reserve it for public-facing API surfaces.
Bad Comments
1. Mumbling
The problem: Writing a comment because you feel you should, or because process requires it, without thinking about whether it says anything meaningful.
// BAD — what does this comment add?
// Added by Fred
catch (IOException e) {
// It's fine
}When you encounter a comment you don’t understand, you have to hunt through the codebase to figure out what the author meant. A comment that requires investigation is a failure.
2. Redundant Comments
The problem: The comment says exactly what the code says, in more words. It takes longer to read than the code itself and adds no information.
// BAD — the comment is slower to read than the code it describes
// The day of the month
int dayOfMonth;
// BAD — restates the method signature
/**
* Utility method that returns when this.closed is true.
* Throws an exception if the timeout is reached.
*/
public synchronized void waitForClose(final long timeoutMillis) throws Exception {
if (!closed) {
wait(timeoutMillis);
if (!closed) throw new Exception("MockResponseSender could not be closed");
}
}C++ equivalent:
// BAD — redundant; the code is self-evident
// Constructor for the UserRepository class
UserRepository::UserRepository() : db_(nullptr) {}Python equivalent:
# BAD — redundant with the function name
def get_user_by_id(user_id: int) -> User:
# Gets user by ID
return user_repo.find(user_id)3. Misleading Comments
The problem: The comment contains subtly wrong information. This is the most dangerous kind of bad comment: developers trust it, write code based on it, and introduce bugs.
// BAD — comment says "fires when closed" but the method fires slightly BEFORE closed is true
// Fires when the connection is closed.
public synchronized void waitForClose(final long timeoutMillis) throws Exception {
if (!closed) {
wait(timeoutMillis);
// 'closed' is still false here — method returns before the flag is set
}
}C++ equivalent:
// BAD — comment is wrong; the function mutates the input
// Returns a sorted copy of the vector without modifying the original.
std::vector<int> sortItems(std::vector<int>& items) {
std::sort(items.begin(), items.end()); // actually sorts in-place!
return items;
}Python equivalent:
# BAD — comment promises idempotency that the implementation doesn't deliver
# Safe to call multiple times; has no side effects.
def register_user(email: str) -> None:
# Actually sends a welcome email every time it's called
db.insert_user(email)
email_service.send_welcome(email)4. Mandated Comments
The problem: A rule that every function must have a Javadoc, or every variable must have a comment, produces a sea of noise. These comments are often auto-generated and carry no information — or worse, they carry wrong information as signatures change and comments don’t.
// BAD — mandated Javadoc on a simple getter adds noise, not information
/**
* @param name The name.
* @return The name.
*/
public String getName() {
return name;
}Fix: Reserve Javadoc for public API surfaces where callers genuinely need documentation. Internal helpers and private methods almost never need it.
5. Journal Comments
The problem: A block of comments at the top of a file tracking every change — who made it, when, what they changed. Source control (Git, SVN) records this far better. Journal comments are clutter.
// BAD — this is what git log --follow is for
/**
* Changes (in reverse chronological order):
* 2024-03-14 — R. Martin: Added retry logic
* 2024-01-08 — K. Beck: Refactored to use strategy pattern
* 2023-11-22 — M. Fowler: Initial implementation
*/
public class OrderProcessor { ... }Fix: Delete journal comments. Use git log or git blame to understand history.
6. Noise Comments
The problem: Comments that restate the obvious. They add visual noise and train developers to ignore all comments — including the important ones.
// BAD — default constructor comment says nothing
/** Default constructor. */
protected AnnualDateRule() { }
// BAD — noise on a field that explains itself
/** The day of the month. */
private int dayOfMonth;7. Position Markers
The problem: Banners that divide sections of a file (/////// Actions ///////). Occasionally useful when a file has sections, but overuse trains developers to skip over them.
// BAD — banner inside a class is usually a sign the class has too many responsibilities
// ============================
// Payment Processing Methods
// ============================Fix: If you need banners, the class or module probably has too many responsibilities. Split it.
8. Closing Brace Comments
The problem: } // end of while or } // end of try signals that the function is so long the reader has lost track of what block they’re in. The fix is to shorten the function, not add a comment.
// BAD — closing brace comment is a smell for a function that is too long
public void processAllPendingOrders() {
for (Order order : pendingOrders) {
try {
// ... 30 lines of logic ...
} catch (OrderException e) {
// ...
} // end of try
} // end of for
} // end of processAllPendingOrders9. Attributions and Bylines
The problem: /* Added by Rick */ belongs in source control, not in source code. It clutters the file and becomes incorrect as the code is modified by others.
// BAD — use git blame, not inline attribution
/* Added by Rick to fix JIRA-3321 */
if (order.isExpired()) return;10. Commented-Out Code
The problem: This is one of the worst sins. Code that is commented out accumulates over time. Others don’t delete it because they fear it was commented out for a reason. It rots: the APIs it references change, it becomes uncompilable, it confuses readers. Source control remembers deleted code — delete it.
// BAD — commented-out code that nobody will delete
//InputStreamResponse response = new InputStreamResponse();
//response.setBody(formatter.getResultStream(), formatter.getByteCount());
// Using string response now
StringResponse response = new StringResponse();
response.setBody(formatter.getResult());C++ equivalent:
// BAD — dead code disguised as a comment
// std::cout << "DEBUG order total: " << total << std::endl;
// auto legacyResult = legacyCalculator.compute(order);
double total = newCalculator.compute(order);Python equivalent:
# BAD — deleted experiments left as commented code
# result = old_algo(data) # switched to new_algo — faster
# for item in data:
# item.validate()
result = new_algo(data)Fix: Delete it. git revert or git log will recover it if needed. It never is.
11. HTML Comments
The problem: Javadoc with HTML tags embedded in them becomes unreadable in the raw source file. Formatting should be the responsibility of the documentation tool, not the author.
// BAD — HTML in source is unreadable
/**
* This method processes the order.<br/>
* <ul><li>Validates the order</li>
* <li>Charges the customer</li></ul>
* <p>See {@link OrderService} for details.</p>
*/
public void processOrder(Order order) { ... }12. Nonlocal Information
The problem: A comment above one function that describes some other, distant part of the system. When the remote thing changes, the comment here doesn’t update.
// BAD — describes the default port in a distant config file, not this function
/**
* Port defaults to 8080. See ServerConfig for how to override.
*/
public void startServer(int port) {
server.listen(port);
}13. Too Much Information
The problem: An essay about the history, the RFC the algorithm came from, or interesting but irrelevant context. Put it in a wiki or a linked document.
// BAD — historical context that belongs in a design doc, not source code
/**
* RFC 2045 introduced MIME types in 1996. The algorithm used here was adapted from
* the Java Mail implementation discussed in detail in the java.net article from 2003.
* At the time, Base64 had several competing implementations...
*/
public String encodeToBase64(byte[] data) { ... }14. Inobvious Connection
The problem: The comment and the code it describes have no obvious connection. The reader must work to figure out what the comment refers to.
// BAD — what does "plus filter bytes" and "200 bytes for header" refer to?
/*
* start with an array that is big enough to hold all the pixels in the image
* (plus filter bytes) and an extra 200 bytes for header info
*/
this.pngBytes = new byte[(this.width + 1) * this.height * 3 + 200];The comment requires the reader to understand PNG encoding internals just to connect the comment to the array size formula.
15. Function Headers
The problem: Short, well-named functions don’t need a description at the top. If you feel the urge to write a function header comment, it usually means the function name is inadequate. Fix the name.
// BAD — comment compensates for a vague function name
// Checks if the user is active and their subscription has not expired
public boolean check(User user) { ... }
// GOOD — name makes the comment unnecessary
public boolean isActiveSubscriber(User user) { ... }Comparison / Summary Table
Good Comments vs. Bad Comments
| Category | Type | Signal | Fix if violating |
|---|---|---|---|
| GOOD | Legal | Required by policy/license | Keep; reference external doc |
| GOOD | Informative | Explains non-obvious result/format | Often replaceable with better name |
| GOOD | Explanation of Intent | Explains WHY, not WHAT | Essential when code can’t express why |
| GOOD | Clarification | Disambiguates library/API ambiguity | Try renaming the API first |
| GOOD | Warning of Consequences | Prevents costly mistakes | Always keep these |
| GOOD | TODO | Tracked, actionable, time-bounded | Link to ticket; don’t let them rot |
| GOOD | Amplification | Protects a critical line from “cleanup” | Essential when the line looks trivial |
| GOOD | Public API docs | Javadoc/Doxygen on public surfaces | Required for library authors |
| BAD | Mumbling | Wrote it because “we should have comments” | Delete |
| BAD | Redundant | Restates what code already says | Delete |
| BAD | Misleading | Subtly wrong — the most dangerous | Fix or delete immediately |
| BAD | Mandated | Every method gets auto-generated Javadoc | Remove mandate; document selectively |
| BAD | Journal | Tracks changes over time | Delete; use git log |
| BAD | Noise | Restates the obvious (default constructor) | Delete |
| BAD | Position Markers | Section banners | Delete; split the class instead |
| BAD | Closing Brace | } // end of while | Shorten the function |
| BAD | Attribution/Byline | /* Added by Rick */ | Delete; use git blame |
| BAD | Commented-Out Code | Dead code left in source | Delete unconditionally |
| BAD | HTML Comments | HTML tags in Javadoc source | Let the tool handle formatting |
| BAD | Nonlocal Information | Describes something distant | Move comment to where it’s relevant |
| BAD | Too Much Information | Historical essays in source | Move to design doc or wiki |
| BAD | Inobvious Connection | Comment doesn’t connect to code | Rewrite or delete |
| BAD | Function Headers | Comment compensates for a bad name | Fix the function name |
When to Apply / Common Exceptions
- Always check: can refactoring eliminate the need for this comment? A better name, an extracted function, or a well-named constant often makes a comment unnecessary before you write it.
- Legal comments: Keep as required. Keep them short and reference an external doc.
- Explanation of Intent: The one category where comments are genuinely irreplaceable — code cannot always explain a decision made in the context of a past architectural constraint.
- Warning comments: Never delete these. Even if the condition they warn about seems obvious, the warning saved someone once.
- TODO comments: Enforce a policy: every TODO must reference a ticket. Review and prune them quarterly.
- Commented-out code: Delete unconditionally. No exceptions. Source control recovers it if needed.
- Javadoc on public APIs: Required for library code. For internal application code, invest effort only on the most complex public interfaces.
Checklist
- For every comment you wrote: can the code itself express this without the comment?
- Are any variable or method names so unclear they required a comment? Rename them instead.
- Is there any commented-out code? Delete it.
- Do any comments describe something that is no longer true (misleading)?
- Are there journal comments (change logs) at the top of files? Delete them.
- Are there closing brace comments (
} // end of while)? Shorten the function instead. - Are there mandated Javadoc comments with
@param The paramnoise? Remove or rewrite them. - Are there attribution comments (
/* Added by X */)? Delete them; usegit blame. - Does any comment describe something distant in the codebase (nonlocal information)?
- Are all TODO comments linked to a tracking ticket and time-bounded?
Key Takeaways
- Comments are a failure — every comment is a compensation for our inability to express ourselves in code. Try to refactor first.
- Good comments explain WHY, not WHAT. Code explains what. Comments explain intent, consequences, and decisions that code cannot represent.
- Misleading comments are worse than no comments. A wrong comment actively causes bugs because developers trust them.
- Commented-out code is the worst sin — it rots, it confuses, it scares maintainers away from deleting it. Delete it always.
- Redundant comments add noise and train readers to ignore all comments, including the important ones.
- Mandated Javadoc on every method creates pages of noise comments. Reserve Javadoc for public API surfaces.
- Journal comments are obsolete. Source control tools do this better; delete change-tracking blocks from source files.
- Closing brace comments (
} // end while) signal functions that are too long. Shorten the function. - TODO comments are legitimate but must be tracked, owned, and pruned. An unlinked TODO is just noise that never gets done.
- Warning of consequences and Amplification comments are the most underused good comment types — use them when a line looks trivial but is critical.
Related Resources
- ch03-functions — Small, well-named functions eliminate most of the reasons for comments
- ch02-meaningful-names — Better names remove the need for explanatory comments on variables and methods
- ch05-formatting — Visual structure reduces the need for position markers and section banners
- ch17-smells-and-heuristics — Master catalog of comment-related smells (C1–C5 in the smells list)
Last Updated: 2026-04-14