Skip to content

What Is a Code Smell?

Published:  at  05:53 AM

Table of Contents

Open Table of Contents

What Is a Code Smell?

Hello.

I spend my days developing software and reviewing code, and I believe that being able to sense code smells is extremely important in that work.

When I judge whether code is good or bad, I do not consult a book every single time.

Suppose, for example, that I come across an unusually long function during a code review. When I see it, I do not usually think about applying the contents of Martin Fowler’s Refactoring or The Art of Readable Code word for word. Instead, a function that is too long makes me feel that it would be difficult to unit test. If it is difficult to unit test, I naturally begin to wonder whether the function contains multiple responsibilities. That leads me to consider whether it can be split up.

The same is true of code with many levels of nested conditionals. The deeper the nesting becomes, the more conditions readers have to keep in mind as they work through the code. That increases cognitive complexity, so I start thinking about whether an early return could help or whether the conditional logic itself could be replaced with a different structure, perhaps by using a design pattern.

Naming is another example. Personally, I think it is desirable to be able to understand, to some extent, what a function does just by looking at its name. If the function name tells me very little and I ultimately have to read its implementation details, the effort required to understand the code increases. Of course, a name cannot express everything, so documentation such as docstrings or PHPDoc may be necessary. At the very least, though, I want to avoid knowing nothing about a function’s responsibility after seeing its name.

When I encounter code like this, a clear principle or a concrete alternative does not necessarily come to mind immediately—although sometimes one does. What comes first is a vague sense that “this could be simpler.” Is the code more complicated than the thing it fundamentally needs to accomplish? Is it strangely difficult to follow?

That kind of cognitive burden is one signal that makes me sense a code smell. Recently, I have come to believe that the ability to detect these smells is a fundamental skill for building software that meets its quality requirements.

A Code Smell Is Not Necessarily a Problem

That said, sensing a code smell does not automatically mean that the code is bad. A long function does not always need to be split up. Nested code does not always need to use early returns. Duplicated code does not always need to be made DRY.

For example, two modules with different responsibilities may happen to contain similar processing. If we extract it solely because “the code is duplicated,” we may couple modules that should remain independent. In that situation, I see no problem with the similar logic appearing twice. DRY is generally considered a good idea, but that does not make it a principle we must always apply.

In other words, to me, a code smell is not the problem itself. It is a signal that a problem may exist. In practice, it is perfectly normal for me to notice a smell during a review and leave no comment. After investigating, I may find that my own understanding was wrong. Something may be improvable but not worth calling out. Or it may bother me slightly, yet still be acceptable enough as it is.

Sensing a smell and deciding that the code should be changed are two separate things.

First, notice the discomfort. Then consider whether it points to a genuine software quality issue. Detecting a code smell is what starts that process, and I believe that beginning is very important.

Superficial Code Reviews

I was not always able to recognize code smells intuitively. Looking back at my earlier code reviews, I think they were far more superficial than they are today. Many of my comments were about awkward naming, a shorter way to write a particular expression, or an opportunity to reduce a few lines of code. At the time, I was reading classics such as The Art of Readable Code and Refactoring and focusing on how to apply what they said directly to real code. My reviews tended to stay at that surface level.

These days, I am less active in reviewing those superficial details, and I often leave them to others. I now look at more fundamental questions: Is this responsibility located in the right place? Should this domain constraint really be expressed through this implementation? Is the abstraction appropriate? Can this design withstand change? Compared with the past, I feel that I look beyond the appearance of the code itself and focus more on the essence behind it.

Facing a Legacy System

The experience that contributed most to this growth was replacing an existing legacy system with my own hands.

To replace a system, you naturally have to understand its current specifications. So I read the existing code, but I simply could not make sense of it. Control jumped from place to place, the dependencies were complex, and the whole system had become spaghetti. I was reading the code to understand the specifications, yet reading it did not reveal what those specifications were. On top of that, I could not see how far the effects of changing any one process would spread.

The most painful part was that the dependencies were too complex for me to write tests. Even when I wanted to write a unit test, the logic under test was surrounded by so many dependencies that I could not isolate it easily.

That experience taught me that poor internal quality is not merely the subjective problem of “messy code.” It is a productivity problem.

When internal quality is poor, it becomes difficult to understand the specifications or determine the impact of a change. Automated tests become difficult to prepare as well. Ultimately, changing the software itself becomes hard.

During the replacement, I learned concepts such as DDD and Clean Architecture while thinking deeply about how to transform legacy code with poor internal quality into a modern structure. Throughout that process, I worked hard to put the underlying problems into words: What is fundamentally wrong? Why is this code so difficult to change? How can it be improved?

AI coding tools like the ones we have today did not exist at the time, so I had to write the code myself and explain my reviews in my own words. I could not hand something I did not understand to an AI and receive a plausible answer. I often visited Stack Overflow too, which feels nostalgic now. I had no choice but to think for myself and keep going until I could explain the issue myself. That experience unquestionably sharpened my sense for code smells.

Knowledge and Experience Compress into Intuition

Seen in that light, the ability to detect code smells is not simply a hunch.

It naturally begins with knowledge:

  • A long function may have multiple responsibilities
  • Deep nesting increases cognitive complexity
  • Cohesion should be high and coupling low
  • Abstractions should be appropriate
  • Inheritance should satisfy the Liskov Substitution Principle
  • And so on

We learn ideas like these from books and articles on the internet, then immediately apply them to the code in front of us. Each time we put them into practice, we discover something we still do not fully understand. Why can this principle be applied here?

Through that repetition, knowledge and experience gradually become compressed and absorbed until they become part of us, allowing us to distinguish one code smell from another.

Once we reach that state, we sense a smell and examine the source of our discomfort. It then connects with design principles we learned before or problems we experienced in the past. Intuition comes first, and a concrete theory explains the reason afterward. That is what it means to me to be able to detect a code smell.

Code That Looks Clean but Smells Wrong by Design

When people hear “code smell,” they may picture something visibly obvious, such as a huge function or excessively deep nesting. In reality, however, plenty of code looks clean while still feeling wrong at the design level.

One example is code that does not use a value object where one would be appropriate. A value object could embed a domain constraint into the type itself, yet it is common to see primitive values used directly and checked condition by condition in the middle of the logic. Those checks might be organized into neat functions, and the code itself might look readable. Even so, I find myself wondering: Should this design allow an invalid state to be represented in the first place?

Design by contract provides another example. A domain boundary or constraint could be implemented as part of the system’s structure, yet some implementations enforce it repeatedly through conditional branches inside each individual operation. Because each piece of logic looks clean in isolation, the underlying problem may actually be harder to notice.

Inheritance is another easy-to-understand example. Suppose Class A and Class B perform the same operation, so a parent Class AB is created solely to share that implementation.

The reduction in duplicated code may look clean at first. Inheritance, however, should be used when an is-a relationship exists in accordance with the Liskov Substitution Principle. If A and B are not truly kinds of AB, I do not think inheritance should be used merely to share an implementation. Code like this may look perfectly clean on the surface. Without the ability to detect the smell properly, however, we cannot point out the real problem.

That is why recognizing smells at the design level is so important.

Separating Code Smells from Personal Preference

At the same time, it is dangerous to label every uncomfortable feeling in a code review as a “code smell,” because some of them are simply matters of personal preference.

For me, the boundary is whether I can logically explain the effect on software quality.

For example:

  • This dependency makes unit tests difficult to write
  • Placing the responsibility here increases the blast radius of a specification change
  • This name forces readers to inspect implementation details to understand the behavior, increasing cognitive load
  • This abstraction couples two modules that should remain independent
  • And so on

If I can explain an issue in those terms, I think it is reasonable to discuss it as a software quality concern.

On the other hand, if I cannot go beyond “this is how I would write it” or “I prefer this style,” then it is probably closer to a matter of taste.

Of course, because code reviews are performed by people, it is difficult to eliminate preference entirely. In a sense, allowing individual preferences to enter the process can help a team develop its own engineering culture. I do not think it is necessarily bad to turn a preference into a review comment, either. In that case, it is perfectly fine to make the premise explicit by saying, “This may just be my preference, but…”

Personally, I try to keep such preferences out of my reviews as much as possible. The implementer has intentions of their own, and if there is no quality issue, I think it is better to leave them some freedom to express those intentions in the code.

Naturally, experienced engineers will sometimes disagree about a code smell. When that happens, there is no clever answer: the team has to discuss it. That is another reason why it is important to build a team with a high degree of psychological safety in everyday work.

What Is Good Code?

After thinking this far, the discussion naturally leads to a more fundamental question: What is good code?

  • It is easy to read
  • It is easy to test
  • It is resilient to specification changes
  • It expresses business concepts appropriately
  • It has low cognitive complexity
  • It has high cohesion and appropriately limited coupling

I believe all of these qualities are important. However, they are not ends in themselves. What matters, in my view, is that they ultimately make the software easier to change.

Building software is only the beginning. When the business or market changes, the software naturally has to change with it. We add new features, revise existing specifications, and sometimes change the architecture itself as the number of users grows.

If internal quality is poor and changing the system becomes frightening, the software will no longer be able to keep pace with changes in the business. Software that is easy to change, on the other hand, can continually adapt its shape to meet business needs.

To me, then, good code is code with enough changeability to continually move the software in the direction that maximizes business value. Making code clean is not the goal in itself. What matters is what we ultimately gain by writing clean code.

AI Can Now Write Remarkably Plausible Code

One reason I am reconsidering code smells now is AI.

Today’s AI can write remarkably plausible code. Its naming is reasonably good, its functions are split into sensible sizes, it adds types in languages that support them, and it can generate tests.

When an existing codebase has clear design principles and already contains enough high-quality code, I think AI output can be quite consistent. The AI follows the established best practices as it generates new code.

Without that foundation, however, the results vary considerably. In real-world work, I often see code slip in that looks clean at first glance but is wrong at the design level.

The fact that it looks well organized makes the problem even more troublesome. AI has dramatically lowered the cost of producing superficially clean code. But looking clean on the surface and remaining easy to change over the long term are two different things.

“Implementation Skills” in the Age of AI

With that in mind, the meaning of implementation skills for software engineers will probably change. Now that AI can generate large amounts of code at high speed, review inevitably becomes the bottleneck if humans have to inspect every line of every diff.

There is, of course, an argument that reviews should be left to AI as well. But if AI writes the code, AI reviews it, and humans themselves cannot tell whether the output is good or bad, I seriously question whether a service can scale successfully over the long term.

When something goes wrong, people cannot verify whether the AI’s judgment was sound. More importantly, can someone who cannot judge the quality of AI-generated code be called a software engineering professional? Can they claim to have taken responsibility for the quality of the product delivered to users?

This is precisely why the ability to detect code smells may become even more important in the age of AI.

The arrival of AI is no reason to neglect our own fundamentals. On the contrary, evaluating AI output requires a strong foundation. In the end, what is fundamentally required of us does not seem to have changed very much from before AI to after it.

This Ability May Eventually Become Unnecessary

Even so, I do not believe that the ability to detect code smells will remain necessary forever. Imagine that one day we can give AI a specification and trust it to design, implement, test, and continually modify the software correctly. Imagine that its quality becomes reliably superior to that of humans. If we reach that point, people will no longer need to read code and judge its quality.

This is similar to compilers. Today, many software engineers do not routinely optimize at the assembly level because compilers handle much of that work with sufficient quality.

If AI comes to play the same role for code, code itself may become an abstraction layer that people no longer need to think about. At that point, the ability to detect code smells may no longer be necessary.

At least for now, however, we are not there yet. Especially when building large-scale enterprise systems, it is not realistic to leave everything to AI while humans cannot even judge the quality of what it produces. For now, we still need a well-developed sense of code smell.

Conclusion

A code smell is not a mechanical rule for identifying bad code. It is the sense of discomfort we experience when reading code: “Is this a little too complicated for what it fundamentally needs to do?” “Will changing this cause pain later?” “Is there something wrong with this design?”

Now that AI performs much of the coding, it seems to have become harder to internalize that sense for code smells. After all, asking AI can produce remarkably plausible code almost instantly.

My own eye for code changed because I wrote code myself, struggled with legacy code, replaced it, and thought deeply about why it was bad and how it should be improved.

In the past, implementing things myself gave me opportunities to examine design flaws on my own and learn by working through the implementation with my own hands. AI now removes much of that friction.

That is wonderful for productivity, of course. But we are also likely to lose some of the instincts we used to develop through that process. In that sense, I sometimes think it will be more difficult for engineers to learn to detect code smells than it was before.

One possible approach is to consciously experiment with the code we encounter every day. Ask yourself, “How would I refactor this further?” and “What differences would emerge if I actually changed its structure?”

In your own time, create a before-and-after example as a learning exercise. Then reflect on why one version is better and how that reasoning was incorporated into the design.

Ultimately, I suspect that this ability can only be developed, to some extent, through effort and hands-on practice.

These are my thoughts on code smells. I hope you found something useful in them.