DEV Community

Muhammad
Muhammad

Posted on

Stop Writing Clean Code. Write Boring Code Instead.

"Clean code" has become the religion of the professional software developer. We have its sacred texts — Clean Code by Robert Martin, A Philosophy of Software Design by John Ousterhout, the writings of the Gang of Four. We have its commandments: functions shall do one thing, abstractions shall not leak, dependencies shall flow in one direction. We have its rituals: refactoring sprints, architecture review meetings, pull request threads debating whether a function is "too long."

And like most religions, it has quietly accumulated some dogma that, when examined carefully, causes real harm.

I want to make a case for boring code — and argue that the pursuit of "clean" has become one of the more reliable ways to make a codebase worse.


What We Mean When We Say "Clean"

When most engineers talk about clean code, they're gesturing at a cluster of related ideas: short functions, meaningful names, minimal duplication, clear abstractions, separation of concerns. None of these are bad ideas in isolation.

The problem is what happens in practice. "Clean" has become an aesthetic rather than an engineering criterion. Code gets refactored not because it's hard to change or hard to understand, but because it doesn't look like the platonic ideal some engineer has in their head.

I've seen a 40-line function that did one coherent thing get split into six functions with six names and a new abstraction layer — because the original was "too long." I've seen working, well-tested code rewritten in a more "functional" style because the previous author had used for-loops. I've seen simple conditional logic replaced with a Strategy pattern because the reviewer felt that direct conditionals were "procedural."

In each case, the refactoring made the code harder to follow, harder to debug, and harder to delete. It made the code cleaner, in the aesthetic sense, and worse, in the engineering sense.


The Abstraction Trap

The single most reliable way to damage a codebase long-term is to introduce the wrong abstraction at the wrong time. And our obsession with clean code, with DRY (Don't Repeat Yourself), with "separation of concerns" — pushes us toward abstraction almost by default.

Here's the thing about abstractions: they're bets. When you build an abstraction, you're betting that the two things you've unified will continue to change for the same reasons, in the same directions, at the same times. When that bet is right, the abstraction pays off. When it's wrong — and it's often wrong — you've bound together things that needed to evolve independently, and now every change to either one requires you to reason about both.

Sandi Metz's talk "All the Little Things" introduced an idea that I think about constantly: duplication is far cheaper than the wrong abstraction. A bit of code that's repeated in two places can be unified later, once you actually understand how it will need to change. An abstraction built too early locks in assumptions that turn out to be false, and costs you far more to unwind than the original duplication would have cost to maintain.

The practical implication is almost the opposite of what most code review culture teaches: when you see two pieces of similar code, your first instinct should not be to unify them. It should be to ask what would have to be true for these two things to always change together. If you can't answer that confidently, leave them separate.


Boring Code Is Predictable

Let me describe what I mean by boring code, because I don't mean sloppy code or lazy code.

Boring code is code where, when something goes wrong at 2am, you can find the problem in five minutes. It's code where a new engineer can read the relevant function and understand what it does without jumping through three layers of abstraction and two interface definitions. It's code that does the obviously right thing instead of the elegantly clever thing.

Boring code tends to be:

Explicit rather than clever. It says what it does. It doesn't require you to know the design pattern to understand the intent. An explicit conditional branch that reads like English is almost always preferable to a polymorphic dispatch that requires you to know which concrete class is in scope.

Flat rather than deeply nested. Not because nesting is aesthetically bad, but because flat call stacks are faster to navigate in a debugger, easier to read in a stack trace, and simpler to test.

Longer rather than fractured. A 60-line function that does one coherent workflow is often easier to understand than six 10-line functions that each do a fragment of it. The question isn't "is this function long?" but "can I understand what this function does end-to-end without constant context switching?"

Direct about its dependencies. Dependency injection frameworks, service locators, and ambient context make code harder to trace. Passing dependencies explicitly — even if it makes function signatures a bit longer — makes the code easier to reason about and test.


The Naming Problem

Here's a subtle way that clean code culture makes things worse: it places enormous emphasis on naming, in a way that encourages naming things that shouldn't exist.

When you introduce a new abstraction — a class, a function, a module — you have to name it. The act of naming creates cognitive overhead: now there's a new concept in the codebase that readers need to build a mental model of. Good naming reduces that overhead. But no naming at all — because the abstraction doesn't exist — reduces it to zero.

I've watched engineers spend twenty minutes in a pull request arguing about whether a helper function should be called processUserData, transformUserRecord, or normalizeUserPayload. The argument is real and the stakes are legitimate — names matter. But the more important question, which often goes unasked, is: should this function exist at all? Is the code clearer with the extraction, or would it be clearer if the logic just lived inline where it's used?

The answer is often "inline is clearer," and the reason we didn't start there is that we were pattern-matching against our idea of what clean code looks like.


On Being Hard to Delete

One of the best tests for a piece of code is: how hard is it to delete?

Code that's easy to delete is code that's doing exactly one thing, has minimal dependencies, doesn't reach into other parts of the system, and has clear tests that tell you what you'll break if you remove it. Code that's hard to delete is code that's become load-bearing — other things depend on it, it carries implicit assumptions that other parts of the system rely on, and changing it sets off unexpected cascades.

Clean code culture, paradoxically, often produces code that's hard to delete. When you've carefully designed a class hierarchy, extracted everything into interfaces, built a layered architecture with a domain model and repository pattern and service layer — you've created a system where everything is neatly separated and also tightly coupled conceptually. Removing any piece requires understanding how the other pieces have been designed around it.

Boring code, because it tends toward the explicit and the flat, tends to be easier to delete. Dependencies are obvious. Scope is limited. If the thing doesn't work anymore or isn't needed, you can see clearly what it touches and cut it out.


When "Clean" Is Actually Right

I don't want to overcorrect. There are patterns from clean code culture that I think are unambiguously good and that I apply consistently.

Meaningful names for things that matter. A well-named variable in a complex algorithm is worth investing time in. The name should capture why, not just what.

Tests that document intent. A test suite that reads like a specification of behavior is one of the clearest forms of documentation. The discipline around testability from clean code culture has made codebases dramatically more maintainable.

Small, stable interfaces at system boundaries. Where two systems or services touch, a clean, minimal interface protects both sides. This is where abstraction investment pays off reliably.

Consistent patterns within a team. If your team has agreed on how you do error handling, how you structure modules, how you name things — consistency beats cleverness every time. The value isn't in any individual pattern, it's in the predictability across the codebase.

The problem isn't clean code ideas. The problem is applying them without asking whether they're making the specific code in front of you easier to understand, change, and delete — or just making it look more like the ideal.


What I'd Change About Code Review Culture

Most code review threads about style and cleanliness are asking the wrong question. They ask: does this code conform to our aesthetic? They should ask: if this code breaks in production, how quickly can we diagnose it? If a new engineer joins next month, how long until they understand this? If we need to change this behavior in six months, what's the blast radius?

Those questions have concrete answers that you can reason about. Whether a function is "too long" or an abstraction is "clean enough" does not.

I'd also add a new norm to most teams I've worked with: before extracting an abstraction, articulate what you're betting. Say it out loud in the PR: "I'm extracting this because I believe X and Y will always change together, and here's why." That single discipline — making the bet explicit — would prevent a significant fraction of the premature abstractions I've seen.


The Actual Goal

The goal of software engineering is not clean code. It's software that works, is easy to change, and survives contact with the people who will work on it over time.

Sometimes clean code and boring code are the same thing. More often, in my experience, they're in tension — and when they are, boring wins. The codebase that a tired engineer can navigate at 2am. The function that a junior developer can modify without being afraid they've misunderstood the abstraction. The module that can be deleted entirely when the product requirement changes without leaving load-bearing assumptions scattered through four other layers.

Boring isn't glamorous. It won't get you compliments in code review. But it'll get your users' problems fixed, your team's velocity up, and your own stress levels down.

Write the boring code. Let someone else win the aesthetic argument.


What's your take? I've found this framing useful, but I'm genuinely curious where clean code culture has served your team well — or where it's burned you. Drop a comment below.

Top comments (2)

Collapse
 
maame-codes profile image
Maame Afua A. P. Fordjour

Thanks for the advice to focus on being clear rather than being clever!

Collapse
 
nandofm profile image
Fernando Fornieles

Having clean code, something easy to read and understand, is good and a way to ease the onboarding of a new developer. What is bad is apply it without judgement. A method having more than a certain number of lines is just a sign that maybe it is doing more stuff than needed, but it could be right. Why not? A piece of code with nested loops can be a code smell but right because this could be an essential complexity of the system.

At the end the important thing is to be consistent across the project, this reduces cognitive load sometimes more that trying to be the clean coder ever :-)

Following best practices like Clean Code, SOLID principles, Design Patterns, ... is good, what is bad is to follow them blindly, without judgement.

Good read!