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-fororkind-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:
- 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.
- 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.
- Binary Compatibility (MiMa): If you provide internal SDKs, use the
sbt-mima-pluginto ensure that moving to 2.13 doesn't break downstream services still on 2.12. - The "Fatal Warnings" Policy: Enable
-Xfatal-warningsearly. 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.
Comments
Be the first to comment on this lesson!