The Myth of ‘Clean Code’ in Large Projects: Why Perfection Can Be the Enemy of Progress
Yacine Ouardi

We’ve all heard it before: “Write clean code.” It’s practically a religion in our industry. Books, blog posts, and Twitter threads praise the beauty of neatly organized files, perfect abstractions, and DRY (Don’t Repeat Yourself) principles. And don't get me wrong—I was (and sometimes still am) a believer. But after working on real-world projects, especially at Legal Doctrine, I've realized that clean code, as we imagine it, can become a trap.
This post is my honest reflection on why the obsession with clean code, especially in large or fast-moving projects, can actually hurt more than it helps.
The Real World Is Messy
In my early days, I used to spend hours refactoring code just to make it look "cleaner." I’d obsess over naming, break down components into ultra-small pieces, and hunt for any duplicated logic to abstract away. It felt productive, but later I’d realize I was solving problems that didn’t exist yet.
One vivid memory: I refactored a React component to be beautifully abstracted, only to have a new feature request arrive a week later that completely broke my neat structure. I ended up rewriting everything. It was a harsh lesson: clean code is meaningless if it isn't flexible for change.
Clean Code vs. Sustainable Code
What I’ve learned is that there’s a difference between clean code and sustainable code.
- Clean code looks nice now.
- Sustainable code survives real-world change.
For example, I used to write checks like this:
javascript
// BEFORE: Hard to read
if (user && user.profile && user.profile.isActive && !user.profile.isBanned) {
// do something
}
Then, thinking I was being “clean,” I refactored:
javascript
// AFTER: Clearer
const isValidUser = user?.profile?.isActive && !user?.profile?.isBanned;
if (isValidUser) {
// do something
}
That’s fine—readability matters. But I learned the hard way that overdoing it (like abstracting isValidUser
into a separate hook too soon) can create unnecessary indirection. Simple and clear is better than overly abstract.
The Danger of Over-Engineering
There’s a huge temptation to abstract everything early. I fell into that trap when I created a withLoading
higher-order component for a single use case:
jsx
// Premature abstraction (no other components use this yet)
const withLoading = (Component) => (props) => {
return props.isLoading ? <Spinner /> : <Component {...props} />;
};
// Could’ve just done this until a second use case appears:
{isLoading ? <Spinner /> : <UserProfile {...props} />}
At first, it felt elegant. Later, my teammates were annoyed because it was harder to trace what was happening. Lesson: abstraction is a tool, not a goal.
Clean Code Culture Can Block Progress
Here’s something I didn’t expect: the pursuit of clean code can block progress on a team. I’ve seen developers hesitate to ship features because they’re “not proud” of their code yet. But in fast-paced environments, shipping value often matters more than perfecting code.
Yes, broken windows theory is real—if everything is messy, people stop caring. But there’s a balance. Sometimes, “good enough” code is exactly what’s needed to move forward.
When to Refactor (and When Not To)
Refactoring is important—but only when it’s justified. I follow a simple rule now:
Refactor when the pain of NOT refactoring is bigger than the effort to refactor.
If a piece of code is:
- Hard to read and
- Actively changing or
- Causing bugs or confusion
—then yes, it’s time to refactor.
Otherwise? It can wait.
A Personal Shift in Mindset
Over the past two years, I’ve shifted from chasing perfect code to chasing effective code. Code that does its job, is understandable by the team, and is flexible enough to change. That doesn’t mean writing sloppy code, but it does mean accepting a bit of imperfection for the sake of progress.
I’ve also learned to trust the team. Clean code isn’t just about what I think is clean—it’s about what’s maintainable and clear to everyone.
Final Thoughts
The clean code movement gave us great principles, but it’s not a religion. In large, evolving projects, the priority should be on clarity, collaboration, and adaptability. Sometimes, the ugliest solution today paves the way for faster, better iterations tomorrow.
So here’s my honest takeaway:
Don’t aim for clean code. Aim for code that works well, evolves easily, and keeps your team moving forward. The beauty can come later.