Skip to Content
Course content

222: Incremental Migration Strategies from Java to Scala

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're renovating a large, old house while you're still living in it. You can't just bulldoze the place and start over—you'd have nowhere to sleep. Instead, you tackle it room by room. You might start with the bathroom because it's small and self-contained. Once that's modern and functional, you move to the kitchen. For a while, you'll have a state-of-the-art smart kitchen sitting right next to a living room with 1970s wallpaper. It looks a bit weird, but the house remains habitable, and you're making progress without crashing your life into the ground.

Migrating a Java codebase to Scala is exactly like that. The "house" is your JVM project. The "rooms" are your packages or modules. Because Scala and Java both compile to JVM bytecode, they can live side-by-side in the same project, calling each other's methods as if they were written in the same language. If you try a "Big Bang" rewrite—stopping all feature work for six months to rewrite everything in Scala—you'll likely fail. The business won't tolerate the freeze, and you'll lose the implicit knowledge hidden in the legacy Java bugs.

Taming the Hybrid Build

The first thing you'll realize is that your build tool needs to handle two compilers. If you're using Maven or Gradle, you're essentially adding a second "phase" to your compilation. I usually recommend starting with a hybrid build where Scala files are compiled first, and then Java files are compiled, referencing the Scala classes. This is because Scala can see Java, and Java can see Scala, but the order of operations matters for the compiler to resolve symbols correctly.

Don't overthink the build too much at the start. Just get the Scala library on the classpath and prove you can call a single Scala function from your main Java entry point. That's your "first renovated tile."

Hunting for the Leaf Nodes

When deciding what to convert first, don't start with the core business orchestrator—the "heart" of the app. That's too risky. Instead, look for the "leaf nodes": those utility classes, POJOs, and data transfer objects that don't depend on much else.

Take a typical Java bean, for example. You've probably got a 100-line `User` class with getters, setters, `equals()`, `hashCode()`, and `toString()`. In Scala, that entire file collapses into a single line: case class User(id: Long, name: String, email: String). By converting these data holders first, you immediately reduce the codebase size and introduce your team to Scala's conciseness without risking a production outage in the critical path.

Bridging the Null Gap

Here is where you'll hit your first real friction point: null. Java loves nulls; Scala hates them. When you call a Java method from Scala, it returns a type that could be null. If you treat it as a non-nullable Scala type and it arrives as null, you'll get a NullPointerException, which defeats the purpose of using Scala in the first place.

I always tell my mentees to create a "border patrol" layer. The moment a value comes from the Java side into the Scala side, wrap it in an Option. Use Option(javaObject.getValue()). This forces the rest of your Scala logic to explicitly handle the "nothing" case using map, flatMap, or getOrElse, effectively insulating your new Scala code from Java's legacy null-culture.

The Strangler Fig Strategy

Eventually, you'll want to move logic. I use the "Strangler Fig" pattern: wrap an existing Java service with a Scala interface. Initially, the Scala interface just delegates everything to the Java implementation. Then, you slowly move one method at a time from the Java class into the Scala class. To the rest of the system, nothing has changed—the interface is the same—but underneath, the Java implementation is being "strangled" and replaced by Scala.

It's a slow process, sure. But it's the only way to migrate a production system while you're still shipping features. You aren't guessing if the new version works; you're replacing it piece by piece and verifying it along the way.




📋 Practical Task

Exercise: Converting the Legacy Inventory Filter to a Scala Collection Pipeline

You have a legacy Java class InventoryManager.java that contains a method filterExpiredProducts(List<Product> products). This method uses a traditional for loop, an if statement, and a manually managed ArrayList to filter out products where product.getExpiryDate() is before the current date.

Your Task:

  • Create a Scala companion object InventoryUtils.
  • Implement a method filterExpired(products: List[Product]): List[Product] that replaces the Java loop with a functional Scala pipeline (using filter).
  • Ensure the method can accept the Java java.util.List (you may need to use scala.jdk.CollectionConverters to convert the Java list to a Scala list).
  • Modify the original Java InventoryManager.java to call your new Scala InventoryUtils.filterExpired() method instead of its own internal loop.

Goal: Demonstrate a successful "Leaf Node" migration where a piece of logic is moved to Scala while the Java entry point remains unchanged.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.