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
98: Modeling Domains with Algebraic Data Types
I've spent a lot of time reviewing PRs from developers transitioning to Scala from Java or Python, and I consistently see one specific pattern: the "everything-bagel" class. They try to model a domain entity by creating a single class with a handful of optional fields and an enum to track the "type" of the object. It feels intuitive at first, but it's a trap that leads to a codebase full of if (type == CREDIT_CARD && cardNum.isDefined) checks.
The Trap: Using Optional Fields to Represent Different States
Imagine we are building a payment processing system. A common mistake is to model a PaymentMethod like this:
case class PaymentMethod(
methodType: String,
cardNumber: Option[String],
paypalEmail: Option[String],
bankAccount: Option[String]
)
On the surface, this looks flexible. But look closer: what's stopping me from creating a PaymentMethod where methodType is "PayPal" but the paypalEmail is None? Or worse, a payment method that has both a credit card number and a bank account? The compiler can't help you here. You've created a model where "illegal states" are perfectly representable. You'll end up spending half your time writing validation logic to ensure your data is actually consistent.
The Fix: Making Illegal States Unrepresentable with Sum Types
In Scala, we use Algebraic Data Types (ADTs) to solve this. An ADT is essentially a combination of Product Types (case classes) and Sum Types (sealed traits). Instead of one bloated class, we define a closed hierarchy where a value is exactly one of a few specific options.
Here is how I would actually model that payment system:
sealed trait PaymentMethod
case class CreditCard(number: String, expiry: String) extends PaymentMethod
case class PayPal(email: String) extends PaymentMethod
case class BankTransfer(accountNumber: String, routingNumber: String) extends PaymentMethod
Notice the sealed keyword. This is the secret sauce. It tells the compiler that all implementations of PaymentMethod are defined in this file. This transforms the trait into a "Sum Type"βthe payment method is either a CreditCard OR a PayPal account OR a BankTransfer. It cannot be a weird hybrid of the three, and it cannot be "none of the above."
The real power hits when you actually use this data. Because the trait is sealed, the Scala compiler can perform exhaustiveness checking during pattern matching:
def processPayment(method: PaymentMethod): String = method match {
case CreditCard(num, _) => s"Charging card $num"
case PayPal(email) => s"Redirecting to PayPal for $email"
// If I forget BankTransfer here, the compiler will give me a warning!
}
I love this because it moves the burden of correctness from my brain (and my unit tests) to the compiler. If I add a CryptoWallet case class to the trait six months from now, the compiler will instantly point me to every single match statement in the entire application that needs to be updated to handle crypto payments.
When you're modeling your domain, stop asking "What fields does this object have?" and start asking "What are the distinct shapes this data can take?" Once you shift that perspective, you'll find you can delete a massive amount of defensive null-checking and validation code.
π Practical Task
Implementing a Flight Booking State Machine
You are tasked with modeling the lifecycle of a flight booking. A booking isn't just a set of fields; it exists in different states, and each state carries different data.
Requirements:
- Create a sealed trait called
BookingState. - Implement three case classes that extend this trait:
Pending: Contains only theflightNumber(String).Confirmed: Contains theflightNumber(String) and aseatNumber(String).Cancelled: Contains theflightNumber(String) and areason(String).
- Write a function
getBookingSummary(state: BookingState): Stringthat uses pattern matching to return:- "Flight [num] is awaiting payment" for Pending.
- "Flight [num] is confirmed in seat [seat]" for Confirmed.
- "Flight [num] was cancelled due to [reason]" for Cancelled.
Ensure your code compiles and that the BookingState trait is properly sealed to enable exhaustiveness checking.
There are no comments for now.