The Problem
You inherited a codebase. It works, mostly. But when you ask AI to make changes, it produces garbage. The AI doesn't understand the implicit architecture, the historical decisions, or the "don't touch that" zones.
Legacy code isn't just hard for humans - it's harder for AI. AI lacks the tribal knowledge that makes legacy code survivable.
The Core Insight
Refactoring for AI is about making implicit knowledge explicit.
What humans learn through months of working with code, AI needs to see in the code itself. The goal isn't to make code "perfect" - it's to make code self-explaining.
The Refactoring Pipeline
Phase 1: Understand Before Touching
Before any refactoring, create an AI-readable map:
# Ask AI to create a system map
"Analyze this codebase structure and create a markdown file
documenting:
1. Main entry points
2. Core domain concepts
3. Key dependencies between modules
4. Areas that look fragile or complex
Files:
[paste directory structure]
[paste key files]"
Save this as ARCHITECTURE.md. Now AI (and future you) has a map.
Phase 2: Add Strategic Comments
Don't comment what code does. Comment why it exists:
// Bad: Increments counter
counter++;
// Good: Rate limit check - prevents API abuse (incident #423)
counter++;
if (counter > RATE_LIMIT) {
// This timeout is intentionally long - shorter values
// caused cascade failures in production (2024-01)
await sleep(5000);
}
Phase 3: Extract and Name
Complex logic should have names that explain intent:
// Before: Magic boolean expression
if (user.createdAt > thirtyDaysAgo && !user.verified && user.loginCount < 3) {
sendReminder();
}
// After: Named intent
const isInactiveNewUser = (user: User): boolean => {
const isNew = user.createdAt > thirtyDaysAgo;
const isUnverified = !user.verified;
const hasLowEngagement = user.loginCount < 3;
return isNew && isUnverified && hasLowEngagement;
};
if (isInactiveNewUser(user)) {
sendReminder();
}
Now AI can understand and modify the logic correctly.
Phase 4: Isolate the Dangerous Parts
Every codebase has "here be dragons" zones. Make them explicit:
/**
* CRITICAL: Payment processing logic
*
* DO NOT MODIFY without:
* 1. Review from payments team
* 2. Full integration test suite passing
* 3. Manual QA on staging
*
* Last audit: 2025-03-15
* Related incidents: PAY-201, PAY-245
*/
class PaymentProcessor {
// ...
}
AI-Readable Warning Pattern
Use consistent markers AI will recognize:
// CRITICAL: - Don't modify without human review
// LEGACY: - Works but don't understand why
// HACK: - Intentional shortcut, explain why
// TODO(importance): - Future work with priority
Safe Extraction Patterns
The Strangler Fig
Gradually replace old code by wrapping it:
// Step 1: Wrap the old function
function processOrderLegacy(order) {
// ... 500 lines of mystery ...
}
function processOrder(order) {
// New validation
validateOrder(order);
// Delegate to legacy for now
return processOrderLegacy(order);
}
// Step 2: Incrementally move logic to new function
function processOrder(order) {
validateOrder(order);
calculateTotals(order); // Extracted!
// Smaller legacy call
return processOrderLegacy(order);
}
// Step 3: Eventually, legacy function is empty
The Seam Test
Before extracting, add a test at the boundary:
// Add this test BEFORE refactoring
describe('processOrder', () => {
it('produces same output as legacy for known inputs', () => {
const testCases = loadHistoricalOrders();
for (const order of testCases) {
const legacyResult = processOrderLegacy(order);
const newResult = processOrder(order);
expect(newResult).toEqual(legacyResult);
}
});
});
Now you can refactor with confidence.
Dependency Analysis
Before splitting a file, understand what depends on what:
# Generate dependency graph (JavaScript)
npx madge --image deps.svg src/
# Or ask AI to analyze
"Analyze these imports and create a dependency diagram:
- Which files import from this file?
- Which files does this file import?
- Are there any circular dependencies?"
Breaking Circular Dependencies
// Problem: A imports B, B imports A
// a.ts
import { B } from './b';
export class A { b: B; }
// b.ts
import { A } from './a';
export class B { a: A; }
// Solution: Extract interface
// types.ts
export interface IA { /* ... */ }
export interface IB { /* ... */ }
// a.ts
import { IB } from './types';
export class A implements IA { b: IB; }
// b.ts
import { IA } from './types';
export class B implements IB { a: IA; }
Failure Patterns
1. Big Bang Refactor
Symptom: You tried to refactor everything at once. Now nothing works and you can't tell what broke.
Fix: One extraction at a time. Commit after each. Test before moving on.
2. Refactoring Without Tests
Symptom: "I thought I was just moving code" but behavior changed subtly.
Fix: Add characterization tests before refactoring. They capture current behavior, right or wrong.
3. Over-Abstraction
Symptom: Three new interfaces, two abstract classes, and a factory for what used to be one function.
Fix: Refactor for clarity, not for design patterns. If AI can't follow the abstraction, humans won't either.
The Golden Rule of Refactoring
If you can't explain the refactoring in one sentence, it's too big. Split it up.
Quick Reference
Before Any Refactoring:
- Create
ARCHITECTURE.md - Identify "danger zones"
- Add characterization tests
- Commit current state
Safe Extraction Checklist:
- [ ] Test exists for the extraction boundary
- [ ] No circular dependencies introduced
- [ ] New file fits comfortably in AI context alongside its tests and callers
- [ ] Clear, intention-revealing names
- [ ] Comments explain "why"
AI-Friendly Markers:
// CRITICAL: requires human review
// LEGACY: don't understand, don't touch
// HACK: intentional, see explanation
// TODO(high): future work, important