DEV Community

Cover image for Clean Code Is Not the Same as Clear Code: Comments Were Never the Problem
Giorgi Kobaidze
Giorgi Kobaidze Subscriber

Posted on

Clean Code Is Not the Same as Clear Code: Comments Were Never the Problem

Includes a helpful regex breakdown in the comments

Table of Contents


A Line That Does Nothing... Except Keep Production Alive

Somewhere along the way, our industry decided that writing a comment means you've failed as an engineer and need to "git good" (don't try that in the terminal, it's not a git command). If your code needs explaining, the logic goes, your code isn't good enough.

I won't lie, I really like the idea. I'd love every codebase to be so clean that you open a file, read it once, and think "yep, got it." Everything logical, everything obvious.

But...

I'd also love roads so well designed they don't need signs. But even the most beautifully engineered mountain road has a "sharp bend ahead" sign, and not because the engineers failed. It's there because you can't see the bend until you're already in it.

Sharp Bend

So here's the quick reality check: not every function can explain what it's doing. Not now, not tomorrow, not ever. There will always be code that:

  1. Was never written with structure in mind, so nobody understands it.

  2. Is written just beautifully, but solves a problem so complex that you still need five hours staring at one function to follow it.

  3. Is simple, clean, and perfectly readable, yet does something for a reason that isn't visible anywhere in the code.

Some Code Can't Explain Itself

Here's a perfectly clean piece of C#:

public static partial class FlightNumbers
{
    [GeneratedRegex(@"^([A-Z]{2}|[A-Z]\d|\d[A-Z])(\d{1,4})([A-Z]?)$")]
    private static partial Regex FlightNumber();

    public static bool IsValid(string input) =>
        FlightNumber().IsMatch(input.Replace(" ", "").ToUpperInvariant());
}
Enter fullscreen mode Exit fullscreen mode

Good names. Modern, source-generated regex. Nothing to refactor, nothing to rename.

Now, quick quiz. Which of these are valid?

FlightNumbers.IsValid("BA123");
FlightNumbers.IsValid("U21234");
FlightNumbers.IsValid("9W5A");
FlightNumbers.IsValid("99123");
Enter fullscreen mode Exit fullscreen mode

Unless you're an aviation nerd like me, or someone who reads regex fluently (if you do, my goodness, more power to you), you have no idea what this method is actually checking.

Sure, you can look it up. Sure, you can read the documentation. And sure, there must be unit tests that describe exactly what's valid and what isn't. Right?!

Well, yes, on paper, however "on paper" and "actually" can be like the burger in the ad and the burger in the box.

Burger

Sometimes there's no documentation. Sometimes there are no unit tests. Sometimes there's just you, the regex, and a growing sense of dread.

But even when the docs and tests exist, why should you have to derail your train of thought, open three tabs, and go on a scavenger hunt, when a single comment could be sitting right there on top of the code, waiting for you?

Let me show you. Then tell me this isn't infinitely better:

// IATA flight number, e.g. "BA123", "U21234", "9W5A".
// Airline code is 2 chars: two letters, or a letter-digit mix (U2 = easyJet).
// Then a 1-4 digit flight number and an optional operational suffix letter.
// Uppercase only, no spaces: normalize input before matching.
[GeneratedRegex(@"^([A-Z]{2}|[A-Z]\d|\d[A-Z])(\d{1,4})([A-Z]?)$")]
private static partial Regex FlightNumber();
Enter fullscreen mode Exit fullscreen mode

Plus, you also learn things the code could never tell you.


The Code Knows What. Only You Know Why

The regex was a translation problem: the code was correct, it just spoke a language most humans don't. But there's a second kind of comment, and it's arguably even more valuable, because it covers something the code can't express in any language.

private const int MaxConcurrentRequests = 47;
Enter fullscreen mode Exit fullscreen mode

Clean. Named. A constant, not a magic number buried in a loop. Textbook.

And yet every developer who sees it has the same thought: why 47? It's not a round number. It's not a power of two. It looks like someone's lucky number, or a typo, or the result of a very long night. The provider's documentation says the limit is 50, so surely this is just a mistake.

So somebody changes it to 50. And a few weeks later, under real traffic, the provider starts rejecting requests at random, and nobody can reproduce it.

Here's what was missing:

// 47, not 50. The provider documents 50 requests per second, but their
// limiter measures bursts over a 1.2s window, so retries at 50 trip it.
// Recheck if they publish new limits.
private const int MaxConcurrentRequests = 47;
Enter fullscreen mode Exit fullscreen mode

This is the part of the code that lives only in the head of whoever wrote it. The code records the decision. The comment records the reasoning. Lose the reasoning, and the decision looks like a bug.

So, in practice, a good comment does one of four jobs:

1. Translates

Dense code like a regex, a bit trick, or a math formula, where the what isn't obvious even when the code is perfect.

2. Explains

The reason behind a choice that looks wrong, arbitrary, or unnecessary.

3. Warns

What breaks if you change this, and how badly.

4. Records

A decision with an expiry date. What was tried, what didn't work, and when it's worth revisiting.

If a comment doesn't do one of those four jobs, it probably shouldn't exist. But if it does, deleting it for the sake of "clean code" isn't cleaning.

By the way, these are extremely simple examples, in practice, you'll come across much more complex scenarios.


"Just Put It in the Commit Message"

At this point, someone in the back raises their hand: the reasoning belongs in git history. Write a good commit message, keep the source clean. That's what version control is for.

Again, on paper, it sounds disciplined. But in practice, go run git log on any file that's older than a year. I'll wait.

...
a91f3c2  apply editorconfig
7d2e8b1  fix
3c4f9a0  fix again
e81b7d4  PR feedback
b02c6f5  final fix
f5a1e93  final fix (actually)
...
Enter fullscreen mode Exit fullscreen mode

Somewhere in there is the reason MaxConcurrentRequests is 47. Good luck. Let me know when you find it.

And what if you don't? You're just going to sit and blame everything and everyone? Or just be pragmatic and write a short comment that'll solve problems for other developers and future you? Maybe if past you hadn't been so dogmatic you'd have been exponentially happier now? Have you thought about that?

And by the way even if your team writes beautiful commit messages, the approach still breaks down for a few boring, practical reasons.

Nobody runs git blame on code that looks fine

This is the big one. You don't go looking for a reason when you don't know there is one. A constant set to 47 looks like a typo, not like a mystery worth investigating. A comment interrupts you before you make the mistake. Git history only answers questions you already thought to ask.

Blame decays

One reformat, one rename, one file split, one squash merge, and the line now points to a commit titled "apply editorconfig." The original reason is still in there somewhere, buried under years of unrelated changes. Finding it is no longer a lookup. It's archaeology. Archaeology takes time and effort.

People leave

Sometimes the real documentation isn't the commit history at all. It's Bill. Bill knows why it's 47. Bill also left for a new company two years ago and won't pick up the phone, because he knows you're going to bother him with those questions.

Commit messages are great at explaining a change: what was different about this commit and why. They're terrible at explaining a current state, because the current state is the sum of dozens of changes, and nobody is going to reconstruct it by reading them in order.

The comment is the only place that describes the code as it is right now, right where you're looking at it.


So What's the Worst That Can Happen?

Let's flip the question. Say you write a comment. What's the worst-case scenario?

The most common argument I hear is this:

"if you change the code, you have to change the comment too."

And... so what?

Seriously. How hard is it? The comment is right there. Not in a wiki, not in a Confluence page nobody has opened since 2021, not in a separate repository. It's one line above the code you're already editing. If you can change 47 to 50, you can change the sentence directly on top of it. You're literally looking at it.

The argument assumes there's some rule that a change should touch only the code and leave everything around it untouched. There isn't. When you change a method's behavior, you update its tests. When you rename a parameter, you update its callers. Updating the comment that describes the thing you just changed is the same kind of work. It's not overhead. It's the job.

This is part of a bigger pattern: taking a good guideline and following it so strictly that it starts hurting the code it was supposed to help.

Take DRY. That's a topic for another episode of this series, but ask yourself honestly: does strictly following DRY always make your code better?

Nuh-uh.

Sometimes two pieces of code look similar, so someone extracts a shared function to avoid the "duplication." Then the two use cases drift apart a little, so the function grows a parameter. Then another. Then a couple of flags. And six months later you're reading this:

ProcessOrder(order, true, false, null, customer, true, 3, "legacy", false, skipValidation: true);
Enter fullscreen mode Exit fullscreen mode

Congratulations, the code is DRY. The only downside is that nobody knows what it does. Not even future you, by the way.

Don't tell me you haven't seen, or better yet, written a function/method like this. I have, when I was junior/mid-level developer. And it made my life miserable.

Sometimes it's perfectly fine to repeat a few lines in two places, because keeping each piece readable on its own matters more than eliminating every repeated pattern. Duplication isn't automatically bad, and abstraction isn't automatically good. Everything is a tradeoff, and knowing which side to pick is exactly the kind of instinct that separates a good developer from someone following a checklist.

Comments work the same way. "Never write comments" and "comment everything" are both checklists. The right answer is to write the comment when it carries something the code can't, and to keep it updated the same way you keep everything else updated: because it's right there.


How Not to Write Comments

Now, before anyone runs off and starts commenting every line: none of this is permission to narrate your code. Bad comments are real, and they're a big part of why comments got a bad reputation in the first place. Here's the hall of shame.

The echo

// Increment the retry count
retryCount++;
Enter fullscreen mode Exit fullscreen mode

The XML doc that says absolutely nothing

/// <summary>
/// Gets the user
/// </summary>
/// <param name="id">The id.</param>
/// <returns>The user</returns>
public User GetUser(int id)
Enter fullscreen mode Exit fullscreen mode

Six lines of ceremony and zero information. Every .NET codebase has thousands of these, usually generated by a tool to make a warning go away. If you're going to write a doc comment, tell me something the signature doesn't.

The liar

// Retry up to 3 times
private const int MaxRetries = 5;
Enter fullscreen mode Exit fullscreen mode

The eternal "temporary" fix.

// TODO: temporary workaround, remove later
Enter fullscreen mode Exit fullscreen mode

Later when? Remove it how? Under what conditions? This comment was written in 2019 and it will outlive us all. If something is temporary, say what it's waiting for: a ticket, a version, a date.

The graveyard

// var result = await _legacyService.CalculateAsync(order);
// if (result.IsValid) { ... }
// var result2 = await _newService.CalculateAsync(order);
Enter fullscreen mode Exit fullscreen mode

Commented-out code tells the reader nothing except that someone was scared to delete it. Delete it. If you ever need it again, you won't.

The novel

A thirty-line method with a three-paragraph comment on top explaining how it works. Sometimes that's necessary. Often it's a sign the code should be rewritten, and the comment is compensating. Try the refactor first. Then comment whatever complexity is left.


Clean Gets You Halfway

Clean code is a great goal. Good names, small methods, clear structure: keep doing all of it. But clean code answers one question, what does this do?, and a real codebase keeps asking more. Why is it like this? What happens if I change it? Is this weird thing a bug or a scar?

Those answers don't live in the syntax. They live in the head of whoever wrote the code, and heads are terrible storage. They change jobs, they go on vacation, and they forget things by Tuesday.

So here's the rule I actually follow, and it fits in one sentence:

If I had to stop and think before writing a line, I write down what I thought.

If the line was obvious, I leave it alone. No echoes, no ceremony, no dragons. But if there was a moment where I went "hmm, careful here," that moment goes into a comment, because the next reader is going to have the exact same "hmm," just without the answer.

Clean code tells the reader what you did. A good comment tells them what you knew. You need both.


Enjoyed this write-up? Let's stay connected!

I share more software engineering insights, projects, and experiments across these platforms:

Top comments (41)

Collapse
 
unitbuilds profile image
UnitBuilds

"^([A-Z]{2}|[A-Z]\d|\d[A-Z])(\d{1,4})([A-Z]?)$"

Breakdown for people who wanna learn regex

[A-Z]{2}| - 2 letters
[A-Z]\d| - letter and a digit
\d[A-Z] - digit and a letter
() - represents a group.
([A-Z]{2}|[A-Z]\d|\d[A-Z]) - Group 1
(\d{1,4}) - Group 2 - 1-4 digits
([A-Z]?) - Group 3 - ? means optional, so an optional letter

So First 2 digits are either 2 letters, a letter and a digit, or a digit and a letter.

2nd set is either 1,2,3,4 digits

3rd set is optionally a letter.

"BA123" - 2 letters, 3 digits - so it passes
U21234" - letter digit, followed by 4 digits - so it passes
FlightNumbers.IsValid("9W5A"); - digit letter, 1 digit, letter - so it passes
FlightNumbers.IsValid("99123"); - No letter in first 2, so it fails the first group of the regex.

And now you understand Regex 😁

Collapse
 
georgekobaidze profile image
Giorgi Kobaidze

"And now you understand Regex" - I'm sure nobody in history has ever said that yet 😄

Collapse
 
unitbuilds profile image
UnitBuilds

😂 99% of the time, these are all you really need. It's that last 1% where wildcards come in and anchors, which is usually where people mess up

Thread Thread
 
georgekobaidze profile image
Giorgi Kobaidze • Edited

I remember when LLMs first showed up, one of my first reactions were: "alright, so now I know who writes/reads regex now! (Not me)" 😄

Collapse
 
natia_bekauri_08aeeec9279 profile image
Natia Bekauri

Rules are always nice to me they give structure and cleanness but practice showed me another thing especially when you work with entitled people- sometimes you need to have comments so others don't suddenly change code (especially without retesting 😂) and the most important - yes, comments should not explain obvious tech nical detsils that's a job of a cleanly written literate code itself, but sometimes business rule can be out of logic and hard to see in shadows so that's where comments help and as always - balance is the key. Thank you for sharing this!

Collapse
 
georgekobaidze profile image
Giorgi Kobaidze

Exactly. And you’re right about rules. They’re good to have as a reference point, but if software engineering was just a strict set of rules, it’d be way too easy and simple.

It’s neither easy, nor simple.

Collapse
 
anthony-gicheru profile image
Anthony Gicheru

Really liked this. I think comments get a bad reputation, but sometimes the code can tell you what’s happening without telling you why. That’s where a good comment really helps.

Collapse
 
georgekobaidze profile image
Giorgi Kobaidze

Totally true. I even remember when I was junior engineer and had to learn the codebase. Some of the comments there saved me probably days of wondering what was it doing.

A good comment can sometimes be more valuable than good code.

Collapse
 
mariobermonti profile image
Mario E. Bermonti Pérez

Loved the humor. I laughed aloud when I saw the actual burger in the box. 🤣

Collapse
 
georgekobaidze profile image
Giorgi Kobaidze

Thanks! 😄 I just write whatever comes to mind, and sometimes it makes my articles way better than when I try to be too serious.

Collapse
 
ingosteinke profile image
Ingo Steinke, web developer

Don't ever change the sharp-bend road sign image! It's such a funny example of how AI is solving our problems.

Sharp bend ahead road sign turning to the left while the actual road turns to the right

Collapse
 
georgekobaidze profile image
Giorgi Kobaidze

I know, this is really good haha.

Collapse
 
pengeszikra profile image
Peter Vivo

A hardest maintain comment is the README.md a good one is short cllear as your program, clear indicate something wrong if you feel creepy when read it.

My favorite comment format is the single line jsDoc - even working better than TS and compatible! I wrote a few blogpost of jsDoc. Even a jsDoc based react typesafe state handling npm library ( jsdoc-duck ) - best advice if borrowing instead of import. 64LOC long, a large part is comment.

Collapse
 
georgekobaidze profile image
Giorgi Kobaidze

Good point. A README file is basically one large comment.😄

Collapse
 
dannwaneri profile image
Daniel Nwaneri

Giorgi, the 47 example makes the case on its own. I have the same pattern in RAG work, a chunk size or a similarity threshold that looks arbitrary until you know the one edge case that broke at the round number.

Clean code shows the current value. Only a comment shows why that value and not the obvious one. I follow that same rule now, write down what made me stop and think.

Collapse
 
georgekobaidze profile image
Giorgi Kobaidze

I'm glad it resonated with you. You clearly understood the intent of this example.

Collapse
 
sizzlebop profile image
Jessica Doering

I’m definitely on the side of comments being useful, as long as they’re actually useful comments. I don’t want every other line explaining something the code already makes obvious, but a well-placed comment can save a ton of time.

Even beyond explaining why something was done a certain way, I find comments really helpful just for navigating a codebase. If I’m jumping into a larger file looking for a particular piece of logic, a few good comments make it so much easier to scan and find what I need without having to read every function along the way.

I think the problem was never really comments themselves. It’s comments that add noise instead of context.

Collapse
 
georgekobaidze profile image
Giorgi Kobaidze

Comments, just like anything else in software engineering, can be used in many different ways. It's all about how we use them. Almost nothing is inherently wrong.

Totally agree with your points!

Collapse
 
nigel_amers_a1374d1a583f2 profile image
Nigel Amers • Edited

Your examples look good but miss even more fundamental engineering that unfortunately most c# code falls into the trap of repeating. Your const is valid except for the fact that if it can conceivably change then it isn't a const is it. Moving into an env var might be a better option, then the name is fine, if there are bursts happening then the consuming code is deficient thus the comment papers over that and leaves buggy code alone. The regex is in a partial, and that smells like generated code to me, so you might want to be careful because it might be lost if regeneration happens. However, the main point of code is that it should be human readable, since the computer/compiler doesn't care, the code is an artefact for the human and unfortunately a lot of the frameworks in c# (not all) are mostly junk. Regex also is self explanatory just not that easy for humans to parse hence why a comment on the regex might be ok if you cannot rewrite to reveal the intent following the 4 rules of simple design. I really do like the point of not want to break the cognition by forcing a reader to jump away from the code, the issue you have with comments is not so much that the comment has to be updated with the code changes, it is that humans don't understand the code and for whatever reason may not even change the comment and that is worse because it will then be telling falsehoods to every future reader from then on. The regex being difficult to parse and the comment claim is different from reality, more dangerous than having a comment. I like the comment about dogma though not a fan at all of dogmatism, if something looks less elegant than 'clean' code but reveals intent, then that is actually clean code and the elegance is aesthetics that misses the point of what code is for... the human.

Collapse
 
nigel_amers_a1374d1a583f2 profile image
Nigel Amers

Actually want to add that the article is good and my points are not to pick apart the article to trash it, but to make a point that there is nuance and to ensure that the thinking goes into some not so obvious choices engineers have to make while engineering a system. This is not supposed to be a criticism of the author and if it looked like it was then I apologise for that. I want to make sure people realise that deeper thinking is sometimes required, because the authors points are valid but not in all situations, just as my points are valid but also not in all situations. So kudos to the author!

Collapse
 
georgekobaidze profile image
Giorgi Kobaidze

Thanks for such a thoughtful comment. There's a lot here I agree with, especially the last part. If less "elegant" code reveals intent better, then it is the clean code. That's pretty much the thesis of the series, and you put it better than I did.

You're also right that a lying comment is worse than no comment. A stale comment actively misleads every future reader. I'd just add that names can lie in exactly the same way (we've all met a GetUser() that also writes to the database). I've also seen get requests that would delete an entity as a side effect. So I see it as a maintenance discipline problem for anything humans read, not a reason to avoid comments.

A couple of places where I'd push back a little:

On the constant: an env var makes sense if the value genuinely varies by environment. I've actually though about that argument, but in the example, 47 isn't a tuning knob. It comes from an upstream constraint, the burst window. Moving it to config changes where the number lives, not why it's 47, so the explanation still has to go somewhere. And sometimes the "deficient" code is a third-party API you can't fix, so the comment is recording a constraint you have to live with rather than covering for a bug.

On the partial: [GeneratedRegex] flips the old designer-file model. I write the declaration, and the source generator emits the implementation into a separate file at build time. My file is never regenerated, so the comment is safe. I get the instinct, though. Years of Form1.Designer.cs taught all of us to be suspicious of partial.

Really appreciate you taking the time. This is exactly the kind of discussion I was hoping the post would start.

Thread Thread
 
nigel_amers_a1374d1a583f2 profile image
Nigel Amers

Thanks, and apologies, clean code is like a red rag to me :-) your title is very correct because "clean" code is wrong used as a shield to not use the code base to communicate. I take your push backs too 👍️ thank you for starting this discussion and sharing your own insights.

Thread Thread
 
georgekobaidze profile image
Giorgi Kobaidze

I feel the same way about clean code. When I was a junior/mid-level developer, I used to think that was the only way to go, but as I gained more experience, I realized software engineering isn't as simple as just following strict rules.😄

Collapse
 
glenallen profile image
Glen Allen

The “current state vs historical reasoning” distinction has an interesting implication for AI-assisted development too. An agent can read clean code and understand what it does, but without the reasoning behind unusual constraints, it may confidently “improve” something that was intentionally designed that way. That makes certain comments more than documentation—they become guardrails against incorrect refactoring. I’d argue the most valuable comments for both humans and coding agents are the ones that explain a constraint, its origin, or the consequence of changing it. In that sense, a good comment isn't competing with clean code; it preserves information that neither the syntax nor a refactor can reliably reconstruct later.

Collapse
 
georgekobaidze profile image
Giorgi Kobaidze

Totally true! We write instructions for AI in .md files, why not write even more specific information in comments when necessary?

Good points!