DEV Community

Cover image for I Think We Confuse Clean Code With Good Code
Jaideep Parashar
Jaideep Parashar

Posted on

I Think We Confuse Clean Code With Good Code

I've seen beautifully organized code that was difficult to understand.

I've also seen ugly-looking code that solved a complicated problem surprisingly well.

That made me wonder:

Are we sometimes confusing clean code with good code?

We talk a lot about clean code.

Small functions.

Meaningful names.

Design patterns.

Abstractions.

DRY principles.

Consistent formatting.

Well-organized files.

All of these things matter.

But there is a problem.

You can follow every clean-code principle and still build something that nobody understands.

Because clean code and clear code are not necessarily the same thing.

Clean Code Can Still Hide the Problem

Imagine opening a project and finding this:

services/
├── adapters/
├── factories/
├── strategies/
├── repositories/
├── handlers/
├── managers/
├── providers/
└── utilities/

Everything looks organized.

Every class has a name.

Every responsibility appears to have its own abstraction.

The code reviewer might even say:

"This is nicely structured."

But then you try to answer a simple question:

"What happens when a user clicks this button?"

And suddenly you're jumping through eight files.

The code is clean.

The architecture is organized.

But the system is difficult to understand.

That's the distinction I think we often miss.

However, in my last post, "I Think AI Is Making Coding Easier and Learning Harder", I emphasised how AI is making coding easier.

Clear Code Answers "Why?"

Clean code often focuses on how the code is written.

Clear code also focuses on why the code exists.

Consider this:

if user.age >= 18:
allow_access()

It's simple.

It's readable.

But imagine the actual business rule is:

Users can access the feature if they are 18 or older, have completed identity verification, and are not currently suspended.

Now we might write:

if (
user.age >= 18
and user.identity_verified
and not user.suspended
):
allow_access()

That's still relatively clear.

But six months later, someone changes the requirement.

Perhaps suspended users can now access the feature under certain conditions.

The code may remain syntactically clean.

What matters now is whether the reason behind the rule is visible.

That's where comments, documentation, tests, naming, and architecture become important.

Not to explain what the code does.

To explain why it does it.

Comments Were Never the Enemy

There is a popular idea in software development:

"Good code shouldn't need comments."

I understand where this comes from.

Bad comments are absolutely a problem.

This:

# Increment counter by 1
counter += 1

doesn't help anyone.

The code already tells us that.

But consider:

We intentionally use a 5-minute window here.
The payment provider can send duplicate webhook events during retry periods.

That's valuable.

The code cannot fully explain the reason.

The comment captures the decision.

And decisions are often more important than syntax.

This is why I don't think the goal should be:

"Write code that needs no comments."

The better goal is:

"Write code that doesn't need comments to explain what it does, but use comments when the why isn't obvious."

Abstraction Can Make Code Less Clear

Abstraction is one of the most powerful tools in software engineering.

It is also one of the easiest to overuse.

Suppose we have:

send_email(user)

That's easy to understand.

But imagine the implementation eventually becomes:

send_email()
↓
NotificationManager
↓
NotificationFactory
↓
EmailProviderResolver
↓
EmailStrategy
↓
TransportAdapter
↓
SMTPService

Every individual component might be perfectly designed.

But the original operation has disappeared beneath the architecture.

This is the abstraction paradox:

An abstraction is useful when it hides unnecessary complexity.

But too many abstractions can create a new kind of complexity:

understanding where the real work happens.

DRY Can Become Wet

One of the first principles many developers learn is:

Don't Repeat Yourself.

It's a good principle.

But I've also seen developers create complicated abstractions simply because two pieces of code looked similar.

The problem is that similarity doesn't always mean they have the same reason to exist.

Two functions can look almost identical today while representing completely different business concepts.

If we force them into one abstraction, we save a few lines of code but create a dependency between two things that may eventually evolve differently.

Now changing one behavior requires understanding the other.

We eliminated duplication.

And introduced coupling.

Sometimes a little duplication is cheaper than a lot of abstraction.

AI Makes This Problem More Interesting

This becomes particularly relevant in the age of AI-assisted coding.

AI is very good at producing code that looks professional.

It can create:

  • classes
  • interfaces
  • abstractions
  • helper functions
  • design patterns
  • error handling
  • documentation
  • tests

The resulting code can look remarkably polished.

But polished code isn't automatically good architecture.

AI doesn't always know which abstraction is worth having.

It may optimize for patterns it has seen repeatedly.

And because the generated code often looks sophisticated, developers may be less likely to question it.

This creates a new review problem.

We shouldn't only ask:

"Is this code clean?"

We should also ask:

"Did we make the system harder to understand than it needed to be?"

The Simplest Code Isn't Always the Best Code Either

I don't want to replace one extreme with another.

"Keep it simple" can become another dogma.

Sometimes complexity is necessary.

Distributed systems are complex.

Security systems are complex.

Financial software can be complex.

Operating systems are complex.

Large-scale applications are complex.

The goal isn't to eliminate complexity.

The goal is to make sure the complexity exists for a reason.

There is a huge difference between:

Necessary complexity

and

Accidental complexity

The first comes from the problem.

The second comes from how we chose to solve it.

Good engineering tries to minimize the second without pretending the first doesn't exist.

I Now Ask a Different Question During Code Review

When I look at code, I don't want to ask only:

"Is this clean?"

I want to ask:

Can I understand the main flow?

If I have to jump through ten files to understand a simple operation, something may be wrong.

Can I understand the important decisions?

Why does this condition exist?

Why is this timeout five seconds?

Why are we retrying three times?

Why are we using this database?

Can I change something safely?

If a small requirement change requires understanding the entire application, the architecture may be hiding too much.

Can a new developer understand it?

This is one of my favorite tests.

Not:

"Can the person eventually figure it out?"

But:

"Can they build the right mental model without needing the original author beside them?"

That's a much higher standard.

Code Is Communication

We often think of programming as communicating with computers.

That's only half the job.

We're also communicating with the next developer.

And sometimes that developer is us six months later.

The computer doesn't care whether the architecture is intuitive.

It executes instructions.

Humans have to maintain the system.

That's why readability matters.

But readability isn't simply about formatting.

It's about reducing the mental effort required to understand a system.

A beautifully formatted maze is still a maze.

The Best Code Might Be Boring

I increasingly like boring code.

A function that does one obvious thing.

A straightforward database query.

A small number of dependencies.

A simple API.

A clear data flow.

A comment explaining an unusual decision.

A test showing an important business rule.

Nothing clever.

Nothing impressive.

Just understandable.

That kind of code rarely gets celebrated.

Nobody posts:

"Look at this extremely boring function I wrote today."

But boring software can be extraordinarily valuable.

Because the people maintaining it don't have to fight it.

My New Definition of Good Code

If I had to simplify it, I'd say:

**Clean code is code that is well structured.

Clear code is code that is easy to understand.

Good code is code that solves the right problem with an appropriate amount of complexity.**

Those are three different things.

And sometimes they overlap beautifully.

But not always.

A codebase can be clean without being clear.

It can be clear without being particularly elegant.

And it can be technically imperfect while still being the right solution for the problem.

That's why I don't think software engineering should be a competition to produce the prettiest code.

The objective is to build software that people can understand, change, operate, and trust.

Maybe We Need Fewer Rules

Software engineering has accumulated an enormous number of principles.

SOLID.

DRY.

KISS.

YAGNI.

Design patterns.

Clean architecture.

Domain-driven design.

Microservices.

Event-driven architecture.

Functional programming.

Object-oriented programming.

All of these ideas can be useful.

But principles are tools.

They aren't laws of physics.

The moment a principle starts making the software harder to understand, we should be willing to question whether we're applying it correctly.

That's probably one of the most important engineering skills:

Knowing when a good rule has become a bad decision.

The Question I Want to Leave You With

The next time you review a piece of code, don't ask only:

"Is this clean?"

Ask:

"Can I understand why this exists?"

And then ask something even more important:

"Did we make this more complicated than the problem required?"

Because the best code isn't necessarily the code with the most elegant abstractions.

It isn't necessarily the code with the fewest lines.

And it certainly isn't the code that looks the most impressive.

Sometimes the best code is simply the code that makes the next developer say:

"Okay. I understand."

And perhaps that's a better definition of clean code than we usually give it.

Want More:

Head over to ReThynk AI to access our latest research, magazine articles, and developer resources. Click Here

Top comments (1)

Collapse
 
jaideepparashar profile image
Jaideep Parashar •

AI is making coding easier now, however, building ecosystem is hard now.