
Introduction
Legacy code is everywhere. It powers critical systems, old projects, and applications that still run businesses today. But it’s often messy, hard to maintain, and resistant to change. Every developer has encountered that file with 2,000 lines of spaghetti code that nobody dares to touch.
That’s where AI for code refactoring comes in. With the rise of large language models (LLMs) like Claude, ChatGPT, and GitHub Copilot, developers now have powerful tools that can help reorganize, modernize, and document legacy code much faster than manual approaches.
In this comprehensive guide, you’ll learn practical techniques for using AI to refactor code, complete with real examples, effective prompts, and strategies for avoiding common pitfalls.
Why Refactor Legacy Code?
Refactoring doesn’t mean rewriting everything from scratch. Instead, it’s about improving what already exists while preserving behavior. Common reasons include:
- Maintainability – Making code easier to understand and modify
- Bug reduction – Eliminating hidden complexity and code smells
- Development velocity – Speeding up future feature development
- Consistency – Ensuring patterns and styles match current standards
- Technical debt – Paying down accumulated shortcuts and workarounds
- Testability – Restructuring code to enable proper unit testing
When done well, refactoring transforms a fragile codebase into something stable, modular, and testable.
How AI Helps with Refactoring
AI tools don’t replace developers, but they dramatically accelerate the refactoring process. Here’s how LLMs can assist at each stage:
1. Spotting Problem Areas
AI can analyze code and highlight issues that warrant attention:
// Prompt to Claude or ChatGPT:
// "Analyze this function and identify code smells, potential bugs,
// and areas that could benefit from refactoring. Explain each issue."
// Before analysis:
function processUserData(data) {
var result = [];
for (var i = 0; i < data.length; i++) {
if (data[i].active == true) {
if (data[i].age > 18) {
if (data[i].email != null && data[i].email != '') {
var user = {};
user.name = data[i].firstName + ' ' + data[i].lastName;
user.email = data[i].email.toLowerCase();
user.isAdult = true;
user.createdAt = new Date();
result.push(user);
}
}
}
}
return result;
}
// AI-identified issues:
// 1. Pyramid of doom - deeply nested conditionals
// 2. Using 'var' instead of 'const/let'
// 3. Loose equality (==) instead of strict (===)
// 4. No error handling
// 5. Function does too many things (filtering + mapping + creating)
// 6. No input validation
// 7. Magic number (18) without explanation
2. Suggesting Modern Patterns
LLMs can recommend moving from outdated practices to modern alternatives:
// Prompt: "Refactor this code to use modern JavaScript patterns,
// eliminate the nested conditionals, and separate concerns."
// After AI-assisted refactoring:
const ADULT_AGE = 18;
const isActiveAdultWithEmail = (user) =>
user.active === true &&
user.age > ADULT_AGE &&
Boolean(user.email?.trim());
const formatUser = (user) => ({
name: `${user.firstName} ${user.lastName}`,
email: user.email.toLowerCase().trim(),
isAdult: true,
createdAt: new Date(),
});
const processUserData = (data) => {
if (!Array.isArray(data)) {
throw new TypeError('Expected an array of user data');
}
return data
.filter(isActiveAdultWithEmail)
.map(formatUser);
};
3. Converting Between Languages or Frameworks
// Prompt: "Convert this class-based React component to a functional
// component with hooks, maintaining the same behavior."
// Before:
class UserProfile extends React.Component {
constructor(props) {
super(props);
this.state = {
user: null,
loading: true,
error: null
};
}
componentDidMount() {
this.fetchUser();
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUser();
}
}
async fetchUser() {
this.setState({ loading: true, error: null });
try {
const response = await fetch(`/api/users/${this.props.userId}`);
const user = await response.json();
this.setState({ user, loading: false });
} catch (error) {
this.setState({ error: error.message, loading: false });
}
}
render() {
const { user, loading, error } = this.state;
// ... render logic
}
}
// After AI-assisted conversion:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
const fetchUser = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
const data = await response.json();
if (isMounted) setUser(data);
} catch (err) {
if (isMounted) setError(err.message);
} finally {
if (isMounted) setLoading(false);
}
};
fetchUser();
return () => { isMounted = false; };
}, [userId]);
// ... render logic
}
4. Extracting Methods and Classes
// Prompt: "This function is too long. Extract logical sections into
// separate, well-named helper functions."
// Before: 150-line function handling order processing
async function processOrder(order) {
// 30 lines: validate order
// 40 lines: calculate pricing
// 25 lines: check inventory
// 35 lines: process payment
// 20 lines: send notifications
}
// After AI-assisted extraction:
class OrderProcessor {
constructor(paymentService, inventoryService, notificationService) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
this.notificationService = notificationService;
}
async process(order) {
this.validateOrder(order);
const pricing = this.calculatePricing(order);
await this.reserveInventory(order);
try {
await this.processPayment(order, pricing);
await this.fulfillOrder(order);
await this.sendConfirmation(order);
} catch (error) {
await this.releaseInventory(order);
throw error;
}
}
validateOrder(order) {
if (!order.items?.length) {
throw new ValidationError('Order must have at least one item');
}
if (!order.customer?.email) {
throw new ValidationError('Customer email is required');
}
// Additional validations...
}
calculatePricing(order) {
const subtotal = order.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const discount = this.calculateDiscount(order, subtotal);
const tax = this.calculateTax(subtotal - discount, order.shippingAddress);
const shipping = this.calculateShipping(order);
return { subtotal, discount, tax, shipping, total: subtotal - discount + tax + shipping };
}
// ... other extracted methods
}
5. Generating Documentation
// Prompt: "Add JSDoc documentation to this function, including
// parameter types, return type, and usage examples."
// Before:
function debounce(func, wait, immediate) {
let timeout;
return function executedFunction(...args) {
const context = this;
const later = () => {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
// After:
/**
* Creates a debounced version of the provided function that delays
* invoking until after the specified wait time has elapsed since
* the last invocation.
*
* @param {Function} func - The function to debounce
* @param {number} wait - Milliseconds to wait before invoking
* @param {boolean} [immediate=false] - If true, trigger on leading edge instead of trailing
* @returns {Function} The debounced function
*
* @example
* // Debounce a search input handler
* const debouncedSearch = debounce((query) => {
* fetchSearchResults(query);
* }, 300);
*
* inputElement.addEventListener('input', (e) => {
* debouncedSearch(e.target.value);
* });
*
* @example
* // Trigger immediately on first call
* const debouncedSave = debounce(saveDocument, 1000, true);
*/
function debounce(func, wait, immediate = false) {
// ... implementation
}
6. Explaining Complex Legacy Code
// Prompt: "Explain what this legacy code does in plain English,
// identifying any potential issues or assumptions it makes."
// Legacy code:
function a(d,k){var r=[];for(var i=0;i<d.length;i++){var x=d[i];if(x.t==1||x.t==3){if(x.v>k){r.push({i:x.id,v:x.v-k,s:x.t==1?'p':'f'});}}}return r.sort(function(a,b){return b.v-a.v;});}
// AI explanation:
// This function filters and transforms a collection of items:
// 1. Takes an array 'd' and threshold 'k'
// 2. Filters items where type (t) is 1 or 3
// 3. Filters items where value (v) exceeds threshold k
// 4. Transforms matching items to objects with:
// - 'i': the item id
// - 'v': value minus threshold
// - 's': 'p' for type 1 (possibly 'partial'), 'f' for type 3 (possibly 'full')
// 5. Returns results sorted by value descending
//
// Issues identified:
// - Cryptic variable names make code unmaintainable
// - Magic numbers (1, 3) without documentation
// - No input validation
// - Mutates nothing but naming suggests otherwise
Effective Prompts for AI Refactoring
The quality of AI-assisted refactoring depends heavily on how you frame your requests. Here are proven prompt templates:
// Prompt templates for different refactoring tasks:
// 1. General refactoring
"Refactor this code following these principles:
- Single Responsibility Principle
- DRY (Don't Repeat Yourself)
- Early returns over nested conditionals
- Descriptive naming
- Pure functions where possible
[paste code here]"
// 2. Performance optimization
"Analyze this code for performance issues and suggest optimizations.
Consider: time complexity, memory usage, unnecessary operations,
and potential caching opportunities.
[paste code here]"
// 3. Error handling
"Add comprehensive error handling to this code:
- Validate inputs
- Handle edge cases
- Use appropriate error types
- Provide meaningful error messages
[paste code here]"
// 4. TypeScript conversion
"Convert this JavaScript to TypeScript:
- Add appropriate type annotations
- Use interfaces for object shapes
- Handle nullable types properly
- Add generic types where appropriate
[paste code here]"
// 5. Test generation
"Generate unit tests for this function:
- Cover happy path
- Cover edge cases
- Cover error cases
- Use descriptive test names
[paste code here]"
Complete Refactoring Workflow
Here’s a systematic approach to AI-assisted refactoring:
Step 1: Understand the Code
// First, ask AI to explain what the code does
// Prompt: "Explain this code's purpose, inputs, outputs, and side effects."
// Verify the explanation matches your understanding
// Identify any hidden dependencies or assumptions
Step 2: Write Tests First
// Before refactoring, ensure behavior is captured in tests
// Prompt: "Generate comprehensive tests for this function that
// capture its current behavior, including edge cases."
describe('processOrder', () => {
it('should calculate correct total with discount', () => {
const order = createTestOrder({ items: [{ price: 100, qty: 2 }] });
const result = processOrder(order);
expect(result.total).toBe(180); // 10% discount
});
it('should throw for empty order', () => {
expect(() => processOrder({ items: [] })).toThrow('Order must have items');
});
// ... more tests
});
Step 3: Refactor Incrementally
// Break refactoring into small, testable steps
// After each change:
// 1. Run tests to verify behavior is preserved
// 2. Commit the change
// 3. Move to next refactoring step
// Example incremental steps:
// Commit 1: "Extract validation into separate function"
// Commit 2: "Replace var with const/let"
// Commit 3: "Replace nested ifs with early returns"
// Commit 4: "Add TypeScript types"
// Commit 5: "Improve error messages"
Step 4: Review and Refine
// AI suggestions are drafts, not final code
// Review for:
// - Correctness (does it preserve behavior?)
// - Style (does it match your codebase conventions?)
// - Performance (any unintended regressions?)
// - Completeness (any edge cases missed?)
// Prompt for review: "Review this refactored code for potential
// issues, performance problems, or edge cases I might have missed."
Real-World Example: Refactoring a Data Processing Pipeline
// Original legacy code (Python)
def process_data(data_file, output_file, config):
f = open(data_file, 'r')
lines = f.readlines()
f.close()
results = []
for line in lines:
parts = line.strip().split(',')
if len(parts) >= 3:
try:
val = float(parts[2])
if val > config['threshold']:
if parts[0] in config['allowed_types']:
item = {
'type': parts[0],
'id': parts[1],
'value': val,
'normalized': val / config['max_value']
}
results.append(item)
except:
pass
out = open(output_file, 'w')
for r in results:
out.write(str(r) + '\n')
out.close()
return len(results)
# After AI-assisted refactoring:
from dataclasses import dataclass
from pathlib import Path
from typing import Iterator
import csv
import logging
logger = logging.getLogger(__name__)
@dataclass
class ProcessingConfig:
threshold: float
max_value: float
allowed_types: set[str]
@dataclass
class ProcessedItem:
item_type: str
item_id: str
value: float
normalized: float
class DataProcessor:
def __init__(self, config: ProcessingConfig):
self.config = config
def process_file(self, input_path: Path, output_path: Path) -> int:
"""Process a CSV file and write filtered results.
Args:
input_path: Path to input CSV file
output_path: Path for output file
Returns:
Number of items processed
Raises:
FileNotFoundError: If input file doesn't exist
ValueError: If config values are invalid
"""
self._validate_config()
items = list(self._process_rows(input_path))
self._write_results(output_path, items)
logger.info(f"Processed {len(items)} items from {input_path}")
return len(items)
def _validate_config(self) -> None:
if self.config.max_value <= 0:
raise ValueError("max_value must be positive")
if self.config.threshold < 0:
raise ValueError("threshold cannot be negative")
def _process_rows(self, input_path: Path) -> Iterator[ProcessedItem]:
with open(input_path, newline='') as csvfile:
reader = csv.reader(csvfile)
for row_num, row in enumerate(reader, 1):
try:
item = self._parse_row(row)
if item and self._should_include(item):
yield item
except ValueError as e:
logger.warning(f"Skipping row {row_num}: {e}")
def _parse_row(self, row: list[str]) -> ProcessedItem | None:
if len(row) < 3:
return None
return ProcessedItem(
item_type=row[0].strip(),
item_id=row[1].strip(),
value=float(row[2]),
normalized=float(row[2]) / self.config.max_value
)
def _should_include(self, item: ProcessedItem) -> bool:
return (
item.value > self.config.threshold and
item.item_type in self.config.allowed_types
)
def _write_results(self, output_path: Path, items: list[ProcessedItem]) -> None:
with open(output_path, 'w') as f:
for item in items:
f.write(f"{item}\n")
Limitations and Pitfalls to Avoid
AI refactoring is powerful, but not perfect. Be aware of these limitations:
| Limitation | Mitigation Strategy |
|---|---|
| Context window limits | Refactor one file or function at a time; provide necessary context |
| Subtle behavior changes | Always run comprehensive tests after each change |
| Style inconsistencies | Provide your style guide; run linters after refactoring |
| Missing domain knowledge | Review business logic carefully; AI may miss domain-specific requirements |
| Hallucinated APIs | Verify that suggested methods and libraries actually exist |
| Security oversights | Review for OWASP vulnerabilities; never trust AI with auth code blindly |
Best Practices Summary
- Always review AI suggestions – Treat output as drafts, not production code
- Refactor incrementally – One function or class at a time; commit frequently
- Use tests as safety nets – Write tests before refactoring; run after each change
- Combine tools – Use AI alongside IDE refactoring tools and linters
- Maintain human oversight – Architectural and business decisions require judgment
- Verify behavior preservation – The goal is cleaner code with identical behavior
- Document your prompts – Keep effective prompts for future refactoring sessions
Common Mistakes to Avoid
- Refactoring without tests – You can’t verify behavior preservation without tests
- Accepting all suggestions blindly – AI can introduce subtle bugs
- Refactoring too much at once – Small changes are easier to verify and revert
- Ignoring performance implications – Some “cleaner” code may be slower
- Losing domain context – Comments and naming may carry business knowledge
- Not running linters – AI output may not match your style conventions
Conclusion
Using AI for code refactoring is a practical way to modernize legacy projects, reduce technical debt, and make systems easier to maintain. LLMs excel at pattern recognition, code explanation, and suggesting improvements that would take developers hours to devise manually.
By combining AI-driven suggestions with human judgment, comprehensive tests, and proven refactoring patterns, you can safely improve codebases without the heavy cost of full rewrites. Start small, verify often, and let AI handle the tedious parts while you focus on the architectural decisions that matter.
For applying clean code practices in your Flutter projects, check out Clean Architecture with BLoC in Flutter. For a deeper dive into proven refactoring strategies, explore Martin Fowler’s Refactoring Catalog and the SourceMaking Refactoring Guide.
3 Comments