"Clean Code" by Robert C. Martin is treated like gospel in our industry. And I get it — the book genuinely improved how millions of developers think about code.
But after 4 years of writing code professionally, I've come to a controversial conclusion:
Blindly following "Clean Code" principles is making our codebases worse.
Before you sharpen your pitchforks, hear me out.
The Problem With Small Functions
Clean Code says: "Functions should do one thing. They should do it well. They should do it only."
Sounds great in theory. In practice, it creates this:
// "Clean" version - 8 functions across 3 files
function processOrder(order) {
validateOrder(order);
const total = calculateTotal(order);
const discount = applyDiscount(total, order.coupon);
const tax = calculateTax(discount, order.region);
const finalAmount = addTax(discount, tax);
chargeCustomer(order.customer, finalAmount);
sendConfirmation(order.customer, order);
updateInventory(order.items);
}
// What you actually need to understand: 8 function signatures,
// 3 files, and constant jumping between them
// "Dirty" version - 1 function, 25 lines
function processOrder(order) {
if (!order.items.length) throw new Error('Empty order');
let total = order.items.reduce((sum, item) => sum + item.price * item.qty, 0);
if (order.coupon) {
const discount = coupons[order.coupon];
if (discount) total *= (1 - discount);
}
const tax = total * TAX_RATES[order.region];
const finalAmount = total + tax;
await stripe.charge(order.customer.paymentId, finalAmount);
await mailer.send(order.customer.email, 'order-confirmation', { order });
await db.inventory.decrementMany(order.items);
}
The second version is longer but infinitely more readable. You can understand the entire flow without jumping between files. Every line tells you exactly what happens and in what order.
The Naming Obsession
Clean Code: "Variable names should be meaningful and descriptive."
The result:
// Over-engineered naming
const numberOfItemsInShoppingCart = cart.length;
const isUserCurrentlyLoggedInToTheApplication = !!session.user;
const listOfProductsThatAreCurrentlyOnSale = products.filter(p => p.onSale);
// Just... normal naming
const itemCount = cart.length;
const isLoggedIn = !!session.user;
const saleProducts = products.filter(p => p.onSale);
Context matters. Inside a CartComponent, everyone knows what items means. You don't need cartItemsForCurrentUserSession.
The Abstraction Addiction
This is the big one. Clean Code teaches that abstraction is always good. DRY (Don't Repeat Yourself) is law.
But here's what actually happens:
Day 1: Two functions share 5 lines of code.
Day 2: You extract a shared function. Feels good.
Day 30: The two callers now need slightly different behavior.
Day 31: You add a parameter flag. doThing(data, { legacy: true })
Day 60: Three more callers, three more flags.
Day 90: The "shared" function has 8 parameters, 4 flags, and nobody understands it.
This is called the wrong abstraction, and it's worse than duplication.
// The "clean" abstraction that grew into a monster
function formatData(data, {
includeTimestamp = false,
useLocalTime = false,
format = 'json',
includeMetadata = false,
stripNulls = true,
flattenNested = false,
customTransform = null,
legacyMode = false
} = {}) {
// 200 lines of branching logic
}
// vs. just writing what you need
function formatUserForAPI(user) { /* 10 lines */ }
function formatOrderForEmail(order) { /* 12 lines */ }
function formatLogEntry(entry) { /* 8 lines */ }
Duplication is far cheaper than the wrong abstraction.
The Single Responsibility Trap
SRP (Single Responsibility Principle) is the most misunderstood principle in software.
Developers interpret it as "every class should do one tiny thing." So you end up with:
UserValidatorUserSanitizerUserTransformerUserFormatterUserSerializerUserRepositoryUserServiceUserControllerUserFactory
Nine files to handle a user. The actual logic? Maybe 100 lines total, spread across 9 files with 9 constructors and 9 sets of imports.
SRP doesn't mean "one thing." It means "one reason to change." A UserService that handles creation, validation, and formatting has ONE reason to change: when user requirements change. That's fine.
What I Do Instead
1. Optimize for Reading, Not Writing
Code is read 10x more than it's written. I optimize for the person who will read my code in 6 months (usually me, confused and angry).
This means: inline over abstraction, colocate over separate, explicit over clever.
2. The Rule of Three
Don't abstract until you have THREE concrete uses. Not two. Not "I might need this later." Three actual, current uses.
3. Functions Can Be 30 Lines
A 30-line function that tells a clear story is better than ten 3-line functions you have to mentally stitch together.
4. Delete Aggressively
The best refactoring is deletion. If nobody uses it, it shouldn't exist.
The Real "Clean Code"
Code is clean when:
- A new team member can understand it quickly
- You can change it without breaking unrelated things
- It does what it says it does
- There's not more of it than necessary
Notice what's NOT on this list: small functions, maximum abstraction, zero duplication, design patterns everywhere.
Clean code isn't about following rules. It's about empathy for the next reader.
I write a lot about practical software development — the stuff that actually works in production, not just in textbooks. More at boosty.to/swiftuidev.
Top comments (0)