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 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.
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.
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.
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.
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.
