After 10+ years in this industry, I've reviewed thousands of lines of code. Pull requests from juniors, seniors, leads, architects. Code written at 2 PM and code written at 2 AM. Code that made me smile and code that made me close my laptop and go for a walk.
Here's what I've learned: bad code doesn't look bad at first. It creeps in. One extra parameter. One more if-statement. One "temporary" workaround that's still there three years later.
But there are patterns. Five of them show up everywhere — in every codebase, every language, every team. And the good news? You can fix all five of them in one day. No rewrite. No architecture overhaul. Just targeted improvements that make your code immediately more readable.
The 80/20 of clean code: These 5 patterns account for roughly 80% of readability problems I see in code reviews. Fix them, and your teammates will genuinely notice the difference on Monday morning.
Sign 1: Methods longer than 20 lines
You open a file and see a method that's 150 lines long. It validates the input, applies business rules, calls the database, sends an email, logs the result, and handles three different error cases. All in one method.
Here's the problem: your brain can hold about 7 things in short-term memory. A 150-line method asks you to hold 30. You can't. Nobody can.
The fix is deceptively simple: one method, one task.
// ❌ The 150-line monster
fun processOrder(order: Order) {
// Validate
if (order.items.isEmpty()) throw ValidationException("No items")
if (order.customerId == null) throw ValidationException("No customer")
for (item in order.items) {
if (item.quantity <= 0) throw ValidationException("Bad quantity")
if (item.price <= 0) throw ValidationException("Bad price")
}
// Business logic
val total = order.items.sumOf { it.price * it.quantity }
val discount = if (total > 10000) total * 0.07 else 0.0
val finalAmount = total - discount
// Save to DB
val entity = OrderEntity(
id = UUID.randomUUID(),
customerId = order.customerId,
total = finalAmount,
status = "PROCESSED"
)
database.save(entity)
// Send email
val customer = customerRepo.findById(order.customerId)
emailService.send(
to = customer.email,
subject = "Order ${entity.id} confirmed",
body = "Your order of $finalAmount has been processed."
)
// ... and it keeps going for 100 more lines
}// ✅ After: each method does ONE thing
fun processOrder(order: Order) {
validate(order)
val processed = applyBusinessRules(order)
save(processed)
notifyCustomer(processed)
}
private fun validate(order: Order) {
require(order.items.isNotEmpty()) { "No items" }
requireNotNull(order.customerId) { "No customer" }
order.items.forEach { item ->
require(item.quantity > 0) { "Bad quantity" }
require(item.price > 0) { "Bad price" }
}
}
private fun applyBusinessRules(order: Order): ProcessedOrder {
val total = order.items.sumOf { it.price * it.quantity }
val discount = calculateDiscount(total)
return ProcessedOrder(order, total - discount)
}Sign 2: Magic numbers
You're reading through the code and you see this:
if (amount > 10000 && days > 30) { fee = amount * 0.07 }
What's 10000? A currency limit? A fraud threshold? Something from a regulation? You don't know. Nobody knows. The person who wrote it left the company two years ago.
These are magic numbers — values that appear in code without explanation. They're a readability nightmare because they force every reader to guess the intention.
The fix is trivial: named constants. Give every number a name that explains its purpose.
// ❌ Magic numbers — what do these mean?
if (amount > 10000 && days > 30) {
fee = amount * 0.07
}
// ✅ Named constants — self-documenting
val LARGE_TRANSACTION_THRESHOLD = BigDecimal("10000")
val RETENTION_PERIOD_DAYS = 30
val EARLY_WITHDRAWAL_FEE_RATE = BigDecimal("0.07")
if (amount > LARGE_TRANSACTION_THRESHOLD && days > RETENTION_PERIOD_DAYS) {
fee = amount * EARLY_WITHDRAWAL_FEE_RATE
}Sign 3: Nested if-else pyramids
Code indented 5+ levels deep. An if inside an if inside an if inside an else inside an if. Like a pyramid of uncertainty. You have to scroll horizontally to read it. Your brain has to track every condition to know what code actually runs.
The fix is a technique called early return (or guard clauses). Instead of nesting deeper and deeper, you handle the "not valid" cases first and return immediately. Then the happy path sits at the top level, easy to read.
// ❌ The pyramid of doom
fun process(user: User?, order: Order?) {
if (user != null) {
if (user.isActive) {
if (order != null) {
if (order.isValid) {
if (order.items.isNotEmpty()) {
// FINALLY — the actual logic
// 5 levels deep, can barely read it
processOrder(order)
}
}
}
}
}
}
// ✅ Guard clauses — flat and readable
fun process(user: User?, order: Order?) {
if (user == null) return
if (!user.isActive) return
if (order == null) return
if (!order.isValid) return
if (order.items.isEmpty()) return
// Business logic at top level — easy to read
processOrder(order)
}Sign 4: Comments that explain HOW, not WHY
Most comments in code are useless. Not because comments are bad — but because they explain things the code already says.
// increment i by 1 above i++? Thanks, I could read that.
The code already tells you how. Comments should tell you why. Why does this number need +1? Why are we retrying exactly 3 times? Why is there a sleep here? Those are the questions comments should answer.
// ❌ Useless — the code already says this
// increment index by 1
i++
// loop through all items
for (item in items) { ... }
// ✅ Useful — explains WHY, not HOW
// +1 because the API uses 1-based indexing
val apiIndex = localIndex + 1
// Retry 3 times because the external payment service
// has transient failures during peak hours (see INC-2847)
repeat(3) { callPaymentService() }
// Sleep 500ms because the rate limiter allows
// max 2 requests per second (see API docs §3.2)
Thread.sleep(500)Sign 5: The Mega Class
You know the one. A class that's 2000+ lines long. It handles user management, sends emails, generates reports, connects to the database, formats dates, and somehow also knows about the tax calculation logic. It's the class that does everything.
This violates the Single Responsibility Principle — the S in SOLID. A class should have one reason to change. When your UserService knows about email templates, you have a problem.
The fix: split responsibilities into separate classes. The diagram below shows exactly what I mean.
Clean code is not a talent. It's a habit. Start with one method, one file. Small improvements every day lead to readable code in a month.