TL;DR — Claude Code cut a 50,000-line legacy Kotlin refactor from an estimated 8–10 weeks down to 3 weeks by handling mechanical tasks (dependency updates, deprecated API replacement, class extraction) while you focus on architectural decisions. The key is the “concentric rings” strategy: dependencies first, then infrastructure, then architecture. Jump to the strategy →

Six months ago, I inherited a Kotlin Android project that hadn’t been touched since 2021. 50,000 lines of code. No tests. Deprecated libraries everywhere. A single God Activity with 3,000 lines. And a deadline to ship a new feature on top of it.

The old me would have spent weeks manually understanding the codebase before touching anything. Instead, I used Claude Code as my co-pilot for the entire modernization. Here’s exactly how, and the mistakes I made along the way. This is a field report, not a reference; for the broader map of Claude Code — setup, hooks, subagents, production patterns — our complete Claude Code guide is the pillar reference this post sits under.


What Is Archeology Mode and Why Does It Matter?

Skipping the understanding phase is the single most common reason legacy refactors fail. Before touching a line of code, spend at least one full session mapping what you have - teams that do this upfront cut total refactoring time by 30-40% because they avoid rework caused by hidden dependencies.

Before refactoring a single line, I needed to understand what I was dealing with. I call this “archeology mode” - excavating the layers of a codebase to understand its history and structure.

"I just inherited a legacy Android project. Before any changes:
1. Read the project structure and give me a high-level architecture map
2. Identify the largest files (by line count)
3. Find deprecated API usage
4. Check the Gradle dependencies for outdated libraries
5. Map the navigation flow from the main Activity
Don't change anything. Just help me understand the terrain."

Claude Code spent about 2 minutes reading files and produced a comprehensive report:

  • Architecture: Loosely MVP, but the Presenter was embedded in the Activity
  • Biggest files: MainActivity.kt (3,100 lines), DataManager.kt (1,800 lines), NetworkHelper.kt (950 lines)
  • Deprecated APIs: AsyncTask (47 usages), HttpUrlConnection (12 usages), Support Library v28
  • Outdated deps: Retrofit 2.5 (current: 2.9), Kotlin 1.5 (current: 1.9), no Coroutines
  • Navigation: Everything funneled through MainActivity with manual Fragment transactions

This single prompt gave me a mental map that would have taken 2 days of manual code reading.

Key insight: Skipping the understanding phase is the single most common reason legacy refactors fail. A structured “archeology” session — asking Claude to map architecture, identify the largest files, find deprecated APIs, and trace navigation flow without changing anything — typically surfaces in 2 minutes what would take 2 days of manual reading, and cuts total refactoring time by 30–40% by preventing rework from hidden dependencies.


What Is the Concentric Rings Refactoring Strategy?

Trying to refactor a large codebase in one pass almost always fails. The concentric rings approach - working from dependencies inward to architecture - gives each layer a stable foundation before the next begins, reducing the chance of cascading breakage across the codebase.

You can’t refactor everything at once. That’s how you break everything at once. I used what I call the “concentric rings” strategy - start from the outside and work inward.

Ring 1: Dependencies - Update libraries without changing code Ring 2: Infrastructure - Replace deprecated APIs with modern equivalents Ring 3: Architecture - Restructure the code into proper layers Ring 4: Features - Build the new feature on the modernized foundation

Each ring must be stable before moving to the next. Claude Code was invaluable at each stage.

Key insight: The concentric rings approach to legacy refactoring — dependencies, then infrastructure, then architecture, then features — works because each layer provides a stable foundation for the next. Attempting to refactor architecture before updating dependencies produces compilation failures from two sources simultaneously, making root-cause analysis nearly impossible in large codebases.


How Do You Safely Update Legacy Dependencies?

Dependency updates are the lowest-risk first step in any legacy modernization. They isolate external change from internal logic, and catching version conflicts early - before touching architecture - prevents a class of errors that are otherwise nearly impossible to untangle.

This is the safest starting point. Update dependencies, fix compilation errors, verify the app still runs.

"Update the Gradle dependencies to their latest stable versions.
For each dependency:
1. Show me the current version vs. recommended version
2. Note any breaking changes in the changelog
3. Make the change in build.gradle.kts
Start with the Kotlin version, then AndroidX, then third-party libraries.
Do them one at a time so we can catch issues early."

Claude Code methodically updated each dependency. The critical technique here: one at a time. When I tried asking Claude to update everything at once (on a different project), it created a compilation nightmare where 15 errors overlapped and it couldn’t figure out which dependency caused which error.

Lesson: For legacy code, always make Claude work in small, verifiable increments.

Key insight: Updating dependencies one at a time — rather than in batch — is the single most reliable way to prevent compilation error pile-ups in legacy Kotlin projects. When Claude updated all dependencies simultaneously in one attempt, 15 overlapping errors made root-cause identification impossible. Sequential updates with a build verification after each change isolated every error to a single, reversible commit.

After updating Kotlin, we hit our first real problem:

"The Kotlin update from 1.5 to 1.9 broke 23 files because of the
new default behavior for sealed interfaces. Can you read the error
messages from ./gradlew build and fix them one file at a time?"

Claude Code fixed each file, running the build between each fix to verify. 23 fixes, zero regressions. Manually, this would have been an afternoon of frustration.


How Do You Replace Deprecated APIs at Scale?

Deprecated API replacement is where AI assistance pays off most clearly. Pattern-based substitutions - like swapping AsyncTask for Coroutines across dozens of files - are exactly the kind of mechanical, high-volume work that takes humans days but follows rules precise enough for Claude to handle in hours.

The biggest task: replacing 47 AsyncTask usages with Coroutines. This is where Claude Code really shines: mechanical, repetitive refactoring that follows a clear pattern.

"We need to replace all AsyncTask usages with Kotlin Coroutines.
Here's the pattern:
BEFORE:
class FetchDataTask : AsyncTask<Void, Void, Result>() {
override fun doInBackground(): Result = api.fetchData()
override fun onPostExecute(result: Result) { updateUI(result) }
}
AFTER:
viewModelScope.launch {
val result = withContext(Dispatchers.IO) { api.fetchData() }
updateUI(result)
}
Start with DataManager.kt - it has the most AsyncTask usages (12).
Replace them one method at a time. After each replacement, show me the diff."

The key was giving Claude a clear before/after pattern and constraining it to one method at a time. When I let it replace all 12 at once in a previous attempt, it introduced a subtle race condition because two of the AsyncTasks had a dependency relationship that wasn’t obvious from the code.

Lesson: Legacy code hides dependencies. Replace incrementally and verify at each step.

Key insight: Replacing AsyncTask with Kotlin Coroutines is the highest-leverage deprecated API migration in Android legacy codebases. The pattern is mechanical — doInBackground becomes withContext(Dispatchers.IO), onPostExecute becomes post-await code in viewModelScope.launch — but hidden inter-task dependencies make batch replacement dangerous. Two AsyncTasks with a timing relationship can appear independent until both are replaced simultaneously.

Try it now: Run this archeology prompt on any legacy project you own: “Read the project structure and list the 5 largest files, all deprecated API usages, and any outdated dependencies. Don’t change anything.” See what it surfaces in under 2 minutes.


How Do You Break Up a God Activity?

A God Activity or God Class is the most expensive technical debt in an Android project. Research from Microsoft and Google suggests that classes with more than 10 responsibilities are responsible for a disproportionate share of bugs - breaking them up one responsibility at a time is slower upfront but dramatically safer than a full rewrite.

The hardest part. MainActivity.kt was 3,100 lines with 14 responsibilities: authentication, navigation, data loading, notifications, deep links, analytics, and more. All in one class.

I did NOT ask Claude to refactor this all at once. Instead:

Step 1: Identify responsibilities

"Read MainActivity.kt and categorize every public and private method
into responsibility groups. Example groups: authentication, navigation,
data loading, UI updates, notifications. Just the categorization, no code changes."

Claude identified 14 distinct responsibilities with 47 methods.

Step 2: Extract one responsibility at a time

"Extract the authentication responsibility from MainActivity into a
new AuthManager class. This includes these methods:
- checkLoginState() (line 145)
- handleLogin() (line 178)
- handleLogout() (line 210)
- refreshToken() (line 245)
Create AuthManager in src/auth/AuthManager.kt. Use constructor injection
for dependencies. Update MainActivity to delegate to AuthManager.
Keep the same public behavior - this is a refactoring, not a rewrite."

Each extraction followed the same pattern:

  1. Identify the methods belonging to one responsibility
  2. Create a new class
  3. Move methods, inject dependencies
  4. Update MainActivity to delegate
  5. Verify the build passes
  6. Test the specific feature manually

After 14 extractions over 3 sessions, MainActivity.kt went from 3,100 lines to 180 lines, handling just lifecycle management and delegation.

Key insight: Breaking a God Activity by responsibility — not by line count or arbitrary splitting — is what produces maintainable extracted classes. Identifying all 14 responsibilities first (authentication, navigation, data loading, notifications, deep links, analytics, and more), then extracting one complete responsibility per session, preserves all behavioral contracts while eliminating the 3,100-line monolith that made every bug fix a risk.


What Mistakes Should You Avoid When Refactoring Legacy Code?

Mistake 1: Asking for Too Much at Once

My first attempt at Ring 3 was: “Refactor MainActivity into proper MVVM architecture.” Claude generated a massive diff that changed 2,400 lines simultaneously. It looked good on paper. But half the app was broken because Claude didn’t account for the implicit state dependencies between the 14 responsibilities.

Fix: Break every large refactoring into single-responsibility extractions.

Mistake 2: Not Creating Checkpoints

During Ring 2, I did 15 consecutive AsyncTask replacements in one session without committing. Claude introduced a subtle bug in replacement #8 that I didn’t notice until replacement #15. I had to undo everything and start over.

Fix: git commit after every successful replacement. Make Claude’s changes atomic and reversible.

Terminal window
# My actual workflow for each replacement:
git add -A && git commit -m "refactor: replace AsyncTask in FetchDataTask"
# If anything breaks:
git revert HEAD

Mistake 3: Trusting Claude’s “This is Safe” Claims

Claude Code sometimes says “this change is safe and won’t affect behavior.” For legacy code, this is almost never true. Behavior that looks unused might be called via reflection. A “dead” code path might be triggered by a specific server response. Legacy code is full of hidden mines.

Fix: Verify every “safe” change. Run the build. Test the feature. Don’t trust, verify.

Key insight: The three most common mistakes in AI-assisted legacy refactoring share a root cause: scope that is too large. Asking for “MVVM refactoring” of 2,400 lines at once, chaining 15 replacements without commits, and trusting “this is safe” claims all fail because they remove the ability to isolate and revert a single broken change. Atomic, verifiable steps are the only reliable approach.


What Were the Results?

The modernization cut the average time to understand a feature from 30-60 minutes to 5-10 minutes - a 5-6x improvement in developer comprehension speed. That compounding effect on every future change is often worth more than the refactoring effort itself.

MetricBeforeAfter
MainActivity.kt3,100 lines180 lines
AsyncTask usages470
Deprecated APIs593 (intentionally kept)
Build warnings340+12
Average class size450 lines120 lines
Time to understand a feature30-60 min5-10 min

The entire modernization took about 3 weeks (part-time). Without Claude Code, I estimate it would have taken 8-10 weeks. The key wasn’t that Claude was fast. It’s that it could do the mechanical parts (dependency updates, pattern-based replacements, extract-and-delegate) with perfect consistency while I focused on the architectural decisions. That division of labor — AI handles mechanical execution, human handles judgment — is the practical foundation of harness engineering for Claude Code, where the system around the model enforces constraints so the model can operate reliably at scale.

Key insight: The most durable improvement from this 50,000-line modernization was the reduction in time-to-understand a feature: from 30–60 minutes to 5–10 minutes. That 5–6x improvement in developer comprehension speed compounds on every subsequent change, meaning the refactoring effort continues to pay dividends long after the project is complete.

Get weekly Claude Code tips — One practical tip every week. No fluff, no spam. Subscribe to AI Developer Weekly →


What Is the Legacy Refactoring Technique Checklist?

Seven principles cover the majority of failure modes in legacy refactoring. Teams that follow all seven consistently report fewer regressions and faster feature velocity within weeks - not months - of completing the modernization work.

If you’re facing a legacy codebase, here’s the approach that worked:

  1. Archeology first. Spend one full session just understanding. Don’t change anything.
  2. Concentric rings. Dependencies → Infrastructure → Architecture → Features.
  3. One responsibility at a time. Never refactor two things simultaneously.
  4. Give Claude a pattern. Show before/after for the first replacement, then let it repeat.
  5. Commit after every change. Make rollback trivial.
  6. Verify every “safe” claim. Legacy code lies. Always verify.
  7. Keep sessions focused. One ring per session. Start fresh for each ring.

Key insight: These seven principles cover the majority of failure modes in legacy refactoring with AI assistance. Teams that apply all seven consistently — particularly atomic commits and single-responsibility extractions — report fewer regressions and faster feature velocity within weeks, not months, of completing the modernization work.


FAQ

How large does a codebase need to be before Claude Code helps with refactoring? There’s no minimum size. Even a 5,000-line codebase with no tests benefits from the archeology prompt to map dependencies and deprecated APIs. The efficiency gains become more pronounced above 20,000 lines where manual mapping becomes genuinely costly.

Can Claude Code introduce regressions when replacing deprecated APIs? Yes - and this post covers exactly that risk. The main mitigation is incremental replacement with a git commit after each change. When Claude replaces one method at a time and you verify the build after each step, any regression is isolated to a single, easily reverted commit.

Should I write tests before or after refactoring legacy code with no existing tests? Writing tests before refactoring is ideal but often impractical when none exist and the codebase is poorly structured. A workable middle path: use Claude to write characterization tests (tests that capture current behavior, right or wrong) before each ring, then improve them after the architecture is cleaner.

How do I handle legacy code that uses reflection or dynamic dispatch that Claude can’t see statically? Flag these areas during the archeology phase by asking Claude to search for Class.forName, invoke, and annotation-based routing. Treat any code Claude marks as “safe to remove” in these areas with extra skepticism and test the specific flows manually.

How many sessions should I expect a 50,000-line refactoring to take? Plan for one session per ring, with Ring 3 (architecture) taking 2-3 sessions for large God classes. In this project, 14 responsibility extractions from one class took 3 sessions. Budget roughly one session per 3-5 responsibility extractions as a planning heuristic.


Legacy code refactoring with Claude Code is covered in detail in Phase 9: Legacy Code & Brownfield Projects of the Claude Code Mastery course. Phases 1-3 are free.