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 map2. Identify the largest files (by line count)3. Find deprecated API usage4. Check the Gradle dependencies for outdated libraries5. 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 version2. Note any breaking changes in the changelog3. 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 thenew default behavior for sealed interfaces. Can you read the errormessages 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 methodinto 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 anew 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 injectionfor 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:
- Identify the methods belonging to one responsibility
- Create a new class
- Move methods, inject dependencies
- Update MainActivity to delegate
- Verify the build passes
- 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.
# My actual workflow for each replacement:git add -A && git commit -m "refactor: replace AsyncTask in FetchDataTask"# If anything breaks:git revert HEADMistake 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
| Metric | Before | After |
|---|---|---|
| MainActivity.kt | 3,100 lines | 180 lines |
| AsyncTask usages | 47 | 0 |
| Deprecated APIs | 59 | 3 (intentionally kept) |
| Build warnings | 340+ | 12 |
| Average class size | 450 lines | 120 lines |
| Time to understand a feature | 30-60 min | 5-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:
- Archeology first. Spend one full session just understanding. Don’t change anything.
- Concentric rings. Dependencies → Infrastructure → Architecture → Features.
- One responsibility at a time. Never refactor two things simultaneously.
- Give Claude a pattern. Show before/after for the first replacement, then let it repeat.
- Commit after every change. Make rollback trivial.
- Verify every “safe” claim. Legacy code lies. Always verify.
- 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.