Build a Migration Plan That Actually Works

Migrating a large-scale Scala project isn't just about changing a version string in your build.sbt. It requires a systematic approach to minimize downtime and prevent regression. The industry-standard path is the 2.13 Bridge: migrating from 2.12 to 2.13 first, then finally to 3.3 LTS.

The 2.13 Bridge Strategy

Why not go straight to 3.3? Scala 2.13 was designed as a transition layer. It shares the same standard library collection framework as Scala 3, which is the biggest source of breaking changes. By moving to 2.13 first, you solve 80% of your source-compatibility issues while still staying within the familiar Scala 2 compiler ecosystem.

Real-World Example: The "Big Bang" vs. Incremental

Imagine a project with 50 microservices. A "Big Bang" migration (upgrading everything at once) usually fails because of transitive dependency conflicts. An Incremental approach using the 2.13 bridge allows you to upgrade one module at a time, keeping the rest of the system running on 2.12 via binary compatibility layers.

Step 1: Taking Inventory

Before writing any code, you must know what you're up against. Run an inventory check on three critical areas:

1. Compiler Flags and Options

Identify which flags are deprecated in 2.13 or removed in 3.0.

  • 2.12 Flag: -Ypartial-unification (Essential for Typelevel libraries)
  • 3.3 Status: Enabled by default, flag removed.

2. Macros and Compiler Plugins

Macros are the biggest hurdle. Scala 2 macros (Def Macros) are not compatible with Scala 3 (which uses Tasty/Quotes).

  • Check: Are you using better-monadic-for or kind-projector? These have specific migration paths or are built into the Scala 3 compiler.

3. Library Dependencies

Check if your critical dependencies (ZIO, Cats, Akka/Pekko) have 2.13 and 3.3 releases. Use the sbt plugin sbt-dependency-graph to find "stuck" transitive dependencies.

Step 2: Choosing Your Build Strategy

Cross-Building

If you are a library author, you likely need to support both Scala 2.13 and 3.3 simultaneously.

// build.sbt
crossScalaVersions := Seq("2.13.12", "3.3.1")

The "Big Bang" (Module-by-Module)

For internal applications, it's often better to move a whole module to the next version and never look back. This reduces the complexity of maintaining multiple version-specific source folders.

Step 3: The Migration Checklist

A successful migration plan must include these four pillars:

  1. Test Coverage: You cannot migrate safely without a green test suite. Ensure your tests run in under 5 minutes to allow for rapid iteration during the "fix-it" phase.
  2. CI Matrix: Set up a separate CI job that attempts to compile the project with the target version. Don't let it block the main build yet, but use it to track progress.
  3. Binary Compatibility (MiMa): If you provide internal SDKs, use the sbt-mima-plugin to ensure that moving to 2.13 doesn't break downstream services still on 2.12.
  4. The "Fatal Warnings" Policy: Enable -Xfatal-warnings early. It forces you to fix deprecations in 2.12 that would otherwise become hard errors in 2.13 or 3.3.

Summary

Your first day of migration isn't about fixing code; it's about visibility. Once you have an inventory of your macros and a CI job showing the 1,000+ errors you need to fix, you can finally start the real work.