I Stopped Writing Clean Code — And My Productivity Doubled
Clean code dogma can slow you down. Here’s a pragmatic approach: ship working software first, refactor when it hurts, and keep readability over cleverness.
TL;DR
Obsessing over clean code up front often delays delivery and adds complexity you don’t need yet. Ship something that works, refactor when real pain appears, and prefer clear code over clever code. Your productivity—and your team’s—will thank you.
LogNroll Team
Engineering
Why “Clean Code” Can Backfire
We’re told that clean code is the goal: small functions, no duplication, perfect abstractions. In practice, chasing that ideal before you have a working feature leads to over-engineering, endless refactors, and delayed releases. The best code is often the code that ships and then evolves with real usage.
Example: you need to fetch active users and send a welcome email. The “clean” approach might look like this—abstract, generic, built for a future you don’t have yet:
// Over-engineered before the feature even works
interface IUserRepository { findActive(): Promise<User[]>; }
interface IEmailService { sendWelcome(user: User): Promise<void>; }
class WelcomeFlow {
constructor(
private repo: IUserRepository,
private email: IEmailService
) {}
async run() { /* ... dependency injection, interfaces, 3 files */ }
}Or you could ship something that works in one place, then extract interfaces when you actually have a second implementation. Ship first.
// Good enough to ship and iterate
async function sendWelcomeToNewUsers() {
const users = await db.query('SELECT * FROM users WHERE status = ?', ['active']);
for (const user of users) {
await sendEmail(user.email, 'Welcome!', getWelcomeBody(user));
}
}Four Shifts That Doubled My Output
Ship first, polish later
Get something working and in front of users. Refactor when you have real usage and feedback, not hypothetical future needs.
Good enough is often enough
Code that works and is maintainable beats code that is theoretically perfect but delayed by endless abstraction.
Refactor when it hurts
Let pain guide you. When duplication or complexity actually slows you down, then invest in cleanup—not before.
Readability over cleverness
Clear, boring code that anyone can change is better than elegant code that only you understand.
When to Refactor (and When Not To)
Refactor when you’re about to add a second use case that would duplicate logic, when a function has become too long to reason about, or when onboarding someone and the code is confusing. Don’t refactor “because it could be cleaner”—refactor when the mess is actually costing you time.
- Do refactor: When you’re touching the same area for the third time, when tests are hard to write, or when a new teammate is lost.
- Don’t refactor: Before the feature works, for hypothetical future requirements, or to satisfy an abstract “clean code” checklist.
Readability Beats Cleverness
The most maintainable code is often boring: clear names, straightforward control flow, a bit of duplication if it makes intent obvious. Clever one-liners and deep abstraction hierarchies might feel satisfying to write, but they slow down everyone who has to read and change the code later—including you in six months.
Clever (hard to scan):
const activeIds = [...new Set(arr.filter(Boolean).map(x => x.id))].filter(id => validIds.has(id));Readable (easy to change):
const ids = items.filter(item => item != null).map(item => item.id);
const uniqueIds = [...new Set(ids)];
const activeIds = uniqueIds.filter(id => validIds.has(id));Same behavior. The second version is a few lines longer but anyone can adjust the rules or add logging without decoding a one-liner.
Bottom Line
Clean code is a means to an end: maintainable, understandable software that delivers value. Getting there by shipping first and refactoring when it hurts will often make you more productive than trying to write perfect code from day one. Your future self—and your team—will thank you for the working feature and the clarity that came with it.