← Back to blog
· 6 min read

Code Smell Explained: From Buzzword to Practical Refactoring Guide

What code smells really are, how to spot them, and how the tooling evolved from Fowler's catalog to AI-powered detection. A then-vs-now perspective from a real SO question.

architecture refactoring stackoverflow best-practices

Code Smell Explained: From Buzzword to Practical Refactoring Guide

In 2015, while learning Swift and trying to write “clean code,” I asked a question on Stack Overflow in Portuguese about Code Smell. I’d seen the term everywhere — blog posts, conference talks, Martin Fowler’s website — but I couldn’t pin down what it actually meant in practice. Was it the same as violating DRY? Was it just another name for “bad code”?

That question got 26 upvotes, which tells me I wasn’t the only one confused. Eleven years later, I have a much clearer picture — and better tools to deal with it.

The 2015 Understanding: Catalog-Driven Thinking

Back then, my understanding of code smells was purely academic. I’d read Fowler’s Refactoring book (well, parts of it), skimmed the Wikipedia page, and memorized a list of names: Long Method, God Class, Feature Envy, Shotgun Surgery. It felt like collecting Pokemon — I could name them, but I couldn’t always recognize them in the wild.

The biggest confusion was mixing up code smells with design principles. My SO question specifically asked whether code smell was “the same thing” as violating DRY, KISS, or YAGNI. The answer I got (and eventually understood) is no — but the relationship is more nuanced than a simple “they’re different.”

I treated code smells as rules: “if a method has more than 20 lines, it smells.” That mechanical approach missed the point entirely.

Common Code Smells (With Actual Code)

Let me show you what I wish someone had shown 2015-me — concrete examples instead of abstract definitions.

Long Method

// This function does authentication, validation, logging,
// database insertion, and email sending. All in one place.
async function registerUser(req, res) {
  const { email, password, name } = req.body;
  if (!email || !email.includes('@')) return res.status(400).send('Invalid email');
  if (!password || password.length < 8) return res.status(400).send('Weak password');
  if (!name || name.length < 2) return res.status(400).send('Name too short');
  const existing = await db.users.findOne({ email });
  if (existing) return res.status(409).send('Email taken');
  const hashed = await bcrypt.hash(password, 12);
  const user = await db.users.insertOne({ email, password: hashed, name, createdAt: new Date() });
  await sendEmail(email, 'Welcome!', `Hi ${name}, thanks for signing up.`);
  logger.info(`New user registered: ${email}`);
  res.status(201).json({ id: user.insertedId });
}

This works. It passes tests. But every time you need to change anything — validation rules, email templates, logging format — you’re editing this one function. The smell here isn’t the line count, it’s the number of reasons this function has to change.

God Class

class UserManager {
  createUser(data) {
    /* ... */
  }
  deleteUser(id) {
    /* ... */
  }
  sendWelcomeEmail(user) {
    /* ... */
  }
  generateInvoice(user) {
    /* ... */
  }
  calculateDiscount(user) {
    /* ... */
  }
  exportToCSV(users) {
    /* ... */
  }
  syncWithCRM(user) {
    /* ... */
  }
  validateAddress(user) {
    /* ... */
  }
}

When a class name ends with “Manager,” “Helper,” or “Utils” and it has 15+ methods touching unrelated domains, you’ve got a God Class. UserManager here handles email, billing, export, CRM, and address validation — each of those deserves its own module.

Feature Envy

function calculateShippingCost(order) {
  const weight = order.items.reduce((sum, item) => sum + item.weight, 0);
  const distance = getDistance(order.warehouse.zip, order.customer.address.zip);
  const rate = order.customer.isPrime ? 0.5 : 1.0;
  return weight * distance * rate;
}

This function reaches deep into the order object — its items, warehouse, customer, and customer’s address. It knows more about Order internals than its own module. This logic probably belongs inside the Order class (or a dedicated ShippingCalculator that receives pre-extracted data).

Duplicated Code (The Subtle Kind)

// In userController.js
const user = await db.findById(id);
if (!user) return res.status(404).json({ error: 'Not found' });

// In orderController.js
const order = await db.orders.findById(id);
if (!order) return res.status(404).json({ error: 'Not found' });

// In productController.js
const product = await db.products.findById(id);
if (!product) return res.status(404).json({ error: 'Not found' });

It’s not copy-paste (the collection names differ), but the pattern is identical. A small middleware or helper would eliminate the repetition and centralize the 404 behavior.

The 2026 Approach: Smells Get Detected Automatically

Here’s what changed most dramatically since 2015: the tools caught up.

ESLint and SonarQube now have rules that flag code smells by default. max-lines-per-function, max-params, complexity (cyclomatic complexity threshold) — these aren’t style preferences, they’re smell detectors. SonarQube even categorizes issues as “Code Smell” explicitly and estimates the technical debt in minutes.

AI code review is the real game-changer. GitHub Copilot code review, Claude in the IDE, and tools like CodeRabbit catch patterns that rule-based linters miss. They can spot Feature Envy, suggest extracting a class, or flag that a function’s name doesn’t match what it actually does. I’ve had Claude catch a God Class pattern that had accumulated over 18 months of “just one more method” commits — something no linter rule would flag because no single commit was the problem.

IDE integrations close the loop. IntelliJ’s inspection system, VS Code extensions like SonarLint — they show smells in real-time as you type, with one-click refactoring suggestions. The feedback loop that used to take days (code review) or weeks (tech debt audits) now takes seconds.

This was the core of my 2015 confusion, so let me be explicit.

DRY, KISS, and YAGNI are principles — they tell you what to aim for. Don’t repeat yourself. Keep it simple. Don’t build what you don’t need yet.

Code smells are symptoms — they tell you something might be wrong. A long method is a symptom. Duplicated code is a symptom. A class with 30 fields is a symptom.

The relationship: violating DRY often produces the Duplicated Code smell. Violating KISS often produces Long Method or overly complex abstractions. Violating YAGNI produces Speculative Generality (building abstractions for use cases that never arrive).

But a code smell doesn’t always mean a principle was violated. Sometimes a 50-line method is the clearest way to express a complex algorithm. Sometimes duplication is better than the wrong abstraction. The smell is a signal to investigate, not an automatic verdict.

Key Takeaway

If I could go back and edit my 2015 SO question, I’d reframe it entirely. I was looking for a definition when I should have been looking for a mindset.

Code smells are symptoms, not diseases. They’re your codebase whispering “hey, look at this.” The cure is never mechanical — it’s not “method > 20 lines, therefore split.” The cure is understanding why the code is there, what it’s trying to do, and whether a different structure would make it easier to change.

The tools we have in 2026 — linters, AI reviewers, IDE inspections — are incredible at finding smells. But deciding what to do about them? That still requires a developer who understands the context. And that understanding, more than any catalog or acronym, is what makes refactoring effective.

Related Posts