Clean Code - Stop Writing Comments. Write Better Code Instead.
Before Start lets check you System Design knowledge
How to Design a Notification System: Frontend + Backend
Every production app has one. Most engineers underestimate it.
Let me make a confession before we get into this.
I used to be the engineer who commented everything.
Every class had a Java doc header. Every method had a description. Every complex block had an inline explanation. I thought I was being a good engineer. I thought I was helping the people who would read my code after me.
I was wrong.
Robert C. Martin says it plainly in in Clean Code:
“Comments are always failures. We must have them because we cannot always figure out how to express ourselves without them, but their use is not a cause for celebration.”
Not a necessary tool. A failure.
That word stopped me when I first read it. Failure. Not bad practice. Not code smell. Failure.
I disagreed for about three days. Then I started looking at the comments in my own codebase.
Most of them were lies.
Why Comments Lie
Not intentionally. That’s the thing.
Comments lie because code changes and comments don’t keep up.
You write a comment that accurately describes what the function does today. Six months later someone changes the function. They update the code. They do not update the comment. Now the comment describes what the function used to do.
The reader trusts the comment. The reader is now confused or — worse — wrong.
Martin describes it this way: code changes and evolves, it moves from here to there, it bifurcates and reproduces. Comments cannot follow. They get separated from the code they describe. They become orphaned.
Here is a real example of exactly this problem from the book:
MockRequest request;
private final String HTTP_DATE_REGEXP =
"[SMTWF][a-z]{2}\\,\\s[0-9]{2}\\s[JFMASOND][a-z]{2}\\s" +
"[0-9]{4}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\sGMT";
private Response response;
private FitNesseContext context;
private FileResponder responder;
private Locale saveLocale;
// Example: "Tue, 02 Apr 2003 22:18:49 GMT"
That comment was added with HTTP_DATE_REGEXP . Then other instance variables were added in between. Now the comment is separated from what it was meant to describe. Technically still accurate. In practice, misleading — you have to stop and figure out which field it actually belongs to.
This is not an extreme case. This is what happens in every codebase over time.
Truth can only be found in one place: the code. The code always tells you what it does. Comments tell you what someone thought it did at some point in the past.
How Instagram Scaled from 0 to 1 Billion Users with 3 Engineers
On October 6, 2010, Instagram launched on the App Store.
The Good Comments (There Are a Few)
Before I tell you what to stop writing, let me be fair.
Martin agrees there are a small number of good comments. They are rare. When you find yourself needing one, it is worth checking twice that you actually need it. But they exist.
Legal comments. Copyright notices and license headers at the top of a file. You do not have a choice here. Write them.
// Copyright (C) 2024 Better Engineers Inc.
// Licensed under the Apache License, Version 2.0
Explanation of intent — the WHY, not the WHAT. Sometimes you make a decision that looks wrong but is correct for a non-obvious reason. A comment explaining why is legitimate.
// Using Thread.sleep here instead of wait/notify because
// this is a mock for testing and we need deterministic timing.
// See: https://jira.example.com/PLATFORM-2847
Thread.sleep(500);
The code tells you what it does. The comment tells you why you chose to do it that way. That is a useful comment.
Warning of consequences.
// Don't run this test unless you have 30+ minutes.
// It loads the full production dataset into memory.
@Test
public void testFullDatasetReconciliation() {
TODO comments. Acceptable, with the understanding that you actually go back and do them. TODOs that survive for three years are not TODOs. They are lies.
// TODO: Replace with proper retry logic (JIRA-1234)
// This is a temporary workaround for the billing-api timeout
That is four types of good comments. Out of the dozens of comment types most engineers write, four are legitimate.
Everything else is a failure of expression.
The Bad Comments — Your Codebase Is Full of These
Redundant comments
This is the most common. The comment says exactly what the code says, but slower.
// Check if employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) && employee.age > 65) {
What does the comment add? Nothing. If you deleted it, would you understand the code worse?
No.
Now rewrite the code instead:
if (employee.isEligibleForFullBenefits()) {
No comment needed. The code now explains itself. And the logic for eligibility lives in isEligibleForFullBenefits() where it belongs, not scattered across every callsite.
I see this pattern constantly. Engineers write a comment because the code is unclear, instead of making the code clear.
The comment is the symptom. The unclear code is the disease.
How WhatsApp Handled 1 Billion Users with 50 Engineers
In 2014, Facebook paid $19 billion for WhatsApp.
Mandated comments
Some teams enforce a rule: every public method must have a Javadoc comment. Every parameter must be documented.
/**
* Processes the payment.
*
* @param userId the user ID
* @param amount the amount
* @param token the payment token
* @return the payment result
*/
public PaymentResult processPayment(String userId, double amount, String token) {
This tells you nothing that the method signature does not already tell you. The only thing it adds is visual noise that you have to scroll past every time you read the file.
Worse — when the method changes, these comments are the first thing that stops being updated. Three months later the parameter is renamed but the comment still says the old name.
Mandated comments are noise. They exist because someone made a rule. They do not serve readers.
Journal comments
Before version control existed, engineers kept a change log at the top of the file:
/*
* 2024-01-15 (DGD): Added refund support
* 2023-11-02 (TKS): Fixed concurrent access bug
* 2023-09-14 (DGD): Initial implementation
*/
Git exists. GitHub exists. Every team in the world has version control.
Delete these. Immediately. git log tells you this story in more detail, with the actual diffs, with the full commit messages.
Journal comments are archaeology artifacts that should not exist in modern codebases.
Noise comments
/** Default constructor */
public CustomerService() {
}
/** The day of the month */
private int dayOfMonth;
“Default constructor.” You can see it is a constructor. You can see it takes no arguments. What does the comment add?
Nothing. It is noise. It pushes the information you actually want to read further down the screen. It trains your eye to skip over comment blocks entirely — which means you might skip a comment that was actually important.
Every noise comment makes the next real comment harder to notice.
Commented-out code
This is the one that irritates me most.
// List<User> users = userRepository.findAll();
// for (User u : users) {
// if (u.getStatus() == ACTIVE) {
// legacyBillingService.charge(u.getId(), u.getMonthlyFee());
// }
// }
userBillingService.chargeActiveUsers();
Why is this here? Was it useful once? Was it an alternative implementation? Was it going to be needed again? Who wrote it and when?
Nobody knows. It has been there for two years. Nobody deletes it because what if it is important?
Delete it.
Git has it. If you need it back, git log -p finds it. The commented-out code is not a safety net. It is litter.
I have seen codebases where 20% of the lines were commented-out code. Nobody knew what any of it was for. Nobody dared delete it. It just accumulated over years until the files were twice as long as they needed to be.
Misleading comments
The most dangerous kind.
// Returns true when it's closed
public synchronized boolean hasBeenClosed() {
return (this.closed > 0);
}
The comment says “returns true when it’s closed.” The code returns true when closed > 0 — not just closed, but closed more than once.
Someone reads the comment. Trusts it. Writes code based on the comment. That code has a subtle bug because the comment was misleading — not wrong, exactly, but not quite right either.
Inaccurate comments are worse than no comments. Martin says it directly and I agree completely. They set expectations that the code does not fulfill. They send engineers in the wrong direction. They take longer to debug because you trust the wrong thing.
Closing brace comments
public void processAllOrders() {
for (Order order : orders) {
if (order.isActive()) {
try {
// ... 40 lines of processing ...
} catch (Exception e) {
// ...
} // end try
} // end if
} // end for
} // end processAllOrders
If your function is long enough to need closing brace comments, the function is too long.
Shorten the function. Extract the inner logic into named methods. The braces will be close enough to their openings that you do not need a comment to match them.
The closing brace comment is not solving the problem. It is a band-aid on a wound that needs stitches.
The Pattern I See Over and Over
When I do code reviews, there is one thing that produces comments consistently.
Complex conditional logic.
// Check if the user is in the premium tier, hasn't exceeded
// their download quota, and is in a country where this
// feature is available
if (user.getTier() == Tier.PREMIUM
&& user.getDownloadCount() < user.getDownloadLimit()
&& availableCountries.contains(user.getCountry())) {
The engineer knew the condition was complex. They added a comment to explain it.
The comment is accurate. But look at what happens when you extract it instead:
if (isEligibleForDownload(user)) {
private boolean isEligibleForDownload(User user) {
return user.getTier() == Tier.PREMIUM
&& user.getDownloadCount() < user.getDownloadLimit()
&& availableCountries.contains(user.getCountry());
}
No comment needed. The code now reads as a sentence. The logic lives in one place with a name that describes what it means. Tests can target isEligibleForDownload directly instead of having to reproduce the full condition.
The comment told you what the code did. The function name tells you what the code means.
That distinction matters. What is easy. Why and what it means are harder. Code that expresses meaning is better than code with a comment explaining what it does.
The Test
Before you write a comment, ask yourself this question:
Can I express this in code instead?
Not always. Sometimes you genuinely need to explain why a decision was made, or warn about a consequence, or satisfy a legal requirement.
But most of the time the answer is yes.
Want to explain what a condition means?
→ Extract it into a method with a descriptive name.
Want to explain what a block of code does?
→ Extract it into a function.
Want to explain what a variable represents?
→ Rename the variable.
Want to explain why you chose this algorithm?
→ That is a legitimate comment. Write it.
Want to explain what the code already says clearly?
→ Delete the comment. The code is already the explanation.
Martin quotes Kernighan and Plaugher at the start of the chapter:
“Don’t comment bad code — rewrite it.”
That is the whole principle in five words.
What I Changed in My Own Code
When I read this chapter the first time I went back through a service I had been working on.
Sixty-two comments in one file. I went through them one by one.
31 were redundant — said what the code said
14 were mandated Javadocs — empty noise
9 were journal entries — should have been in git
4 were commented-out code — should have been deleted
3 were closing brace comments on a function that was too long
1 was a legitimate TODO with a JIRA ticket
I deleted 61 comments. I replaced several of them with better names and extracted methods.
The file went from 380 lines to 310 lines.
It reads faster now. Not because there is less to read — the logic is the same. Because the noise is gone. Every line that remains earns its place.
The one comment I kept:
// TODO: Remove this workaround after PLATFORM-3421 is fixed.
// billing-api returns 200 even on failure for amounts > ¥100,000.
// We check the response body to detect real failures.
if (result.getStatusCode() == 200 && result.getBody().contains("error")) {
That comment explains something the code cannot express. It tells you why the strange check exists, not what it does. It references a ticket. It will be deleted when the ticket is resolved.
That is a good comment.
One Last Thing
Some engineers push back on this. “But what about documentation? How do other engineers know what the code does?”
The answer is: from the code.
If the code is so complex that it requires a prose explanation to understand, the code needs to be simpler. Not commented. Simpler.
Comments are not documentation. They are apologies.
The goal is code so clear that it needs no apology.
This is the second post in a series based on Clean Code by Robert C. Martin.
First post: Why Your Code Is Slow to Read — and How to Fix It in One Hour.
Next post: Don’t Return Null — and What to Do Instead. The one change that eliminates an entire category of production bugs.
Share this with one engineer who writes more comments than code.






"TODOs that survive for three years are not TODOs. They are lies" So true 😂😂😂