When I joined CodeBrew Labs in 2020, I inherited a 50K-line Java Android codebase. It had a 4.2% crash rate, a 2.8-star Play Store rating, and spaghetti code that hadn't been refactored since 2017. Clients were threatening to leave.

Over 14 months, we migrated to Kotlin + MVVM and reduced crashes by 35%. Here's the exact process, the pitfalls, and what I'd do differently.

📖 Context

This was a delivery logistics app with 50K+ daily active users. The migration ran in parallel with active feature development — we couldn't pause the roadmap.

The Codebase We Had

Massive Activity classes (some 3,000+ lines), logic mixed between UI and data layers, AsyncTask everywhere, and null pointer exceptions as the top crash reason — every week, consistently. Classic legacy code.

The Migration Strategy: Incremental, Not Big Bang

We adopted the Boy Scout Rule at scale: every new feature went in Kotlin, and every time you touched an existing Java class, you migrated it to Kotlin first.

// Old Java pattern — the culprit behind most NPE crashes
public void loadUser(String userId) {
    User user = userRepository.getUser(userId); // Could return null
    textView.setText(user.getName()); // NPE waiting to happen
}

// Kotlin replacement — nullability is explicit at compile time
fun loadUser(userId: String) {
    viewModelScope.launch {
        val user = userRepository.getUser(userId) // Returns User?
        _uiState.update { it.copy(userName = user?.name ?: "Unknown") }
    }
}

Kotlin's null safety alone eliminated our top crash category within three months. The compiler forces you to handle nulls that Java silently lets through.

The Architecture Rethink

We used the migration as an opportunity to introduce MVVM properly. New screens: ViewModel + Repository + clean separation. Migrated screens: extract logic from Activities into ViewModels first, then the data layer.

The hardest part wasn't the code — it was the team. Getting four developers to follow the new patterns consistently required documented architecture decisions, code review checklists, and weekly architecture syncs. I wrote a 10-page internal guide that became our team's bible.

The Results After 14 Months

  • Crash rate: 4.2% → 2.7% (35% reduction)
  • Play Store rating: 2.8 → 4.3 stars
  • Build times: 20% faster (Kotlin incremental compilation)
  • Code coverage: 8% → 34% (testable architecture made testing viable)
  • Junior onboarding time: halved (clearer, consistent patterns)

The business case for migration isn't abstract — fewer crashes means fewer support tickets, better ratings mean more installs, and better architecture means faster feature delivery. Every engineering leader should know these numbers before going into the budget conversation.