-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Scala
-
Section 4: Functional Scala
-
Section 5: Collections in Depth
-
Section 6: Type System
-
Section 7: Concurrency and Ecosystem
-
Section 8: Practical Projects
-
Section 9: Interview Practice
-
Section 10: Data Structures and Algorithms in Scala
-
Section 11: More Practice Exercises
-
Section 12: Advanced Functional Patterns
-
Section 13: More Ecosystem
-
Section 14: Scala Collections Library Deep Dive
-
Section 15: Scala Standard Library Deep Dive
-
Section 16: Akka Ecosystem Deep Dive
-
Section 17: Cats and Cats Effect Deep Dive
-
Section 18: Play Framework Deep Dive
-
Section 19: Apache Spark with Scala Deep Dive
-
Section 20: Scala Build Tools Deep Dive
-
Section 21: Scala 3 Specific Features
-
90: Union and Intersection Types
-
Section 22: Scala Testing Deep Dive
-
Section 23: Functional Domain Modeling
-
Section 24: More Data Structures and Algorithms in Scala
-
Section 25: Scala for Data Engineering
-
Section 26: More Practical Projects
-
Section 27: More Interview and Review
-
Section 28: ZIO Ecosystem Deep Dive
-
Section 29: Scala for Machine Learning
-
Section 30: Scala Microservices Architecture
-
Section 31: Scala Type System Deep Dive
-
Section 32: More Practice and Drills
-
Section 33: Scala Performance Deep Dive
-
Section 34: Scala Ecosystem Tooling
-
Section 35: Scala for Reactive Systems
-
Section 36: More Real-World Case Studies
-
Section 37: Scala for Financial Systems
-
Section 38: Scala GraphQL and gRPC
-
Section 39: More Final Projects
-
Section 40: More Interview and Final Review
-
Section 41: Scala for Streaming Data
-
Section 42: Scala Security Practices
-
Section 43: More Language Deep Dive
-
Section 44: Scala Command-Line Tools
-
Section 45: Scala Documentation and Style
-
Section 46: Scala Dependency Management
-
Section 47: More Practical Backend Patterns
-
Section 48: Scala for Event-Driven Architecture
-
Section 49: More Practice Drills Round 2
-
Section 50: Scala Compiler Deep Dive
-
Section 51: Scala for Web Frontends
-
Section 52: More Data Engineering Practice
-
Section 53: Scala Observability
-
Section 54: More Advanced Practice Projects
-
Section 55: Scala for Legacy Java Integration
-
Section 56: More Testing Practice
-
Section 57: Final Mastery Review
-
Section 58: Scala History and Ecosystem Context
-
Section 59: More Concurrency Patterns
-
Section 60: Scala for Configuration Management
-
Section 61: More Domain Modeling Practice
222: Incremental Migration Strategies from Java to Scala
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 (usingfilter). - Ensure the method can accept the Java
java.util.List(you may need to usescala.jdk.CollectionConvertersto convert the Java list to a Scala list). - Modify the original Java
InventoryManager.javato call your new ScalaInventoryUtils.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.
There are no comments for now.