I spend a lot of time at Bito running AI code reviews, both on my own projects and with the team.
Over time, I started noticing a pattern. The reviews weren’t just pointing out syntax issues or missing tests. They kept surfacing something subtler — code smells.
At first, I didn’t think much of it. If the code compiles and the feature works, isn’t that good enough? But the more I worked with smelly code, the more I realized how quickly it drags you down.
You open a file and suddenly it takes 20 minutes just to understand what’s happening. You go to fix a bug, and you’re scared to touch anything because one change might break five other things. You hand it off to a teammate, and they look at you like you just cursed them.
That’s when it clicked for me: code smells are not bugs, but they’re warning signs. Ignore them, and you end up buried in technical debt. Spot them early, and you save yourself a lot of pain later.
Since I keep running into this during AI code reviews, I thought I’d put together some thoughts on what code smells are, the ones that show up the most, and a few practical ways to deal with them.
Why Code Smells Matter
Code smells are easy to ignore in the beginning. The program runs, tests pass, and everything looks fine on the surface. But over time, those small issues start to grow.
A long function becomes harder to follow each time someone adds a new condition. A giant class turns into a dumping ground where nobody remembers what belongs where. Copy-pasted logic means fixing one bug in three different places.
The problem isn’t that the code stops working. The problem is that it slowly becomes harder to read, harder to test, and harder to change without breaking something else. This is how technical debt sneaks in.
I’ve seen it firsthand during reviews. A piece of code that seemed “good enough” in the beginning later took twice as long to debug because the design had rotted. If you work in a team, it also slows everyone else down, because they need to untangle the mess before they can even start adding new features.
That is why code smells matter. They are not just about clean code for the sake of clean code. They are about saving yourself and your team from pain later.
The Most Common Code Smell Patterns
Over time you start to notice the same kinds of smells showing up again and again. They look different in every codebase, but the patterns are surprisingly common. Here are a few that stand out the most.
Long Methods
Long methods make it hard to follow logic, and hard to test. Break them into small functions that do one thing.
Smell:
function processOrder(order) { // validate if (!order.id || !order.items) throw new Error('invalid') // calculate totals let subtotal = 0 for (const item of order.items) { subtotal += item.price * item.qty } let tax = subtotal * 0.08 // apply discounts if (order.coupon) { subtotal -= order.coupon.amount } // build payload const payload = { id: order.id, total: subtotal + tax } // send to billing sendToBilling(payload) // notify user sendEmail(order.userEmail, 'order processed') }
Refactor:
function validateOrder(order) { if (!order.id || !order.items) throw new Error('invalid') } function calculateTotal(items, coupon) { let subtotal = 0 for (const item of items) subtotal += item.price * item.qty if (coupon) subtotal -= coupon.amount const tax = subtotal * 0.08 return subtotal + tax } function processOrder(order) { validateOrder(order) const total = calculateTotal(order.items, order.coupon) sendToBilling({ id: order.id, total }) sendEmail(order.userEmail, 'order processed') }
Duplicate Code
Duplicate logic causes multiple fixes. Extract shared code once, then reuse it.
Smell:
function createAdminUser(data) { const user = { name: data.name, email: data.email, role: 'admin', createdAt: new Date() } saveUser(user) } function createGuestUser(data) { const user = { name: data.name, email: data.email, role: 'guest', createdAt: new Date() } saveUser(user) }
Refactor:
function buildUser(data, role) { return { name: data.name, email: data.email, role, createdAt: new Date() } } function createAdminUser(data) { saveUser(buildUser(data, 'admin')) } function createGuestUser(data) { saveUser(buildUser(data, 'guest')) }
Large Classes (God Objects)
A class with many responsibilities becomes hard to change. Split responsibilities into focused classes.
Smell:
class OrderService { constructor(db) { this.db = db } createOrder(data) { /* validate, calculate, save, notify */ } calculateTotals(items) { /* lots of logic */ } sendInvoice(order) { /* email logic */ } exportOrdersCsv() { /* file logic */ } }
Refactor:
class OrderCalculator { calculate(items, coupon) { /* totals logic */ } } class OrderRepository { constructor(db) { this.db = db } save(order) { /* db save */ } } class OrderNotifier { sendInvoice(order) { /* email logic */ } } class OrderService { constructor(calc, repo, notifier) { this.calc = calc this.repo = repo this.notifier = notifier } createOrder(data) { const total = this.calc.calculate(data.items, data.coupon) const order = { ...data, total } this.repo.save(order) this.notifier.sendInvoice(order) } }
Primitive Obsession
Passing raw primitives hides intent, and it makes validation and behavior scattered. Create small types or objects.
Smell:
function createUser(name, email, addressLine1, addressLine2, city, zip) { const user = { name, email, addressLine1, addressLine2, city, zip } saveUser(user) }
Refactor:
function buildAddress(line1, line2, city, zip) { return { line1, line2, city, zip } } function createUser(name, email, address) { const user = { name, email, address } saveUser(user) } // usage const addr = buildAddress('123 St', '', 'Pune', '411001') createUser('Asha', 'asha@example.com', addr)
Feature Envy
When a method reaches into another object to pull data, the logic likely belongs closer to that data. Move behavior to the right place.
Smell:
class OrderFormatter { format(order) { return `${order.user.firstName} ${order.user.lastName} placed order ${order.id}` } }
Refactor:
class User { constructor(firstName, lastName) { this.firstName = firstName this.lastName = lastName } fullName() { return `${this.firstName} ${this.lastName}` } } class OrderFormatter { format(order) { return `${order.user.fullName()} placed order ${order.id}` } }
Data Clumps
If the same group of values travels together, pack them into an object. This reduces errors and clarifies intent.
Smell:
function scheduleMeeting(title, startDate, endDate, organizerName, organizerEmail) { // lots of params passed around }
Refactor:
function scheduleMeeting(title, range, organizer) { // range is { startDate, endDate } // organizer is { name, email } } const range = { startDate: '2025-09-01', endDate: '2025-09-01' } const organizer = { name: 'Sam', email: 'sam@example.com' } scheduleMeeting('sync', range, organizer)
How to Spot Code Smells in Practice
Code smells are tricky because they do not break your build or throw an error. The code still runs, which makes it easy to miss them. But once you know what to look for, they start standing out everywhere.
Peer reviews and pair programming
Having another set of eyes on your code helps a lot. A teammate who has not been staring at the same file for hours will quickly notice when a method is too long, a class is too heavy, or logic feels out of place.
Automated tools
Linters and static analysis tools can catch certain smells, like duplicate code or unused variables. AI code review tools go a step further and point out design-level issues that humans might overlook during a busy sprint. This is something I see every day using Bito. Our blog on code smell detection goes deeper into how AI reviews help spot these patterns early.
Your gut feeling as a developer
Sometimes you just know something smells off. If you have to scroll too much, pass around too many parameters, or write the same code twice, that is usually a sign. Trust that feeling and take a closer look.
The goal is not to obsess over every small thing, but to develop awareness. Once you can recognize these signals, you can choose which ones are worth fixing now and which can wait.
When to Fix vs When to Let Go
One of the hardest parts of dealing with code smells is knowing when to act. Not every smell deserves your attention right away. Some are harmless quirks, while others will slow your team down if you leave them alone.
Fix right away
If a code smell is blocking readability, slowing down debugging, or creating duplicate logic, it is usually worth fixing on the spot. For example, a long method that you are already editing is the perfect candidate for a quick cleanup. Small changes made in context are the easiest wins.
Let it go (for now)
If the code works, is rarely touched, and nobody is struggling with it, you might not need to refactor immediately. A messy utility function that runs once a month is not as urgent as a controller that every developer touches daily.
The Boy Scout Rule
A good way to balance this is to follow the Boy Scout Rule: leave the code cleaner than you found it. If you touch a file for a new feature or a bug fix, take a moment to clean up the worst smells while you are in there. Over time, the whole codebase improves without big refactor projects.
The real skill is not fixing everything, but knowing what to fix now and what to leave alone. That discipline saves you from wasting time while still keeping the codebase healthy.
Top comments (0)