How to Migrate to Scala 3 Without Disrupting Production

Most teams that decide to migrate to Scala 3 treat it as one event. Pick a sprint, switch the compiler, fix what breaks, ship it. For a small service with one branch and no external users depending on stability, that can work fine. For a production system your business runs on, that approach is how a migration turns into an outage, or into a project nobody wants to touch again for another year.

There's a different way to run this, one where the migration never blocks a release, never freezes the team, and never risks an intermediate broken state. It takes longer. It also means nothing ever breaks on the way there.

TL;DR

A non-disruptive Scala 3 migration runs a parallel probe branch against the new compiler while your team keeps shipping on Scala 2, fixes dependencies before touching the language version, and leans on TASTy compatibility to avoid an all-or-nothing cutover. It's slower than a big-bang rewrite. It's also the version that doesn't put your release cadence or your team's bandwidth at risk.

Four-stage diagram showing how to migrate to Scala 3 without disruption: run a probe branch, fix dependencies first, bridge with TASTy, then flip the switch

A Non-Disruptive Migration Is a Different Project Than a Fast One

These are two separate goals, and conflating them is the most common way a migration project goes sideways. A fast migration optimizes for calendar time and accepts real risk along the way: a frozen release, an all-hands sprint, a temporary broken build. A non-disruptive migration optimizes for zero risk to what your team ships in the meantime, and accepts that the calendar will stretch to match.

For a startup with one branch, no legacy maintenance burden, and appetite for a clean cutover, the fast path can be the right call. For a team maintaining a production system with a real release cadence, customers depending on stability, and no tolerance for an extended broken state, the non-disruptive path is usually the only responsible option. The rest of this post is about how to run that version well.

Run a Parallel Probe Branch Before Touching Your Main Line

The core mechanic that makes a non-disruptive migration possible is simple: maintain a separate branch where the compiler is already switched to Scala 3, running in parallel with your actual release branches. Nothing in that probe branch ships. Its only job is to tell you what breaks.

What a Probe Branch Actually Catches

Every time you merge changes into your main line, you mirror them into the probe branch and see what the Scala 3 compiler flags. Some of what surfaces will be genuine incompatibilities that need a real fix. A lot of it will be smaller things, deprecated syntax, stricter type-checking, patterns Scala 3 no longer allows implicitly. Cataloging these as they appear, instead of discovering all of them at once during a single cutover attempt, is what keeps the scope of the eventual switch predictable.

Every Fix That Works in Scala 2 Ships Immediately

The discipline that makes this approach actually safe is simple to state and easy to skip under deadline pressure: never commit anything to a release branch that doesn't compile cleanly in Scala 2. Whenever a fix for a Scala 3 incompatibility can be expressed in code that still runs correctly on Scala 2, it goes straight into the next normal release. You're not waiting for the migration to finish to get the benefit of these fixes, and you're never carrying a branch that only works on one compiler. The migration becomes something your team is quietly doing in the background of ordinary release work, not a separate project competing for the same sprint.

Fix Dependencies Before You Touch the Compiler Flag

The single biggest scoping mistake in a Scala 3 migration is estimating the timeline before auditing dependencies. Language syntax changes are largely mechanical. Dependency problems are where real weeks and months disappear, and they come in two distinct flavors that need different handling.

Unmaintained Macro Libraries Are the Real Time Sink

Scala 3 replaced how Scala 2 macros work at a fundamental level, and Scala 2 macros simply don't run in Scala 3. Any library your codebase depends on that used macros internally either has a Scala 3 port already, or it doesn't. If it doesn't, and the maintainer isn't actively working on one, that's a hard stop, not a scheduling problem. No amount of engineering time on your side fixes an upstream library that hasn't been ported.

This is why an early, honest audit of every dependency using Scala macros matters more than any other planning step. Find out which ones are ported, which ones are in progress, and which ones have gone quiet, before your team commits to a timeline based on the parts of the migration that are actually within your control.

Replacing a Library Versus Waiting for Its Maintainer

When a dependency is stuck, you have two real options: replace it with a Scala 3 native alternative, or wait for the upstream port. Replacement is a real refactor, sometimes a large one if the library is used broadly across your codebase, but it's a refactor you control and can schedule. Waiting is lower effort in the short term but puts your timeline at the mercy of someone else's roadmap. Neither choice is automatically wrong. The decision should be made deliberately, library by library, rather than defaulted into because nobody looked closely enough to see it coming.

TASTy Compatibility Means You Don't Need a Big-Bang Cutover

Scala 3 ships with a compatibility mechanism called TASTy that lets a Scala 3 build consume libraries still compiled for Scala 2.13. In practice, this means you can switch your own codebase's compiler to Scala 3 while some of your dependencies are still on Scala 2, as long as those dependencies have published Scala 2.13 artifacts with TASTy support.

How Scala 2 Dependencies Can Run Inside a Scala 3 Build

This is the feature that turns "every dependency must be Scala 3 ready" from a hard prerequisite into a nice-to-have. You can complete the language switch on your own codebase, keep a handful of stubborn dependencies running on their last Scala 2.13 release through TASTy interop, and finish porting those dependencies on your own schedule afterward. For teams with one or two libraries stuck waiting on an upstream maintainer, this is often the difference between migrating this year and migrating whenever that maintainer gets around to it.

The Switch Itself Should Be the Least Eventful Part

If the probe branch work and dependency cleanup happened correctly, flipping the compiler flag on your main line should be close to anticlimactic. Most of what would have broken already got caught and fixed during the probe phase. The build goes green, the test suite passes, and the team notices mainly that nothing changed about how they work day to day.

That's the actual measure of success for this approach, not speed, but the absence of drama. A migration that ends in a quiet, unremarkable green build is a migration that was planned correctly from the start.

A Non-Disruptive Migration Takes Months, Not Weeks

Be honest about the trade-off going in. A non-disruptive migration on a real production codebase, run this way, is typically measured in months, not weeks, and for a codebase with significant macro-heavy dependencies or multiple maintained branches, it can stretch well beyond that. The cost is calendar time and the discipline to keep the probe branch current instead of letting it drift.

What you get in return is a migration with no forced downtime, no frozen release cycle, and no point where the team is blocked waiting on an architectural project that produces zero visible value to the business in the meantime. For most engineering organizations running a live product, that trade is worth making. The alternative, a fast cutover that goes wrong mid-flight, tends to cost far more than the extra months this approach asks for.

This is also where team composition matters. Dependency auditing and macro replacement work are specialized enough that they're often best handled by engineers who've done this specific kind of migration before, working alongside your core team rather than pulling your core team off product work entirely. It's also worth remembering why this matters beyond compliance with a newer language version: tools like Ox's direct-style concurrency model are only available once a codebase has actually made the switch. If your organization is weighing that staffing question, it's worth reading how it connects to the underlying framework and concurrency decisions a Scala 3 migration actually unlocks.

A non-disruptive migration is a real staffing decision, not just a technical one.

Auditing macro-dependent libraries, sequencing dependency fixes, and running a probe branch correctly takes specific experience most teams have only done once. If you want that experience running point while your core team stays on product work, talk to a Scala expert.

Frequently Asked Questions

What Is a Probe Branch in a Scala 3 Migration?

A probe branch is a separate, non-shipping branch where the compiler is switched to Scala 3 so a team can identify what breaks, while the actual release branches continue running on Scala 2. It lets a team catch incompatibilities early without putting any live release at risk.

Can You Use Scala 2 Libraries in a Scala 3 Project?

Yes, through a compatibility mechanism called TASTy, which lets a Scala 3 build consume libraries still published for Scala 2.13. This allows a codebase to complete its own migration to Scala 3 while a small number of dependencies finish their own Scala 3 ports separately.

Why Do Scala Macros Break During a Scala 3 Migration?

Scala 3 replaced the underlying macro system that Scala 2 used, so Scala 2 macros do not run in Scala 3 at all. Any library that relied on Scala 2 macros internally needs a real Scala 3 port, not just a version bump, before it can be used in a migrated codebase.

How Long Does a Non-Disruptive Scala 3 Migration Take?

It depends heavily on how many macro-dependent libraries a codebase relies on and how many branches need to stay in sync. A careful, non-disruptive migration on a real production system is typically measured in months rather than weeks, especially compared to a faster, higher-risk cutover approach.

Should You Fix Dependencies Before or After Switching to Scala 3?

Before. The most common scoping mistake is switching the compiler version before dependencies are sorted out. Getting dependencies Scala 3 ready first, while still running on Scala 2, keeps the eventual language switch mechanical and isolates problems as they come up instead of surfacing all of them at once.

Next
Next

Ox vs Kyo: Scala's Concurrency Crossroads