Contents

Migrating to Scala 3

Migrating to Scala 3 webp image

There are several ways to migrate your existing Scala 2 project to Scala 3. You could start with implementing a new separate service in Scala 3 which is called a Parallel Migration. This approach has a few pros and cons however, with significant hurdles being the need to manage two codebases temporarily and the possibility of APIs being inconsistent if not handled carefully. It can be a challenging task, but sometimes it is the only way.

In this blog post, I’d like to explore alternative approaches for migrating an existing Scala 2 codebase to Scala 3 – options that might be a better fit for many of us planning the transition. With Scala 3 introducing exciting new features, it’s natural that we’re eager to take advantage of them! 🙂

Cross-Building a Scala 2 project with Scala 3

Migrating a project from Scala 2 to Scala 3 can be complex, particularly for large-scale applications. Instead of attempting a full migration in one step, cross-building (also known as cross-compile) allows for a gradual transition while ensuring that your codebase remains functional in both versions.

This method enables your project to compile with both Scala 2 and Scala 3, allowing incremental changes, continuous verification, and smoother dependency transitions before entirely switching to Scala 3.

What is Cross-Building?

Cross-building refers to compiling the same codebase against multiple Scala versions. This is particularly useful in situations where you need to:

  • Test Compatibility
    • Ensure that your Scala 2 code remains valid under Scala 3.
    • Detect potential breaking changes before full migration.
  • Migrate Incrementally
    • Refactor the codebase progressively while maintaining compatibility.
    • Reduce migration risks for large projects.
  • Adapt to Library Support
    • Some dependencies may not yet support Scala 3.
    • Cross-building allows you to continue using Scala 2 while waiting for library updates.

Setting up cross-building in SBT

To enable cross-building in SBT, update your build.sbt with the following:

ThisBuild / crossScalaVersions := Seq("2.13.20", "3.3.6")
ThisBuild / scalaVersion := "2.13.20" // Default Scala version

Note: Using Scala LTS e.g. 3.3.x, gives you less warnings and a more incremental approach-- advice based on experience from VirtusLab developers.

Compiling for Different Versions

Once cross-building is configured, you can compile and test your project with different Scala versions:

sbt ++2.13.20 compile  # Compile using Scala 2.13
sbt ++3.3.6 compile    # Compile using Scala 3

SBT will automatically determine which source files and dependencies apply to each version.

Handling Scala 2 and Scala 3 differences

While many Scala 2 features are compatible with Scala 3, some may require adaptation. You can create version-specific directories for such cases:

src/main/scala      -> Shared code (works in both Scala 2 and Scala 3)
src/main/scala-2    -> Code compiled only in Scala 2
src/main/scala-3    -> Code compiled only in Scala 3

SBT will automatically select the appropriate source files based on the active Scala version.

Example: Symbol Literal Syntax

Scala 2 supports symbol literals (e.g.,'mySymbol), but they were removed in Scala 3. You can isolate such usages to maintain compatibility:

Scala 2 (src/main/scala-2/Identifiers.scala)

object Identifiers {
  val symbol = 'mySymbol  // Valid in Scala 2
}

Scala 3 (src/main/scala-3/Identifiers.scala)

object Identifiers {
  val symbol = Symbol("mySymbol")  // Required in Scala 3
}

This demonstrates how to adapt language syntax changes across versions without breaking your shared logic.

Using -Xsource:3 for early detection

Before migrating fully to Scala 3, you can enable forward compatibility checks in Scala 2:

scalacOptions ++= Seq("-Xsource:3")

This helps identify potential issues while still compiling under Scala 2.

Handling dependencies in cross-building

Some libraries may not support Scala 3 yet. You can handle this in build.sbt by using CrossVersion.for3Use2_13 identifier like this:

libraryDependencies += "com.lihaoyi" %% "utest" % "0.7.11".cross(CrossVersion.for3Use2_13)

The above definition will use a Scala 2.13 version of the library. This will work if no macros are used or provided by the library.

You can also try to use the syntax below as well if you must explicitly use different versions for Scala 2 and Scala 3. It can rarely happen that you will have to do it, but you never know:

libraryDependencies ++= {
  CrossVersion.partialVersion(scalaVersion.value) match {
    case Some((3, _))  => Seq("org.typelevel" %% "cats-core" % "2.10.0")  // Scala 3 version
    case Some((2, _))  => Seq("org.typelevel" %% "cats-core" % "2.9.0")   // Scala 2 version
    case _             => Seq()
  }
}

Practical example

To demonstrate how such migration may look like, I used one of our old projects written in Scala 2. Below are the steps I had to perform to migrate the code to Scala 3. You can also review all the changes in that PR.

Upgrade sbt

Let’s start with sbt compile to see what I must do first.

It appears that this project is using sbt 0.13.x.
This version has been deprecated since 2014 and officially reached end-of-life in 2018.
Using it may lead to compatibility issues, missing out on important features, performance improvements, and security fixes.

Please upgrade to the latest sbt version by updating the sbt.version value in the build.properties file.

Helpful Resources:
- Migration Guide: https://www.scala-sbt.org/1.x/docs/Migrating-from-sbt-013x.html
- Latest Releases: https://github.com/sbt/sbt/releases or https://www.scala-sbt.org/download

That was expected. The project is several years old and sbt is outdated. Changing sbt.version defined in build.properties to 1.10.11 (or to the latest available version) solves the problem.

Outdated sbt plugins

Re-running with the new sbt shows information about outdated sbt plugins, which can also be easily fixed:

[warn]  Note: Some unresolved dependencies have extra attributes.  Check that these dependencies exist with the requested attributes.
[warn]      me.lessis:bintray-sbt:0.3.0 (sbtVersion=1.0, scalaVersion=2.12)
[warn]      com.geirsson:sbt-scalafmt:0.4.10 (sbtVersion=1.0, scalaVersion=2.12)

The plugin com.geirsson" % "sbt-scalafmt" % "0.4.10" can be replaced by the latest version of sbt-scalafmt plugin like this:

addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0")

Meanwhile, the second plugin "me.lessis" % "bintray-sbt" % "0.3.0" can be simply removed from the config as it’s not needed right now.

Integration tests

I won't delve into the tests right now. What I want to achieve is to be able to compile the project’s main source code with Scala 3.

slackit/build.sbt:44: warning: value IntegrationTest in trait LibraryManagementSyntax is deprecated (since 1.9.0): Create a separate subproject for testing instead
val slackit = (project in file(".")).settings(commonSettings).settings(dependencies).configs(IntegrationTest).settings( Defaults.itSettings : _*)

We can ignore this warning for now and perform the rest of the migration first.

Scala version

Let's upgrade the very outdated Scala version 2.11.8 to the latest Scala 2.13.x as a first step in my path to Scala 3. After reloading the project by using the reload command I will see what happened.

Dependencies

Once I tried to compile the project using ;clean;compile a bunch of artifacts cannot be resolved as they don't exist for Scala 2.13. Let's upgrade each of them to the latest version. Please remember to reload the project once you change all the dependencies, and let's try to compile again.

After upgrading Circe to the latest version, I got an issue with de.heikoseeberger:akka-http-circe - if you open the project site on Github, you can notice the project is archived and it's part of Pekko now. Let's try to use the latest version anyway.

Using 1.40.0-RC3 helped, so I am good to go! Finally, I removed scalacOptions as not needed for now.

Code changes

After upgrading all the dependencies to the latest versions, I was able to reload the project once more, and all looked good at first glance. Yet running compile exposed a few problems around the code. First of all CirceSupport object was gone, so I had to replace it with FailFastCirceSupport. Re-compiling the project showed a few binary incompatibilities, so I decided to use Akka-related versions matching AkkaHttp Circe 1.39.2. This solved all the problems.

Toward Scala 3

Once the above problems have been addressed, it's time to check if the current code syntax won't produce too many warnings and errors when running on Scala 3. The simplest option is to add a compiler flag -Xsource:3 like this:

scalacOptions := Seq("-Xsource:3")

and recompile the project.

Having this done and if no warnings or errors have been spotted, we can try to switch to Scala 3. Let's use LTS version to avoid major problems

scalaVersion := "3.3.6"

Time to reload and recompile the project to see if all is good :)

Migration to Pekko

That would be too easy, in most cases Akka is a blocker in migration to Scala 3:

[info] Resolved slackit_3 dependencies
[warn]
[warn]  Note: Unresolved dependencies path:
[error] stack trace is suppressed; run last update for the full output
[error] (update) sbt.librarymanagement.ResolveException: Error downloading com.typesafe.akka:akka-http-core_3:10.2.7
[error]   Not found
[error]   Not found
[error]   not found: ~/.ivy2/local/com.typesafe.akka/akka-http-core_3/10.2.7/ivys/ivy.xml
[error]   not found: https://repo1.maven.org/maven2/com/typesafe/akka/akka-http-core_3/10.2.7/akka-http-core_3-10.2.7.pom
[error] Error downloading de.heikoseeberger:akka-http-circe_3:1.39.2
[error]   Not found
[error]   Not found
[error]   not found: ~/.ivy2/local/de.heikoseeberger/akka-http-circe_3/1.39.2/ivys/ivy.xml
[error]   not found: https://repo1.maven.org/maven2/de/heikoseeberger/akka-http-circe_3/1.39.2/akka-http-circe_3-1.39.2.pom
[error] Error downloading com.typesafe.akka:akka-http_3:10.2.7
[error]   Not found
[error]   Not found
[error]   not found: ~/.ivy2/local/com.typesafe.akka/akka-http_3/10.2.7/ivys/ivy.xml
[error]   not found: https://repo1.maven.org/maven2/com/typesafe/akka/akka-http_3/10.2.7/akka-http_3-10.2.7.pom
[error] Total time: 1 s, completed Apr 18, 2025, 11:39:17 AM

Before being able to use Scala 3, I must migrate to the Apache Pekko, so let's do it. After all the dependencies upgrades and code changes, what's left is this error:

[error] -- Error: slackit/src/main/scala/com/softwaremill/slackit/webapi/users/UsersApi.scala:48:106
[error]  48 |      exec(GetUsersInfo(token, userId)).via(asJsonFlow()).via(circeDecoderFlow[UserObject, Member](_.user))
[error]     |                                                                                                          ^
[error]     |           method derived is declared as `inline`, but was not inlined
[error]     |
[error]     |           Try increasing `-Xmax-inlines` above 32
[error]     |---------------------------------------------------------------------------
[error]     |Inline stack trace
[error]     |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[error]     |This location contains code that was inlined from package.scala:31
[error]     |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

so let's increase the max number of inlines:

scalacOptions := Seq("-Xmax-inlines:1024")

Now reload and recompile the code. Once done, full success!

sbt:slackit> ;clean;compile
[success] Total time: 0 s, completed Apr 18, 2025, 12:02:31 PM
[info] compiling 27 Scala sources to slackit/target/scala-3.3.5/classes ...
[warn] -- [E029] Pattern Match Exhaustivity Warning: slackit/src/main/scala/com/softwaremill/slackit/package.scala:31:10
[warn] 31 |          case container: ErrorContainer if container.ok => response
[warn]    |          ^
[warn]    |match may not be exhaustive.
[warn]    |
[warn]    |It would fail on pattern case: com.softwaremill.slackit.ErrorContainer(_, _)
[warn]    |
[warn]    | longer explanation available when compiling with `-explain`
[warn] one warning found
[success] Total time: 3 s, completed Apr 18, 2025, 12:02:34 PM
sbt:slackit>

I still have to fix unit tests, but the main code compiles with Scala 3, which is what I wanted to demonstrate.

Conclusion

Cross-building is a powerful strategy for gradually migrating a Scala 2 service to Scala 3. By compiling the same codebase under both versions, you enable incremental migration, better compatibility testing, and smoother dependency transitions. It is particularly useful if you need a safe migration path without disrupting production, have a large and complex codebase requiring incremental refactoring, or if some dependencies haven’t fully migrated to Scala 3 yet. Migrating a project from Scala 2 to Scala 3 doesn’t always require a complete rewrite. Scala 3 provides interoperability mechanisms that allow Scala 3 code to work with Scala 2.13 binaries using TASTy Reader. This approach enables a gradual transition without forcing immediate in-place changes across your entire codebase.

Choosing the right migration strategy is the key to a successful transition to Scala 3. No matter which approach you take, it’s essential to plan each step carefully and reassess your strategy as you progress. Migration is a learning process - unexpected challenges may arise, and adapting your plan along the way will help you navigate them more effectively.

Equally important is documenting your decisions. Using Architecture Decision Records (ADRs) ensures that the rationale behind your chosen approach is well recorded. This not only helps you track your own reasoning but also provides valuable context for others, especially newcomers who join the project later.

Finally, remember that you're not alone in this process! Many have faced similar challenges, and seeking guidance can save time and effort. Whether it's consulting the community or reaching out to experienced companies, don't hesitate to ask for help - we're here to support you on your migration journey. And there is even more, VirtusLab (Scala maintainers) offer free support for migrating your Scala 2 project to Scala 3.

Do you have any concerns or questions? Don't hesitate to comment!

Reviewed by Paweł Stawicki, Krzysztof Borowski, Łukasz Biały, Tomek Godzik, and Wojciech Mazur.

Blog Comments powered by Disqus.