WalzoneInterview Prep
📞 Interviewing soon? Practice with a realistic AI mock phone interview — it calls you, then scores you. First 15 min FREE →

Scala · Expert · question 67 of 100

How can you use Scala’s type system to enforce domain constraints at compile-time?

📕 Buy this interview preparation book: 100 Scala questions & answers — PDF + EPUB for $5

Scala’s type system is powerful and flexible, allowing you to express complex data modeling constraints with type safety. By leveraging the type system, we can create types that enforce domain constraints at compile-time, which means that invalid programs will not even compile, preventing runtime errors and improving overall code robustness.

Here are a few techniques that can be used for enforcing domain constraints using Scala’s type system:

1. **Value classes**: You can use value classes to create a lightweight wrapper around a value. These wrapper classes help express domain-specific constraints without any runtime overhead.

// Only accepts valid email addresses
case class EmailAddress(val value: String) extends AnyVal {
  require(value.contains("@"), "A valid email address must contain '@'")
}

// Only accepts positive numbers
case class PositiveInt(val value: Int) extends AnyVal {
  require(value > 0, "A positive integer must be greater than zero")
}

2. **Phantom types**: Phantom types are types that don’t have any runtime representation. They can be used to create a type-level state machine that enforces a specific order of operations.

// Phantom types for enforcing connection lifecycle ordering
sealed trait ConnectionState
sealed trait Disconnected extends ConnectionState
sealed trait Connected extends ConnectionState

class Connection[S <: ConnectionState] private () {
  def connect()(implicit ev: S =:= Disconnected): Connection[Connected] = {
    println("Connected!")
    new Connection[Connected]
  }

  def disconnect()(implicit ev: S =:= Connected): Connection[Disconnected] = {
    println("Disconnected!")
    new Connection[Disconnected]
  }
}

object Connection {
  def create(): Connection[Disconnected] = new Connection[Disconnected]
}

3. **Refined types**: The third-party library ‘refined‘ provides a way to define type constraints in a more expressive way. You can create constrained types by refining the base type with predicates. The library checks if these predicates hold at compile-time, and if not, it will prevent the code from compiling.

First, add the library to your build.sbt:

libraryDependencies += "eu.timepit" %% "refined" % "0.9.26"

Next, you can use refined types by importing the library and defining your constraints:

import eu.timepit.refined._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.numeric._
import eu.timepit.refined.string._

type PositiveIntRefined = Int Refined Positive
type EmailAddressRefined = String Refined Email

val five: Either[String, PositiveIntRefined] = refineV[Positive](5)
val emailAddress: Either[String, EmailAddressRefined] = refineV[Email]("user@example.com")

To access the value, you can use pattern matching or other methods provided by the ‘Either‘ type.

In conclusion, Scala’s type system enables you to model domain constraints in ways that can help improve code safety and robustness. By using the techniques mentioned above, you can enforce some constraints at compile time, significantly reducing the risk of runtime errors.

Reading is step one. Saying it out loud is the interview. Our AI interviewer calls your phone and runs a realistic Scala interview — then scores it.
📞 Practice Scala — free 15 min
📕 Buy this interview preparation book: 100 Scala questions & answers — PDF + EPUB for $5

All 100 Scala questions · All topics