Clean code isn't what I thought it was
What working on real systems taught me about maintainable code.
My second job was the first time I worked with an international team where everyone had ten or more years of experience. I had maybe two. It was also the first time I was part of proper code reviews, branching strategies, and pull request workflows. Everything felt new and slightly intimidating.
One of my first tasks was adding spacing between two elements. It should have been a simple margin or padding change, but I added a <br> tag instead. The feedback on that PR was polite but clear, and it made me a little embarrassed.
That moment, along with dozens of similar ones, made me want to get better. I started reading about clean code and caring deeply about how my code looked. Small functions, no repetition, everything abstracted and organized. For a while, that served me well. It helped me grow from a junior developer into someone who could write code that passed review without a wall of comments.
But over time, as I worked on larger systems with real users and real constraints, I started noticing that the rules I had learned didn't always hold up. Sometimes the "clean" approach made things worse, and sometimes messy-looking code worked better than the elegant version I would have written.
This post is about how my definition of clean code expanded. I still believe in the principles I learned early on. I'd just add a few things to them now.
What I thought clean code meant
When I first started paying attention to code quality, my idea of clean code was mostly about appearances. If the code looked organized and followed certain patterns, it was clean. If it didn't, it wasn't.
I believed in small functions for everything. If a function was longer than fifteen or twenty lines, something was wrong. I would extract pieces into helpers even when they were only used once, just because the parent function felt "too long."
I was strict about DRY. Any time I saw similar logic in two places, I would immediately pull it into a shared utility or a reusable component. It didn't matter if the two use cases were genuinely related or just happened to look alike at the time.
I also loved building abstractions early. If I was creating a component, I would already be thinking about all the ways it could be reused later and designing it to handle cases that didn't exist yet. I was making a new component for the smallest of differences, convinced that someday we would need it for a specific use case that never came.
And I'll admit I had a weakness for clever solutions. Writing a compact, elegant utility function felt like a win, even if it took someone else twice as long to understand what it did. Clever code only feels good while you're writing it. After that, everyone who touches it pays the price.
None of this was coming from a bad place. I genuinely wanted to write good code. But I was optimizing for how code looked in the moment, without thinking much about how it would hold up over time.
What production systems taught me
Messy code that works is not the enemy
I've inherited a frontend codebase written by a backend developer twice in my career. If I had a nickel for every time that happened, I'd have two nickels. That's not a lot, but it's funny that it happened twice.
By any textbook standard, both were messy. Lots of copy-pasting, components stretched into thousands of lines, and the kind of structure that makes you sigh when you first open it.
And yet, both systems worked.
They were stable, shipped, and served their purpose. Users didn't care what the code looked like underneath. That forced me to ask myself an uncomfortable question: if the code works and users are happy, what exactly is the problem?
I'm not saying we should all write sloppy code on purpose. Those codebases were genuinely hard to navigate, and working in them was slower than it needed to be. But they taught me that "messy" and "broken" are not the same thing. I've seen ugly systems serve users reliably for years and elegant ones collapse under the first real requirement change. What actually causes problems is code that's hard to understand and hard to change, regardless of how it looks.
Early abstraction costs more than duplication
I used to treat duplication as a problem that needed to be solved immediately. The DRY principle was practically a reflex.
What I didn't appreciate was that duplication is only a real problem when the duplicated pieces are likely to evolve together. And in practice, they often don't. Two components that look the same today might need to go in completely different directions next month, and if you've already tied them together with a shared abstraction, you're now fighting the code to pull them apart.
The clearest lesson came when I was building an MVP for my own app. We made a deliberate decision to skip early optimization and tolerate duplication, because we weren't sure if we were going to keep any of that code. Sure enough, we pivoted a few months later and scrapped most of it. Had we spent the time making everything DRY and properly abstracted, all of that effort would have been wasted.
Duplication is cheap to fix later once you actually understand the pattern. Bad abstractions are a different story, because everything that depends on them has to change when you try to undo them.
Architecture matters more than style
Early in my career, most of my attention went to the code inside a file. Was the function clean? Were the variable names good? Was the logic well-structured? Those things matter, but they matter a lot less than where the code lives in the first place.
In one project, I extracted inline data fetching logic from large components into separate service files. Each service was maybe fifty or sixty lines of straightforward code. But the difference was enormous. When something broke in an API call, we could open a single small file and find the problem, instead of scrolling through a thousand-line component trying to figure out where the fetching logic was buried between rendering, state management, and event handlers.
Good architecture means a new developer joining the team can find their way around.
Refactor when there's pain, not when there's opportunity
When I joined a project that clearly needed structural improvements, my first instinct was to fix everything. Some of those improvements were genuinely needed. But at some point I had to stop, because the remaining improvements weren't solving real problems. They were just making the code look nicer.
This was a hard lesson because refactoring feels productive. But refactoring without a clear reason is just reorganizing your desk instead of doing your work. It feels good in the moment, but it doesn't move anything forward. And it introduces risk, because every change to working code is a chance to break something.
Now I follow a simple rule: refactor when something is actively causing pain. A component is slow? Fix it. A file is so long that people keep introducing bugs? Break it up. But if the code is just ugly and it works fine, leave it alone.
What clean code means to me now
Clean code, to me, is code that makes the next person's job easier. That next person might be a teammate, a new hire, or me six months from now having forgotten everything about why I wrote it.
Can someone open this file and understand what it does without a walkthrough? When requirements shift, can this code adapt without a rewrite? Does it behave the way you'd expect from reading it? Does it follow the same patterns as the rest of the codebase? Those are the questions I ask now.
Write code that's easy to read, easy to find, and easy to change. Everything else is secondary.
