How to Identify and Fix Code Smells

Code smells are subtle signs in your code that suggest deeper structural issues. They don’t break your code but can make it harder to maintain, understand, and expand. Ignoring them leads to technical debt, which slows down development and increases the risk of bugs.
Here’s what you need to know:
- Common Code Smells: These include bloated methods, misused object-oriented principles, redundant code, and excessive dependencies.
- Detection Methods: Combine manual reviews, static analysis tools (e.g., SonarQube, ESLint), and complexity metrics.
- Fixing Techniques: Refactor using methods like abstraction, breaking down large methods, and simplifying method calls.
- AI Assistance: Use agentic coding tools (e.g., Claude Code, Codex) and AI-augmented static analysis to detect smells and draft refactors – kept honest by tests and human review.
- Prioritisation: Focus on frequently updated or bug-prone areas with high complexity.
- Team Practices: Integrate refactoring into daily workflows, use automated tools, and ensure strong test coverage to avoid introducing new issues.
Common Types of Code Smells

5 Main Categories of Code Smells in Software Development
Improving code quality starts with recognising code smells, which generally fall into five main categories. Knowing these categories can help you identify patterns that weaken your software’s structure.
Bloaters are code elements that have grown too large, making them harder to maintain. For example, a processOrder function that handles inventory, shipping, payment, and notifications all at once becomes unwieldy. Adam Allard, Full-Stack Software Engineer at Northrop Grumman, puts it this way:
Most can agree that anything above 25 lines of code should make you start asking questions.
These oversized methods or classes can reduce readability and make testing more challenging.
Object-Oriented Abusers occur when object-oriented programming (OOP) principles are misapplied. A classic case is an Ostrich class inheriting a fly() method from a Bird class, even though ostriches cannot fly. Joe Eames, CEO at Thinkster.io, highlights the problem:
Feature envy creates high levels of coupling between two classes, which makes our systems brittle. It also implies that we may have unnecessary ceremonious code.
Such issues often break the Open-Closed Principle, leading to rigid and fragile inheritance structures.
Change Preventers make updates costly because a single change requires modifications in multiple places. For instance, if altering a date format demands changes across several files, this is a classic example of Shotgun Surgery.
Dispensables refer to unnecessary pieces of code that only clutter your project. Examples include dead code, duplicate code, and redundant comments, all of which make the codebase harder to navigate.
Couplers create excessive dependencies between classes, reducing modularity. A common sign is the use of message chains like a.getB().getC().getD(), which indicate high coupling and make the system less adaptable.
Recognising these categories is a key step in identifying structural issues before relying on detection tools. These smells not only make code harder to work with but also increase the risk of bugs and technical debt, often necessitating legacy app modernization.
How to Identify Code Smells
Spotting code smells involves combining manual reviews, automated tools, and complexity metrics. Each method plays a role in creating a thorough code review process.
Manual Code Reviews
Peer reviews are one of the best ways to catch issues that automated tools might overlook. Experienced developers can identify nuanced problems, like Feature Envy or architectural inconsistencies, that require human judgement. A good review starts by ensuring the code meets the requirements before diving into individual lines.
Martin Nadeau, a Software Development Expert, highlights:
Code smells can help us identify common design problems that are quick to spot (or sniff out!). This allows us to target issues that occur everywhere and at every level of abstraction.
- Martin Nadeau, Software Development Expert
During reviews, look out for repeated logic that violates the DRY principle. Spot hard-coded values, like Magic Numbers or Magic Strings, and replace them with named constants for better readability. A helpful rule of thumb is the “25-line rule” – if a method exceeds around 25 lines, it might need splitting into smaller, more focused units.
For issues that are harder to detect manually, automated tools step in to provide systematic checks.
Static Code Analysis Tools
Tools like SonarQube, ESLint, and PyLint are invaluable for identifying code smells such as duplicated logic, overly long methods, and high cognitive complexity. Integrating these tools into your CI/CD pipeline or IDE ensures real-time feedback and enforces consistent coding standards.
By embedding static analysis into your CI/CD workflow, every pull request gets automatically scanned. This automation allows manual reviews to concentrate on deeper logic while the tools handle structural issues. It’s an efficient way to reduce technical debt and keep the codebase clean. Increasingly, these tools are being augmented with AI – both to explain why something smells and to draft a fix – which we return to below.
Complexity Metrics
Metrics provide measurable insights into code quality, revealing problem areas that might not be obvious. Cyclomatic complexity, for instance, measures the number of independent paths through a method. If a module’s cyclomatic complexity exceeds 10, it’s likely too complex and may need refactoring to simplify it. Cognitive complexity focuses on how challenging code is for humans to understand, considering factors like deep nesting and tangled logic. High cognitive complexity can slow down development by making code harder to interpret.
Tracking these metrics helps identify trouble spots in the codebase, guiding refactoring efforts. Setting clear team-wide limits, like maximum line counts, can also prevent unnecessary code bloat. Together, these tools and strategies ensure your code remains clean, scalable, and easier to maintain.
Refactoring Techniques to Fix Code Smells
Once you’ve spotted code smells, the next step is to fix them by restructuring the code without altering its external behaviour. Three main approaches – abstraction, composing methods, and simplifying method calls – are particularly effective in addressing common structural issues. These methods directly tackle the problems flagged by code smells and build on the identification techniques discussed earlier.
Refactoring by Abstraction
Abstraction helps manage bloated classes and tangled logic by isolating complexity into smaller, focused units. For instance, the Extract Class technique can break down a “Large Class” or “God Object” by dividing it into separate classes based on distinct responsibilities. Take a sprawling User class, for example – it could be split into Authenticator, BillingManager, and ProfileService to streamline functionality.
Another powerful technique, Replace Conditional with Polymorphism, eliminates complex switch or if-else statements by delegating responsibilities to subclasses. This aligns with the Open/Closed Principle, where classes are open for extension but closed for modification. When dealing with Feature Envy – where a method excessively interacts with another class’s data – Move Method is a great option. Relocating the method to the appropriate class strengthens encapsulation and reduces dependency.
Refactoring by Composing Methods
Long, unwieldy methods are a common issue, and Extract Method can help by breaking them into smaller, more focused functions. Each function should perform a single, clear task, such as validateInput, saveToDatabase, or sendEmail. As Sonali Sood from CodeAnt AI explains:
If you can’t explain what the method does in one sentence, it’s doing too much.
Instead of focusing on the number of lines, assess whether each method handles a single responsibility. Techniques like Replace Temp with Query can further simplify methods by replacing local variables with method calls. Similarly, Split Temporary Variable ensures that each variable serves only one purpose, making the code easier to understand and maintain.
After breaking down large methods, it’s essential to streamline how they interact. This leads to the next approach.
Refactoring by Simplifying Method Calls
Simplifying method calls reduces complexity and improves readability. The Introduce Parameter Object technique tackles “Data Clumps” by grouping related variables – such as startDate and endDate – into a single object. This not only tidies up method signatures but also makes the relationships between data clearer. Another useful technique is Rename Method, where vague names like calculate are replaced with something more descriptive, such as calculateTotalPrice, to improve clarity.
Finally, Preserve Whole Object simplifies method signatures further by passing entire objects rather than individual data elements. This reduces the need for excessive parameters and keeps the code clean.
For example, software engineer Clare Sudbery successfully refactored a “God Class” by gradually moving methods into a new FileLoader class. This incremental approach reduced the size of the original class while maintaining its functionality. Her example highlights how careful, step-by-step refactoring can significantly improve code structure without introducing new issues.
Using AI Coding Agents to Find and Fix Code Smells
The detection and refactoring methods above were, until recently, almost entirely manual or rule-based. That has changed. Agentic coding tools such as Claude Code and OpenAI Codex can now read an entire codebase, run your build and tests, and propose changes – which makes them genuinely useful for both spotting smells and clearing them. Used carelessly, though, they introduce smells faster than any human ever could, so the workflow matters far more than the tool.
Where AI Helps With Detection
Where a static analyser flags a rule violation, an agent can reason about intent. Point one at a module with a prompt like “analyse this class, map the data flow, and identify any code smells with their location and severity,” and it will surface bloaters, feature envy, message chains and god objects, usually with a suggested refactoring for each. It is effectively a tireless first-pass reviewer that never gets bored halfway through a 2,000-line file.
This pairs well with the static analysis already in your pipeline rather than replacing it. Sonar, for example, now layers AI on top of its rule engine: AI CodeFix proposes a fix for a detected issue without changing the code’s behaviour, while its AI Code Assurance feature applies stricter verification to code it recognises as AI-generated. The deterministic engine finds the issue; the model drafts the remedy.
Where AI Helps With Fixing – and How to Keep It Safe
The single most important rule is the one that already applies to manual refactoring: tests first. Anthropic’s own Claude Code guidance is blunt that giving the agent a way to verify its work – a test suite, a build, a linter – is the highest-impact thing you can do, because without it “looks done” is the only signal the agent has. For legacy code, that means asking the agent to write characterisation tests that lock in the current behaviour before it touches anything, then refactoring in small, reviewable steps.
A reliable loop looks like this: have the agent explore and explain the code, write a plan, generate the tests, then refactor incrementally against them. Keep each change small and scoped to a single concern – agents handle a clear single-file Extract Method far better than a sprawling multi-file change that ripples across services. And because a model grading its own work is a weak check, add a second review pass in a fresh context (a separate session, or a dedicated reviewer agent) to inspect the diff before you trust it.
The Catch Worth Taking Seriously
AI makes it trivial to produce more code; it does not automatically produce better-structured code. GitClear’s analysis of 211 million changed lines found that as AI assistance spread, the share of changed lines associated with refactoring fell from 25% in 2021 to under 10% in 2024, while copy-pasted lines rose from 8.3% to 12.3% – 2024 being the first year on record that cloning overtook code reuse. In other words, the default behaviour of AI-assisted teams is to add duplication: the very smell this article warns against.
The takeaway is not to avoid these tools, but to point them at the problem deliberately. Use agents to detect smells and to draft refactors, keep your tests and static analysis as the gate, and review every diff with human judgement. The agent is a fast, capable pair of hands. It is not the engineer who decides what “clean” means.
Sample Prompts You Can Adapt
The prompts below follow the patterns Anthropic publishes in its Claude Code best practices, but they are model-agnostic and work just as well with Codex, Cursor or Copilot. The common thread is that every one of them is scoped, points at specific code, and gives the agent a way to verify its own work – the three things that separate a reliable result from a plausible-looking one.
1. Audit a file for smells (detection). Scope it to one file and ask for evidence, not opinions:
Read
src/orders/OrderService.javaand list the code smells you find. For each one, give the line range, the smell category (bloater, object-oriented abuser, change preventer, dispensable or coupler), the cyclomatic complexity if relevant, and the specific refactoring you’d apply. Don’t change any code yet.
2. Lock in behaviour before touching anything (characterisation tests). This is the non-negotiable safety net for legacy code:
Before we refactor
OrderService, write characterisation tests that capture its current behaviour, including the edge cases around partial payments and cancelled orders. Use no mocks for the core logic. Run the suite and confirm it passes against the existing code, then stop so I can commit it as a checkpoint.
3. Plan before writing code (explore, then plan). Separating planning from execution stops the agent solving the wrong problem:
The
Userclass has grown into a god object. In plan mode, map its responsibilities and propose how to split it using Extract Class – name the new classes, what moves to each, and what stays. List the files that change and anything that is explicitly out of scope. Do not edit anything yet.
4. Apply one refactoring at a time (incremental fix). Small, single-concern diffs are what agents handle reliably:
Apply only the Extract Method refactoring to
processOrder: pull validation, persistence and notification into separate private methods, each doing one thing. Behaviour must not change. Run the characterisation tests after the change and show me the output. Address the root cause – do not suppress or rewrite the tests to make them pass.
5. Reference existing patterns (consistency). Point the agent at your conventions so it doesn’t invent new ones:
Replace the
switchonpaymentTypeinPaymentProcessorwith polymorphism. Follow the strategy pattern already used insrc/shipping/ShippingStrategy.java. Keep it minimal – use the simplest approach that works and don’t add abstractions we don’t need yet.
6. Gate the work on verification. Make the agent prove it, rather than asserting success:
Run the full test suite, the linter, and report the cyclomatic complexity of every method you changed. Show me the commands and their output. If any method is still above a complexity of 10, explain why before continuing.
7. Review in a fresh context (adversarial check). A model grading its own work is a weak check, so use a separate reviewer:
Using a sub-agent with no prior context, review the diff on this branch against the plan. Check that behaviour is preserved, the tests cover the refactored paths, and nothing outside the stated scope changed. Report only gaps that affect correctness or the requirements – not style preferences.
One guardrail worth setting once: agents tend to over-refactor, adding helper functions and premature abstractions unless told not to. A single line in your CLAUDE.md (or the equivalent rules file) – “use the simplest approach that works; do not add unsolicited abstractions” – keeps that tendency in check across every session.
How to Prioritise Refactoring Efforts
Once you’ve identified code smells and potential refactoring methods, the next step is deciding where to focus your efforts for the biggest impact.
Not every code smell demands immediate attention. Factors like severity, frequency of changes, and business impact should guide your priorities. For instance, focus on code that is frequently modified or prone to bugs. These areas often deliver the highest return when cleaned up. Take a module that’s updated nearly every sprint – refactoring this will have a much larger effect than tweaking stable code that’s rarely touched.
Another key consideration is cognitive complexity. If a section of code is hard to understand, it not only increases the likelihood of errors but also slows down future development efforts. Modules with high cognitive or cyclomatic complexity (values exceeding 10) should be at the top of your refactoring list.
It’s also important to align your refactoring with business goals. Prioritise fixing code smells that directly impact core features or current requirements. Minor quirks in areas of the codebase that are rarely used can often wait.
Timing matters too. The best time to refactor is right before adding new features or updates. Starting new work on a clean foundation ensures better quality. Following the “Boy Scout Rule” – making small, incremental improvements as you go – can also help maintain code health over time. Alexandre Walsh, VP and Product Manager at Axify, emphasises this approach:
Progress toward continuous delivery means advocating for smaller changes. Small changes are easier to test, fix, and deploy and ultimately improve speed and quality.
Before diving into refactoring, ensure you have a solid testing framework in place. Complete test coverage acts as a safety net, reducing the risk of introducing new problems during the process. To keep things manageable, separate refactoring tasks from feature development by using distinct pull requests. This approach prevents unnecessary complexity and simplifies code reviews.
Finally, tracking metrics like “Flow Distribution” can help balance refactoring and feature development. A clear view of how much effort is spent managing technical debt can ensure it doesn’t slow down delivery velocity.
Building a Culture of Refactoring
Once you’ve got the strategies for identifying and tackling refactoring down, the next step is embedding a mindset that prioritises code quality across your team. Refactoring shouldn’t feel like a one-off chore – it needs to become second nature. This shift requires more than technical skills; it’s about rethinking how your organisation approaches code. The goal? Move from focusing solely on “pure functionality” to creating “survivable code” – code that remains understandable and adaptable long after it’s written. Sonali Sood, Founding GTM at CodeAnt AI, explains:
Clean code isn’t just about writing ‘beautiful’ code. It’s about writing survivable code. Code that plays well with teammates, makes sense months later, and doesn’t break without causing unexpected failures.
This mindset change is the cornerstone of building organisational support for refactoring and ensuring it gets the attention it deserves.
Gaining Management Support
To make refactoring a priority, you’ll need to show its value to decision-makers. Highlight how it can speed up time-to-market, reduce outages, and open up new opportunities for revenue. Hard data works wonders here. For example, if developers are spending 30% of their sprint fixing bugs in a troublesome module, that’s a clear sign refactoring could save time and resources. A good way to pinpoint problem areas is to ask your team a simple question: “If you had a magic wand, which part of the code would you rewrite?”.
Encouraging Collaboration
Refactoring thrives in a collaborative environment. Practices like pair programming and code reviews help spread knowledge and create a shared understanding of code quality. During reviews, focus on open-ended questions like, “How can we simplify this?” to encourage discussion without putting anyone on the defensive. This approach fosters psychological safety and helps your team feel comfortable talking about improvements.
To take it further, invest in training. Teach your team how to spot and fix code smells, and introduce design patterns that make refactoring easier. The more equipped they are, the more proactive they’ll be in maintaining clean, manageable code.
Automating Standards
Automation can take some of the load off your team. Tools like static analysers and linters, integrated directly into your CI/CD pipeline, can enforce coding standards automatically, catching issues before they snowball. Pair these tools with clear, agreed-upon style guides to ensure consistency.
Finally, refactoring shouldn’t be treated as a separate task or sprint. Instead, it needs to be woven into your everyday workflow. As Srujana from Tembo puts it:
Refactoring shouldn’t be a separate sprint where all developers sit and pick different parts of code to refactor. It should be an ongoing process, integrated into your regular development workflow.
Conclusion
Code smells are more than just minor nuisances – they’re red flags hinting at deeper design problems that can disrupt your entire development process. As Robert C. Martin aptly said: “Even bad code can function. But if code isn’t clean, it can bring a development organisation to its knees”. The line between well-functioning software and a maintenance disaster often depends on how you handle these early warnings.
The good news? You don’t have to fix everything all at once. Start by pairing manual code reviews with automated static analysis tools – and, increasingly, AI coding agents that can both flag smells and draft the refactor, provided you keep tests and human review as the safety net – to uncover a variety of code smells. Then, focus on the parts of your codebase that are updated most often or have a history of bugs. And remember, before diving into refactoring, ensure you have solid test coverage. As the saying goes: “Refactoring without tests isn’t refactoring; it’s just… changing stuff and hoping for the best”.
Refactoring isn’t something to save for a dedicated sprint or occasional clean-up effort. Instead, make it part of your routine – ideally just before adding new features – so you’re always working on a stable foundation. Take small, deliberate steps: extract a method, rename a confusing variable, run your tests, and commit. Over time, these little improvements add up, enhancing readability, maintainability, and overall development speed.
FAQs
Which code smells should we fix first?
When it comes to improving code quality, tackling code smells that affect maintainability and scalability should be a top priority. These “smells” are indicators of deeper issues in your codebase that, if left unchecked, can lead to bigger problems down the line.
Some common examples include:
- Duplicated code: Repeated code fragments make updates harder and increase the risk of inconsistencies.
- Long methods: Methods that try to do too much can be difficult to read, test, and debug.
- Large classes: Overloaded classes often lack a clear purpose and can become unmanageable.
- Excessive coupling: When components are overly dependent on each other, changes in one part can ripple through the entire system.
Addressing these issues early not only makes your code easier to read and understand but also helps avoid headaches in the future. Focus on the most critical design flaws first – this approach ensures you’re improving code quality in a way that’s both efficient and impactful.
How do you refactor safely without breaking behaviour?
To refactor effectively and minimise risks, start by recognising code smells such as overly long methods, oversized classes, or repeated chunks of code. Focus on making small, gradual adjustments to the code’s structure, ensuring that its outward behaviour stays the same.
Leverage static analysis tools to identify potential issues, conduct thorough code reviews, and run extensive tests after every change. If you use an AI coding agent, the same discipline applies: have it write characterisation tests first, keep each change small, and review every diff yourself. This disciplined approach helps maintain consistency in functionality and reduces the chances of regressions or unexpected problems.
What complexity metrics should we track and what limits are sensible?
When keeping an eye on complexity metrics to spot code smells, pay attention to method length, class size, and parameter count. A good rule of thumb is to aim for methods that stay under 20-30 lines, classes that don’t exceed a few hundred lines, and functions with a maximum of 3-5 parameters. Sticking to these limits helps avoid unnecessary clutter, making the code simpler to read and maintain.
