Upgrading a Scala compiler used to be an event. In the Scala 2 era every minor version, 2.11, 2.12, 2.13, broke binary compatibility, so a whole ecosystem had to republish before anyone could move. Scala 3 changed the deal. Within the 3.x series, newer compilers can consume libraries built by older ones, so applications can upgrade on their own schedule, and since Scala 3.3 the project has published Long-Term Support releases alongside a faster-moving Next line.

That model only helps if you know its rules, because the rules are asymmetric. A library compiled with a newer minor version cannot be used by a project on an older one, and that single fact decides which compiler a library should build with. This article explains the Next and LTS lines, what the compatibility guarantees do and do not cover, what changed with Scala 3.8 and the 3.9 LTS released on 3 September 2026, how library authors and application teams should choose versions, how to check compatibility in CI, and a runbook for upgrades. For the language itself, see the Scala 3 overview; this page is about versions and promises.

Two release lines: Next and LTS

Scala 3 is released on two lines. The Next line receives new minor versions regularly; each may add language features, stabilise experimental ones, tighten warnings and raise minimum requirements. The LTS line is a single minor version kept alive with patch releases for years, receiving bug fixes, security fixes and JDK compatibility work but no new language features. Scala 3.3 was the first LTS. Scala 3.9.0, released on 3 September 2026, is the second, three years later.

LTS line3.3 LTSpatches only; JDK 8+next LTS, 2026-09-033.9 LTSat least three years; JDK 17+Next line3.43.5 ... 3.73.8JDK 17, stdlib in Scala 33.10+new featuresstabilises intocontinues asLibrary built on 3.3usable by 3.3 .. 3.9 .. 3.10+Library built on 3.9usable by 3.9 and Next, NOT by 3.3Newer compilers read older artifacts; older compilers cannot read newer ones.A library chooses its users when it chooses its compiler version.
Two release lines: Next moves through minor versions and periodically stabilises into an LTS; compatibility flows from older compilers to newer ones, never back.

The published support windows are the part to put in a planning document. According to the 3.9 release announcement, 3.9 LTS is guaranteed maintenance for at least three years, and 3.3 LTS remains actively supported for one year after 3.9.0, which puts the end of its active period in the second half of 2027. Earlier project communication described 3.3 dropping afterwards to a reduced mode of JDK compatibility updates and critical fixes only, similar to Scala 2.12 today. Treat those dates as the project's stated intent, and re-check them on scala-lang.org before committing a multi-year plan.

The LTS releases also differ in what they demand from the platform. Scala 3.3 runs on old JDKs. Scala 3.8 raised the minimum to JDK 17 and was the first release whose standard library is compiled with Scala 3 itself instead of reusing the Scala 2.13 library. 3.9 LTS inherits both. If any of your deployment targets still run JDK 8 or 11, that, not the language, is what keeps you on 3.3.

What the compatibility guarantees cover

The guarantees are stated in terms of patch and minor versions. Within a single minor version, patch releases are both backward and forward compatible: code compiled with 3.9.2 links against libraries built with 3.9.0 and the other way round. Across minor versions, Scala 3 promises backward compatibility only: a project on 3.9 can use a library built with 3.3, but a project on 3.3 cannot use a library built with 3.9. The same policy applies to TASTy, the typed tree format that Scala 3 stores in class files and that the compiler reads when it type-checks against a dependency. Scala 3 can also consume libraries built with Scala 2.13.

Library built withUsed by 3.3.xUsed by 3.9.xUsed by 3.10+Used by 2.13 with TASTy reader
3.3.xyesyesyesyes
3.7.xnoyesyesyes
3.8.xnoyesyesno
3.9.xnoyesyesno
2.13.xyesyesyesnative

The last column is new pressure in 2026. Scala 2.13 can read Scala 3 libraries through its -Ytasty-reader option, which made mixed 2.13 and 3 builds workable during migration. According to the 3.9 release notes, that reader works only for artifacts up to Scala 3.7, not 3.8 or later. A library that moves to 3.8 or 3.9 therefore drops its Scala 2.13 consumers who relied on that bridge, unless it keeps cross-publishing a 2.13 build. The same notes warn that runtime reflection through scala-reflect can fail on 3.8 and later because the Scala 3 compiled standard library lacks the Scala 2 signature attributes it expects.

What the guarantees do not cover is source compatibility. A newer compiler may reject or warn on code that an older one accepted, because a deprecated feature was removed or a bug that allowed unsound code was fixed. Binary compatibility means your dependencies keep linking; it does not mean your own source compiles unchanged. The compiler's -source setting is the escape hatch: -source:3.3 asks a newer compiler to treat code as the older language version, and the matching -source:<version>-migration setting together with -rewrite lets it rewrite code automatically where a migration path exists.

Choosing a compiler version for a library

Because compatibility only flows forward, the compiler version a library builds with is a statement about who can use it. The conservative rule that most of the ecosystem followed during the 3.3 era was simple: build libraries on the oldest supported LTS, so every application on that LTS or any later version can depend on them. Applications, which nobody depends on, can use whatever version they like.

With two LTS lines alive, library authors face a real choice. Staying on 3.3 keeps the widest audience, including JDK 8 and 11 users and 2.13 users reading through TASTy, but gives up features that landed later and ties you to a line whose active support ends in 2027. Moving to 3.9 gives access to everything stabilised since 3.3 and to the newer standard library, and loses every user still on 3.3. A common middle path is to keep the main branch on 3.3 until a stated date, publish from 3.9 after that, and announce the switch in release notes a few versions early. Whatever you choose, make it explicit in the build and the README rather than letting a dependency bump drag the compiler forward.

// build.sbt for a library that targets the 3.3 LTS audience
ThisBuild / scalaVersion       := "3.3.x"        // pin the latest 3.3 patch, not a Next version
ThisBuild / crossScalaVersions := Seq("3.3.x", "2.13.x")

// Fail the build if this release breaks binary compatibility with the last one
mimaPreviousArtifacts := Set(organization.value %% name.value % "1.4.0")

Replace the placeholders with exact patch versions. Dependencies matter too: if any library you depend on is built with 3.9, your library effectively requires 3.9 regardless of your own scalaVersion. sbt surfaces this as a compiler error when the older compiler meets newer TASTy, which is the moment many teams first learn the rule. Build tooling details for sbt and Mill are covered separately.

Checking compatibility in CI

Promises are cheap; checks are what keep them. A library should run three kinds of compatibility checks on every pull request.

  1. Binary compatibility of your own API. MiMa compares the class files of the new build with the previous release and fails on removed or changed public members. It catches the breakage your users would see as a linkage error at runtime.
  2. TASTy compatibility. TASTy-MiMa performs the equivalent comparison on TASTy, catching changes that keep bytecode compatible but break code that inlines or type-checks against your library, such as changes to inline methods.
  3. A consumer matrix. A small sample project that depends on the freshly built artifact and compiles on each Scala version you claim to support, for example the oldest supported 3.3 patch, current 3.9, and the latest Next. This is the only check that catches an accidental compiler or dependency bump.

Applications need less: a build on the chosen version with warnings promoted to errors, and a scheduled job that compiles against the latest Next release so deprecations are seen months before they become removals. Dependency update bots help here, as long as compiler version bumps are reviewed rather than auto-merged.

Experimental features and how they stabilise

The Next line is also where new features are tried, and Scala separates them carefully. Features go through the Scala Improvement Process; while unsettled they are marked experimental, and experimental language features and definitions annotated @experimental can only be used after an explicit opt-in. Code that uses an experimental definition must itself be experimental, so the marker spreads through your codebase. That is deliberate: an experimental feature may change or disappear in any minor release, and the stability guarantees above do not extend to it.

The practical rule is to keep experimental code out of anything published, and to quarantine it in applications behind a small module you are willing to rewrite. Features that do stabilise reach the next LTS: Scala 3.8 stabilised, among others, the better-fors syntax and runtimeChecked, and 3.9 stabilised SIP-71, the into mechanism for declaring which parameters accept implicit conversions. Metaprogramming deserves extra caution because macros compile against compiler internals exposed through the quotes API; see Scala 3 metaprogramming for what is stable there.

Worked example: upgrading from 3.3 LTS to 3.9 LTS

A worked example makes the runbook concrete. Consider an application on 3.3 LTS with forty dependencies, deployed on JDK 11, that wants 3.9 LTS. The work splits into platform, dependencies and source.

  1. Platform first. Move the runtime to JDK 17 or later while still on Scala 3.3, which supports it. Deploy and soak. This isolates JVM behaviour changes from compiler changes.
  2. Inventory dependencies. List every Scala library and the newest version of each. Anything without a 3.x build is a blocker regardless of minor version. Libraries built on 3.3 are fine on 3.9; you do not need them to upgrade first.
  3. Compile with warnings visible. Switch scalaVersion to the latest 3.9 patch with -deprecation and -feature on, and read every new warning before fixing anything. Use -source:3.3 temporarily if a language change blocks compilation, and -rewrite with the matching migration source where it applies.
  4. Check runtime-sensitive code. Search for scala-reflect usage, Java serialisation of Scala collections, and frameworks that inspect Scala signatures, since the 3.8 standard library change affects those paths.
  5. Run the full test suite, then a canary. Ship one instance, compare error rates and latency, then roll out.
  6. Remove the crutches. Drop -source:3.3 once the code compiles cleanly at the new level, so the next upgrade starts clean.

Teams that take this path once per LTS spend a few days every three years. Teams that follow Next instead upgrade more often, in smaller steps, and get features sooner; both are legitimate, but mixing them, an application on Next publishing internal libraries others consume, recreates the forward-compatibility trap inside your own organisation.

Failure modes

  • Accidental Next library. An internal library bumps to the latest compiler; every service on 3.3 fails with a TASTy version error. Pin library compilers and check with a consumer matrix.
  • Transitive compiler bump. A dependency release built with 3.9 forces all consumers onto 3.9. Read release notes for compiler changes, and test dependency upgrades on the oldest supported version.
  • Silent 2.13 breakage. Moving a shared library to 3.8 or later cuts off 2.13 consumers that used the TASTy reader. Cross-publish or announce the drop.
  • JDK mismatch. Upgrading to 3.8 or 3.9 on a fleet with JDK 11 hosts fails at startup or in CI images. Upgrade the JDK first.
  • Experimental leakage. A published API exposes an experimental type and breaks on the next minor. Keep experimental code internal.
  • Choosing 3.8.0. The 3.8.0 release had a runtime regression; the project recommends 3.8.1 or later.

What to do next

  1. Write down, in your build or README, whether each project follows LTS or Next and why.
  2. For every library you publish, check which compiler version it builds with, and decide whether to stay on 3.3 or move to 3.9, with a date.
  3. Add MiMa and TASTy-MiMa to library CI, plus a consumer project compiled on each supported Scala version.
  4. Inventory JDK versions across environments; plan the move to JDK 17 before any 3.8 or 3.9 upgrade.
  5. Search for scala-reflect, 2.13 consumers using the TASTy reader and experimental annotations; each is a known upgrade risk.
  6. Add a scheduled CI job that compiles against the latest Next release to see deprecations early.
  7. Mark the end of 3.3 LTS active support in your planning calendar and re-check the date on scala-lang.org each quarter.
  8. Review how givens and other contextual abstractions in your code interact with deprecation warnings when you raise the compiler version.
Key takeaway: Scala 3 compatibility flows one way: newer compilers read artifacts from older ones, and within a minor version patches are interchangeable. Libraries therefore choose their audience when they choose a compiler, and the move from 3.3 LTS to 3.9 LTS, with its JDK 17 baseline and Scala 3 standard library, is a deliberate decision with a date. Pin compiler versions, check binary and TASTy compatibility in CI, upgrade the JDK first, and keep experimental features out of published code.