Upgrading a production application to a new Scala version is not just a matter of changing the compiler version in the build file. The application may depend on libraries compiled with different Scala versions, compiler plugins, macros, build-tool integrations, and Java APIs. A change that looks small in the build configuration can expose compatibility problems across several modules.
Scala 3.3.9 LTS provides a relatively controlled upgrade path for applications already running Scala 3.3.x. It includes selected bug fixes and improvements from newer Scala releases while maintaining the compatibility guarantees of the 3.3 line. For teams maintaining existing applications, this makes it worth evaluating as a maintenance upgrade rather than treating it as a language migration.
This article explains how to plan the upgrade, update an sbt project, verify dependencies, and test the application before deploying it.
Understand What Scala 3.3.9 Changes
Scala 3.3.9 is a patch release in the 3.3 LTS series. It backports most bug fixes and selected improvements introduced in Scala 3.9.0, provided they meet the compatibility requirements of the older LTS line.
Some notable changes include:
- Improved Java generic signatures for higher-kinded types and value classes.
- Exhaustiveness checking for sealed abstract Java classes.
- More actionable compiler diagnostics when members are missing.
- Warnings for questionable negative literal syntax.
- A fix for a Scaladoc failure involving JDK 25.
These changes can affect compilation, Java interoperability, and documentation generation. They do not mean that every application will need source-code changes. A project that already compiles successfully on an earlier Scala 3.3 patch version may require only a compiler update and validation.
The distinction between Scala LTS and Scala Next is also important. Scala 3.3.9 belongs to the older LTS line, which continues to receive eligible fixes. Scala 3.10.0 belongs to the Scala Next line and receives newer language and ecosystem development. Choosing between them depends on the application's compatibility requirements, library ecosystem, and release strategy.
For an application that prioritizes a stable maintenance baseline, staying on the 3.3 LTS line can be a reasonable decision. Teams planning a broader language or standard-library transition should evaluate the newer LTS line separately.
Check the Existing Project Before Changing the Version
Start by identifying exactly what the application currently uses. Do not assume that every module shares the same Scala version or that all dependencies are compatible with the target compiler.
Review the following areas:
- Current Scala compiler version.
- sbt version and build plugins.
- Java Development Kit used by local development and CI.
- Scala libraries and their published Scala versions.
- Compiler plugins and macro libraries.
- Modules shared between Scala 2.13 and Scala 3.
- Tests, code generators, and documentation tasks.
For example, an sbt project might currently contain:
ThisBuild / scalaVersion := "3.3.8"
ThisBuild / organization := "com.example"
ThisBuild / version := "1.0.0"
Before upgrading, confirm that the application builds successfully with its current configuration. Record the existing test results and dependency versions. This gives the team a baseline for identifying problems introduced by the upgrade.
It is also useful to inspect the dependency tree and identify libraries that rely on compiler-specific features. A normal runtime dependency and a compiler plugin do not necessarily have the same compatibility requirements.
Update the Scala Version in sbt
For a project already using Scala 3.3.x, the first change is straightforward.
Update the Scala version in build.sbt:
ThisBuild / scalaVersion := "3.3.9"
If the build defines multiple modules, check whether each module inherits the shared setting or declares its own Scala version.
A multi-module project might look like this:
lazy val core = project
.settings(
name := "application-core"
)
lazy val service = project
.dependsOn(core)
.settings(
name := "application-service"
)
lazy val root = project
.aggregate(core, service)
.settings(
name := "application"
)
When the modules inherit the same build-level Scala version, changing the shared setting updates their compiler version together.
For projects with separate version settings, update them deliberately. Avoid leaving one module on an older patch version simply because its configuration is maintained in a different file.
After changing the version, run:
sbt clean compile
A clean compilation is useful because it prevents previously generated class files from hiding compilation problems. If compilation fails, examine the first meaningful compiler error rather than trying to fix every reported message at once.
Review Dependencies and Compiler Plugins
Dependency compatibility is one of the most important parts of a Scala upgrade.
Scala libraries are commonly published with Scala-version-specific artifact names. For example, a dependency published for Scala 3 may use a different artifact from the equivalent library published for Scala 2.13.
A dependency declaration might look like this:
libraryDependencies ++= Seq(
"org.example" %% "example-library" % "1.2.0"
)
The double-percent operator allows sbt to select the artifact associated with the project's Scala version.
However, this does not guarantee that every library release supports Scala 3.3.9. Check the library's published compatibility information, especially when it depends on compiler internals or has version-specific implementations.
Pay particular attention to:
- Compiler plugins that must match the Scala compiler version.
- Libraries that generate code during compilation.
- Macro implementations and annotation-based code generation.
- Serialization and database libraries.
- Framework integrations that depend on specific Scala versions.
If a compiler plugin is incompatible, the application may fail before ordinary source compilation begins. Updating the plugin independently may solve the issue, but verify its supported Scala versions rather than selecting the newest release automatically.
Also avoid upgrading unrelated dependencies at the same time. Keeping the compiler change isolated makes failures easier to diagnose and reduces the number of variables involved in testing.
Check Java and Build Tool Compatibility
Scala applications running on the JVM depend on both the Scala compiler and the Java toolchain.
Scala 3.3 LTS supports JDK 8 and newer versions. The exact combination should still be checked against the application's build tools and libraries. For example, a project using an older sbt release may have different tooling constraints from one using a current sbt version.
Check the JDK used by the developer environment and CI pipeline:
java -version
sbt --version
Do not assume that the Java version shown on a developer's workstation is also used by the build server. CI images, containers, and deployment environments can use different JDK versions.
For applications that integrate with Java libraries, include interoperability checks in the upgrade. Scala 3.3.9 improves some generated Java generic signatures, including signatures involving higher-kinded types and value classes. These improvements are relevant when Java code consumes Scala-generated classes or when Scala code interacts with Java APIs.
A successful Scala compilation does not replace testing these integration points.
Handle Applications Migrating From Scala 2.13
Moving from Scala 2.13 to Scala 3.3.9 requires more work than upgrading an existing Scala 3.3 application.
Most Scala 2.13 source code can be migrated, but some language constructs have changed. The Scala 3 migration guide recommends using Scala 3.3 as an intermediate migration target before moving to later Scala versions.
A practical migration usually involves the following work:
- Identify Scala 2-specific language features and compiler plugins.
- Check whether dependencies have Scala 3-compatible artifacts.
- Review implicit definitions and contextual abstractions.
- Identify Scala 2 macros that need to be rewritten.
- Compile the code using the Scala 3 compiler and address migration diagnostics.
- Run tests and verify runtime behavior.
Scala 2 macros deserve particular attention. Scala 3 uses a different metaprogramming system, so existing Scala 2 macro implementations cannot simply be assumed to work with the new compiler.
Large applications can also consider cross-building shared modules for Scala 2.13 and Scala 3. This allows teams to migrate modules gradually while keeping existing Scala 2 consumers operational. It does, however, introduce additional build and testing overhead.
For a project already on Scala 3.3.8, these migration steps are generally unnecessary. A patch-level upgrade should not be confused with a Scala 2-to-Scala 3 conversion.
Validate the Application Beyond Compilation
Compilation confirms that the source code can be built. It does not prove that the application behaves correctly after the upgrade.
Run the complete test suite, including unit, integration, and end-to-end tests where applicable.
For a typical sbt project:
sbt clean compile
sbt test
sbt IntegrationTest/test
The final command applies when the project has an IntegrationTest configuration. Use the actual test configurations defined in the application's build.
Also verify the parts of the application that interact with external systems:
- Database drivers and connection handling.
- HTTP clients and server frameworks.
- Message brokers and serialization.
- Java-based libraries.
- Scheduled jobs and background workers.
- Reflection, generated classes, and runtime loading.
If the application produces documentation, run the Scaladoc task as well. This is particularly relevant because Scala 3.3.9 includes a Scaladoc fix related to JDK 25.
For production systems, compare application logs, startup behavior, memory usage, and request processing against the previous release. A compiler upgrade should not be considered successful solely because the build pipeline is green.
Roll Out the Upgrade in a Controlled Way
Avoid combining the Scala compiler upgrade with a major framework migration, database change, or large dependency refresh.
A safer rollout is to create a dedicated upgrade branch, change the compiler version, resolve necessary compatibility issues, and run the existing test pipeline. Once the build is stable, deploy the application to a staging environment.
Use staging to verify startup, health checks, integrations, and representative workloads. For services with multiple instances, a gradual production rollout provides an opportunity to detect unexpected behavior before all instances are updated.
Keep the previous application artifact available so that the team can roll back if the new build causes problems. Also record the compiler and JDK versions used to produce the release artifact. This makes the deployment easier to reproduce.
Common Upgrade Problems
| Problem |
Likely cause |
What to check |
| Dependency resolution fails |
A library does not publish a compatible Scala 3 artifact |
Check the dependency's supported Scala versions |
| Compiler plugin fails to load |
Plugin and compiler versions are incompatible |
Verify the plugin's Scala version support |
| Compilation reports new warnings |
Updated compiler diagnostics |
Review the affected source code and warning configuration |
| Java integration behaves differently |
Generated signatures or interoperability assumptions |
Inspect Java-facing APIs and integration tests |
| Documentation generation fails |
Scaladoc or JDK compatibility issue |
Run the documentation task with the intended JDK |
| CI passes locally but fails remotely |
Different JDK, sbt, or dependency environment |
Compare toolchain versions and build settings |
Do not suppress new compiler warnings without understanding their cause. Some warnings identify questionable code that deserves correction, while others may be acceptable in a specific project context. The appropriate response depends on the warning and the application's compatibility requirements.
Conclusion
Scala 3.3.9 LTS gives teams maintaining Scala 3.3 applications an updated compiler with selected fixes and improvements while preserving the compatibility expectations of the existing LTS line.
The upgrade is relatively contained when the application already runs on Scala 3.3.x, but dependency compatibility, compiler plugins, Java integration, and CI toolchains still need attention. Applications moving from Scala 2.13 should treat the work as a separate migration project, particularly when macros and Scala-version-specific libraries are involved.
A controlled compiler update, followed by clean builds, integration testing, and a staged deployment, provides a practical way to adopt Scala 3.3.9 without introducing unnecessary changes into an established application.
Join the conversation! Your thoughts help the community grow.