Scala 3 — Book

Domain Modeling


Scala supports both functional programming (FP) and object-oriented programming (OOP), as well as a fusion of the two paradigms. This section provides a quick overview of data modeling in OOP and FP.

OOP Domain Modeling

When writing code in an OOP style, your two main tools for data encapsulation are traits and classes.


Scala traits can be used as simple interfaces, but they can also contain abstract and concrete methods and fields, and they can have parameters, just like classes. They provide a great way for you to organize behaviors into small, modular units. Later, when you want to create concrete implementations of attributes and behaviors, classes and objects can extend traits, mixing in as many traits as needed to achieve the desired behavior.

As an example of how to use traits as interfaces, here are three traits that define well-organized and modular behaviors for animals like dogs and cats:

trait Speaker:
  def speak(): String  // has no body, so it’s abstract

trait TailWagger:
  def startTail(): Unit = println("tail is wagging")
  def stopTail(): Unit = println("tail is stopped")

trait Runner:
  def startRunning(): Unit = println("I’m running")
  def stopRunning(): Unit = println("Stopped running")

Given those traits, here’s a Dog class that extends all of those traits while providing a behavior for the abstract speak method:

class Dog(name: String) extends Speaker, TailWagger, Runner:
  def speak(): String = "Woof!"

Notice how the class extends the traits with the extends keyword.

Similarly, here’s a Cat class that implements those same traits while also overriding two of the concrete methods it inherits:

class Cat(name: String) extends Speaker, TailWagger, Runner:
  def speak(): String = "Meow"
  override def startRunning(): Unit = println("Yeah ... I don’t run")
  override def stopRunning(): Unit = println("No need to stop")

These examples show how those classes are used:

val d = Dog("Rover")
println(d.speak())      // prints "Woof!"

val c = Cat("Morris")
println(c.speak())      // "Meow"
c.startRunning()        // "Yeah ... I don’t run"
c.stopRunning()         // "No need to stop"

If that code makes sense—great, you’re comfortable with traits as interfaces. If not, don’t worry, they’re explained in more detail in the Domain Modeling chapter.


Scala classes are used in OOP-style programming. Here’s an example of a class that models a “person.” In OOP fields are typically mutable, so firstName and lastName are both declared as var parameters:

class Person(var firstName: String, var lastName: String):
  def printFullName() = println(s"$firstName $lastName")

val p = Person("John", "Stephens")
println(p.firstName)   // "John"
p.lastName = "Legend"
p.printFullName()      // "John Legend"

Notice that the class declaration creates a constructor:

// this code uses that constructor
val p = Person("John", "Stephens")

Constructors and other class-related topics are covered in the Domain Modeling chapter.

FP Domain Modeling

When writing code in an FP style, you’ll use these constructs:

  • Enums to define ADTs
  • Case classes
  • Traits


The enum construct is a great way to model algebraic data types (ADTs) in Scala 3. For instance, a pizza has three main attributes:

  • Crust size
  • Crust type
  • Toppings

These are concisely modeled with enums:

enum CrustSize:
  case Small, Medium, Large

enum CrustType:
  case Thin, Thick, Regular

enum Topping:
  case Cheese, Pepperoni, BlackOlives, GreenOlives, Onions

Once you have an enum you can use it in all of the ways you normally use a trait, class, or object:

import CrustSize.*
val currentCrustSize = Small

// enums in a `match` expression
currentCrustSize match
  case Small => println("Small crust size")
  case Medium => println("Medium crust size")
  case Large => println("Large crust size")

// enums in an `if` statement
if currentCrustSize == Small then println("Small crust size")

Here’s another example of how to create and use an ADT with Scala:

enum Nat:
  case Zero
  case Succ(pred: Nat)

Enums are covered in detail in the Domain Modeling section of this book, and in the Reference documentation.

Case classes

The Scala case class lets you model concepts with immutable data structures. A case class has all of the functionality of a class, and also has additional features baked in that make them useful for functional programming. When the compiler sees the case keyword in front of a class it has these effects and benefits:

  • Case class constructor parameters are public val fields by default, so the fields are immutable, and accessor methods are generated for each parameter.
  • An unapply method is generated, which lets you use case classes in more ways in match expressions.
  • A copy method is generated in the class. This provides a way to create updated copies of the object without changing the original object.
  • equals and hashCode methods are generated to implement structural equality.
  • A default toString method is generated, which is helpful for debugging.

You can manually add all of those methods to a class yourself, but since those features are so commonly used in functional programming, using a case class is much more convenient.

This code demonstrates several case class features:

// define a case class
case class Person(
  name: String,
  vocation: String

// create an instance of the case class
val p = Person("Reginald Kenneth Dwight", "Singer")

// a good default toString method
p                // : Person = Person(Reginald Kenneth Dwight,Singer)

// can access its fields, which are immutable           // "Reginald Kenneth Dwight" = "Joe"   // error: can’t reassign a val field

// when you need to make a change, use the `copy` method
// to “update as you copy”
val p2 = p.copy(name = "Elton John")
p2               // : Person = Person(Elton John,Singer)

See the Domain Modeling sections for many more details on case classes.

Contributors to this page: