Scala
Completed
-
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
235: Scala 2 vs Scala 3 Migration Considerations
I've seen a lot of teams approach the Scala 3 migration like they're painting a house—just slapping a new coat of paint over the existing walls and hoping the cracks underneath disappear. If you're moving a production codebase from 2.13 to 3, the most tempting path is the "naive" one: update your scalaVersion, run the compiler, and spend the next three days fixing red squiggles until it finally builds. While that gets you to a successful build, it often leaves you with a codebase that is technically Scala 3 but logically stuck in 2017.
The "Fix as You Go" Trap
The naive approach usually involves keeping all your implicit keywords exactly where they are. Since Scala 3 maintains a level of backward compatibility for implicit, your code will likely compile. But here is the problem: implicit in Scala 2 was a "Swiss Army knife" that did three different things: it defined implicit parameters, implicit values, and implicit conversions. This ambiguity is exactly what the Scala 3 team tried to kill by introducing given, using, and extension methods.
When you just "fix the errors," you miss the opportunity to decouple your logic. I once worked on a project where the team kept their old implicit conversions for a UserContext object. Because they didn't migrate to given, the compiler was still performing "magic" type coercions in the background that made the code nearly impossible to trace during debugging. You'd see a method call that didn't seem to match any signature, only to realize some obscure implicit conversion was firing five files away.
Replacing Hidden Magic with Explicit Givens
Let's look at a concrete example. Suppose you have a system that handles currency conversion. In the Scala 2 way, you might have written a "naive" implicit conversion to make your API feel "slick."
// The Scala 2 / Naive Scala 3 approach
implicit def intToMoney(amount: Int): Money = Money(amount, "USD")
def processPayment(m: Money) = println(s"Processing ${m.amount}")
// This works, but it's "magic" and dangerous
processPayment(100)
In Scala 3, we want to be honest about what is happening. The better way is to separate the definition of the value from the extension of the type. We use given for the instance and extension for the functionality. If you really need that conversion, you should use a given conversion, but I'd argue you should probably just use an extension method or an explicit constructor instead.
// The Scala 3 Professional approach
case class Money(amount: Int, currency: String)
extension (i: Int)
def toMoney: Money = Money(i, "USD")
def processPayment(m: Money) = println(s"Processing ${m.amount}")
// Now the intent is explicit. No more guessing why an Int became Money.
processPayment(100.toMoney)
The trade-off here is a tiny bit more typing for a massive gain in maintainability. When a new engineer joins your team, they don't have to hunt through the codebase to find out why processPayment(100) isn't throwing a type error; the .toMoney call tells them exactly what's happening.
Managing the Binary Compatibility Gap
One thing that often bites people during migration is the assumption that they can just mix-and-match libraries. Scala 2 and Scala 3 are not binary compatible. You cannot use a library compiled for 2.13 directly in a Scala 3 project unless that library was specifically published as a "cross-build" (usually denoted by _2.13 and _3 suffixes).
If you hit a wall with a library that hasn't been updated, don't try to force it. The "naive" move is to try and find some obscure compiler flag to make it work. The professional move is to create a "shim" module—a small Scala 2.13 project that wraps the legacy library and exposes a simplified API, which you then consume via a bridge or, more commonly, prioritize the migration of that specific dependency to a Scala 3 alternative. It sounds like more work upfront, but it prevents your dependency graph from becoming a house of cards.
📋 Practical Task
Refactoring a Legacy Session Context to Scala 3 Givens
You have been handed a legacy Scala 2.13 module that uses an implicit parameter to pass a UserSession through various service layers. The current code is "naive" because it relies on the ambiguous implicit keyword for both the definition and the usage.
Your Task: Refactor the following code to use Scala 3's given and using syntax. Remove the ambiguity of the implicit keyword to make the context propagation explicit.
// LEGACY CODE
case class UserSession(userId: String, token: String)
object SessionProvider {
implicit val currentSession: UserSession = UserSession("user-123", "abc-789")
}
def fetchUserData(userId: String)(implicit session: UserSession) = {
println(s"Fetching data for $userId using token ${session.token}")
}
// Usage
import SessionProvider._
fetchUserData("user-123")
Requirements:
- Replace
implicit valin the provider withgiven. - Replace
(implicit session: UserSession)in the function signature with theusingclause. - Ensure the code still compiles and executes the
printlnstatement.
There are no comments for now.