-
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
247: Practice Exercise: Modeling a Booking System with ADTs
I remember the first time I tried to build a booking system for a travel app a few years back. My instinct was to create one big Booking class with every possible field: flightNumber, roomNumber, seatAssignment, and a bunch of booleans like isCancelled or isConfirmed. It felt efficient at the time, but it quickly became a nightmare of if (booking.flightNumber != null) checks. I was essentially fighting the language to prevent invalid states.
The 'Everything Class' Trap
Let's start where I did. Imagine we want to track a booking that could be a Hotel or a Flight, and it can be either Pending, Confirmed, or Cancelled. Here is the "naive" way to do it:
case class Booking(
id: String,
hotelRoom: Option[Int],
flightNumber: Option[String],
status: String // "Pending", "Confirmed", "Cancelled"
)
Now, here is the problem. I can accidentally create a Booking that has both a hotelRoom and a flightNumber. Or, even worse, a booking that is "Cancelled" but still has a hotelRoom assigned. The compiler doesn't care; it just sees a bunch of Options and a String. I'm spending all my time writing validation logic to make sure the data makes sense, rather than actually building features.
Separating State from Identity
I realized I needed to stop treating the "status" as a piece of data and start treating it as a type. In Scala, this is where Algebraic Data Types (ADTs) come in. Instead of a String, let's use a sealed trait. I'm using sealed here because I want the compiler to yell at me if I forget to handle one of the statuses in a pattern match later.
sealed trait BookingStatus
case object Pending extends BookingStatus
case class Confirmed(confirmationCode: String) extends BookingStatus
case class Cancelled(reason: String) extends BookingStatus
Look at what happened here. Notice how Confirmed now carries the confirmationCode? In my previous version, that code would have been another nullable field in the main class. Now, it's physically impossible to have a confirmation code unless the booking is actually in the Confirmed state. That's a huge win for type safety.
Handling Diverse Booking Data
But we still have the Hotel vs. Flight issue. I can't just keep adding Options to the main class. I need the type of booking to dictate what data is available. I'll apply the same ADT pattern here:
sealed trait BookingDetails
case class HotelDetails(roomNumber: Int, hotelName: String) extends BookingDetails
case class FlightDetails(flightNumber: String, seat: String) extends BookingDetails
Now I can tie it all together. My Booking class becomes a lean container that holds the identity of the booking, the specific details of what was booked, and the current status.
case class Booking(
id: String,
details: BookingDetails,
status: BookingStatus
)
Testing the Exhaustiveness
The real magic happens when I actually use this in a function. Let's say I want to send a notification to the customer. Because I used sealed traits, the compiler acts as my safety net.
def sendNotification(booking: Booking): String = {
booking.status match {
case Pending => "Your booking is being processed."
case Confirmed(code) => s"Confirmed! Your code is $code."
// I'll intentionally leave out 'Cancelled' for a second
}
}
If I try to compile this, Scala will give me a warning: "match may not be exhaustive. It would fail on the following input: Cancelled(_) ". This is exactly what I want. The compiler is basically reminding me, "Hey, you forgot to handle the case where a user cancels their trip." I don't have to maintain a checklist of edge cases in my head; the type system does it for me.
By nesting these ADTs, I've moved the business rules from "runtime checks" (where they crash in production) to "compile-time constraints" (where they are fixed before the code ever runs).
📋 Practical Task
Exercise: Implementing a Multi-Tiered Loyalty Booking System
You are expanding the booking system to handle different loyalty tiers. Some bookings are "Standard," while others are "VIP" (which requires a membership ID) or "Corporate" (which requires a company tax ID).
Your Task:
- Create a sealed trait called
LoyaltyTier. - Implement three case classes/objects for
LoyaltyTier:Standard(no extra data),VIP(contains amembershipId: String), andCorporate(contains ataxId: String). - Update the
Bookingcase class from the lesson to include atier: LoyaltyTierfield. - Write a function
calculateDiscount(booking: Booking): Doublethat uses pattern matching on thetierto return a discount rate:- Standard: 0.0
- VIP: 0.15
- Corporate: 0.10
- Ensure your
calculateDiscountfunction is exhaustive so that the compiler is satisfied.
There are no comments for now.