By Sagar Shankaran, Founder of CallSphere
Strategies for using Claude Code to refactor legacy code — from targeted function rewrites to large-scale migrations, with patterns for safe incremental modernization.
Key takeaways
Every software team accumulates technical debt. A module written in haste three years ago now handles 10x the traffic it was designed for. A Python 2 codebase needs to run on Python 3. Express.js callback patterns need to become async/await. jQuery-powered frontends need to become React applications.
Refactoring legacy code is tedious, risky, and time-consuming — exactly the kind of work where Claude Code adds the most value. It can read the entire existing codebase, understand patterns, and systematically apply changes while maintaining behavior.
The strangler fig pattern replaces legacy code incrementally. Instead of rewriting everything at once, you wrap old code in new interfaces and replace it piece by piece. Claude Code excels at this pattern because it can understand both the old and new code simultaneously.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
flowchart LR
USER(["User message"])
LOOP{"messages.create<br/>agent loop"}
THINK["Extended thinking<br/>optional"]
TOOL{"stop_reason<br/>tool_use?"}
EXEC["Execute tool<br/>append tool_result"]
DONE(["stop_reason<br/>end_turn"])
USER --> LOOP --> THINK --> TOOL
TOOL -->|Yes| EXEC --> LOOP
TOOL -->|No| DONE
style LOOP fill:#4f46e5,stroke:#4338ca,color:#fff
style THINK fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style DONE fill:#059669,stroke:#047857,color:#fff
You: We have a legacy UserService class with 1,200 lines. I want to refactor it
using the strangler fig pattern. Start by extracting the authentication-related
methods into a new AuthenticationService, keeping the old methods as thin wrappers
that delegate to the new service.
Claude Code will:
# Before: Monolithic UserService (1,200 lines)
class UserService:
def login(self, email, password): ...
def logout(self, session_id): ...
def reset_password(self, email): ...
def verify_token(self, token): ...
def create_user(self, data): ...
def update_profile(self, user_id, data): ...
# ... 40 more methods
# After: UserService delegates to AuthenticationService
class AuthenticationService:
"""Extracted from UserService — handles all auth concerns."""
def login(self, email: str, password: str) -> AuthResult: ...
def logout(self, session_id: str) -> None: ...
def reset_password(self, email: str) -> None: ...
def verify_token(self, token: str) -> TokenPayload: ...
class UserService:
def __init__(self, auth_service: AuthenticationService):
self._auth = auth_service
def login(self, email, password):
return self._auth.login(email, password) # Thin wrapper
def create_user(self, data): ... # Remains in UserService
def update_profile(self, user_id, data): ... # Remains
Claude Code can systematically find and replace patterns across an entire codebase. This is ideal for:
You: Convert all callback-style database queries in src/services/ to async/await.
The current pattern is:
db.query(sql, params, (err, result) => { ... })
Replace with:
const result = await db.query(sql, params)
Handle errors with try/catch. Process each file one at a time and run tests after each file.
Claude Code processes each file:
// Before
function getUser(id, callback) {
db.query("SELECT * FROM users WHERE id = ?", [id], (err, rows) => {
if (err) return callback(err);
if (rows.length === 0) return callback(new Error("Not found"));
callback(null, rows[0]);
});
}
// After (Claude Code's refactored version)
async function getUser(id) {
const rows = await db.query("SELECT * FROM users WHERE id = ?", [id]);
if (rows.length === 0) {
throw new Error("Not found");
}
return rows[0];
}
Adding types to an untyped codebase is one of Claude Code's strongest refactoring use cases. It can analyze function usage, infer types, and add annotations incrementally.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
You: Convert src/utils/validators.js to TypeScript. Add full type annotations
to all functions. Infer types from usage and return values. Keep all existing
logic exactly the same.
// Before: validators.js
function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
function validateAge(age) {
return typeof age === "number" && age >= 0 && age <= 150;
}
function validateUser(data) {
const errors = [];
if (!data.name || data.name.length < 2) errors.push("Name too short");
if (!validateEmail(data.email)) errors.push("Invalid email");
if (data.age !== undefined && !validateAge(data.age)) errors.push("Invalid age");
return { valid: errors.length === 0, errors };
}
// After: validators.ts (Claude Code's conversion)
interface ValidationResult {
valid: boolean;
errors: string[];
}
interface UserData {
name: string;
email: string;
age?: number;
}
function validateEmail(email: string): boolean {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
function validateAge(age: number): boolean {
return typeof age === "number" && age >= 0 && age <= 150;
}
function validateUser(data: UserData): ValidationResult {
const errors: string[] = [];
if (!data.name || data.name.length < 2) errors.push("Name too short");
if (!validateEmail(data.email)) errors.push("Invalid email");
if (data.age !== undefined && !validateAge(data.age)) errors.push("Invalid age");
return { valid: errors.length === 0, errors };
}
You: Add complete type annotations to all functions in app/services/.
Use modern Python typing (3.10+ syntax with | instead of Union).
Run mypy after each file to verify correctness.
Migrating between frameworks (Express to Fastify, Flask to FastAPI, React class to hooks) requires understanding both the old and new framework patterns.
You: Migrate app/routes/users.py from Flask to FastAPI. Maintain the same
URL paths and response formats. Replace Flask-specific patterns with
FastAPI equivalents:
- @app.route -> @router.get/post/put/delete
- request.json -> Pydantic models
- jsonify() -> direct dict return
- abort() -> HTTPException
# Before: Flask
@app.route("/api/users", methods=["GET"])
def list_users():
page = request.args.get("page", 1, type=int)
limit = request.args.get("limit", 20, type=int)
users = User.query.paginate(page=page, per_page=limit)
return jsonify({"users": [u.to_dict() for u in users.items], "total": users.total})
@app.route("/api/users", methods=["POST"])
def create_user():
data = request.json
if not data.get("email"):
abort(400, "Email is required")
user = User(**data)
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict()), 201
# After: FastAPI (Claude Code's migration)
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, EmailStr
router = APIRouter(prefix="/api/users")
class CreateUserRequest(BaseModel):
email: EmailStr
name: str
age: int | None = None
class UserResponse(BaseModel):
id: str
email: str
name: str
age: int | None
class UserListResponse(BaseModel):
users: list[UserResponse]
total: int
@router.get("", response_model=UserListResponse)
async def list_users(
page: int = Query(1, ge=1),
limit: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
):
offset = (page - 1) * limit
result = await db.execute(select(User).offset(offset).limit(limit))
total = await db.scalar(select(func.count()).select_from(User))
users = result.scalars().all()
return {"users": users, "total": total}
@router.post("", response_model=UserResponse, status_code=201)
async def create_user(
request: CreateUserRequest,
db: AsyncSession = Depends(get_db),
):
user = User(**request.model_dump())
db.add(user)
await db.commit()
await db.refresh(user)
return user
Before refactoring, write tests for the current behavior of UserService.login().
Test all branches: successful login, wrong password, nonexistent user, locked account.
Refactor one file at a time. After each file, run the test suite.
Do not proceed to the next file until all tests pass.
Long refactoring sessions generate a lot of context. Compact after every 3-5 files to stay within the context window.
After each successful refactoring step, commit with a descriptive message.
This gives us rollback points if something goes wrong.
After refactoring, run the full test suite: npm test
If any test fails, fix the failure before moving on.
Ask Claude Code to track refactoring metrics:
Analyze the codebase and report:
1. Number of files still using callback patterns
2. Number of untyped function parameters in src/services/
3. Lines of code in the largest files (identify files > 500 lines)
4. Cyclomatic complexity of the top 10 most complex functions
Claude Code is exceptionally well-suited for refactoring because it can hold both the old pattern and the new pattern in context simultaneously, applying changes systematically across many files. The key is working incrementally — one file or one pattern at a time — with tests as your safety net. Combined with git commits at each step, Claude Code transforms refactoring from a dreaded multi-sprint project into a methodical, low-risk process.

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
Anthropic unveiled 10 pre-built finance agent templates on May 5, 2026 across pitchbook building, KYC screening, and month-end close. What each template does and the hours it replaces.
Anthropic shipped finance plugins for Claude Cowork and Claude Code on May 5, 2026. How analysts use them in practice and what the plugin model means for adoption.
Enterprise CIO Guide perspective on Claude Code 2.1 ships background agents, sub-agent spawning, and a hooks API that turn it into a true multi-agent coding platform.
Pair Claude Code with Code-Review-Graph and you have a local-first agentic IDE with deterministic context, blast radius PR review, and zero per-seat indexing fees.
Claude has owned developer mindshare since 3.5 Sonnet. Is the coding edge real? A benchmark and tooling examination of where Claude actually leads in 2026.
Every coding agent has to pick a model. With GPT-5.5 winning Terminal-Bench and Opus 4.7 winning SWE-bench Pro, the agent stack you pick affects which model you get.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.
Try Live DemoBook a DemoCalculate Your ROI