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.


The Starting Point: Archeology Mode

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.


The Strategy: Concentric Rings

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.


Ring 1: Dependency Updates

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.

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.


Ring 2: Replacing Deprecated APIs

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.


Ring 3: Breaking Up the God Activity

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 — just lifecycle management and delegation.


The Mistakes I Made

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.


The Results

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.


The Technique Checklist

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.

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.