0Pricing
Scala for Backend Engineering & Functional Programming · Lesson

Modeling with ADTs

Make illegal states unrepresentable.

Modeling with ADTs is a free Scala for Backend Engineering & Functional Programming lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Scala for Backend Engineering & Functional Programming learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Algebraic Data Types

Algebraic data types (ADTs) are the foundation of functional domain modeling. They combine product types (AND) and sum types (OR) to describe data precisely.

  • Product: a record with several fields.
  • Sum: a choice among several variants.
case class Point(x: Int, y: Int) // product type

object Main:
  def main(args: Array[String]): Unit =
    println(Point(1, 2))

Product Types

A product type bundles values together. In Scala a case class is a product: an instance holds all its fields at once.

case class User(name: String, age: Int)

object Main:
  def main(args: Array[String]): Unit =
    val u = User("Ada", 36)
    println(u.name)
    println(u.age)

Sum Types

A sum type is a value that is exactly one of several variants. Scala 3 enums express this directly.

enum PaymentMethod:
  case Cash
  case Card(number: String)
  case Crypto(wallet: String)

object Main:
  def main(args: Array[String]): Unit =
    val p: PaymentMethod = PaymentMethod.Card("1234")
    println(p)

Make Illegal States Unrepresentable

The core principle: design types so that invalid data cannot even be constructed. If the type system forbids a bad state, you never need a runtime check for it.

enum Connection:
  case Disconnected
  case Connected(sessionId: String)

object Main:
  def main(args: Array[String]): Unit =
    // No way to have a sessionId while Disconnected
    val c: Connection = Connection.Connected("abc")
    println(c)

Bad Design vs Good Design

A flat record with nullable fields invites illegal states. Modeling each case as a variant removes them.

  • Bad: case class Conn(connected: Boolean, sessionId: String) allows connected=false with a sessionId.
  • Good: a sum type ties the field to the right state.
enum Door:
  case Open
  case Closed
  case Locked(key: String)

object Main:
  def main(args: Array[String]): Unit =
    val d: Door = Door.Locked("k1")
    println(d)

Combining Products and Sums

Real models nest products inside sums and vice versa. Each variant can carry its own product of fields.

case class Address(city: String, zip: String)

enum Contact:
  case Email(value: String)
  case Postal(address: Address)

object Main:
  def main(args: Array[String]): Unit =
    val c: Contact = Contact.Postal(Address("Paris", "75001"))
    println(c)

Pattern Matching ADTs

You consume ADTs with pattern matching. Because the type is closed, the compiler warns if you miss a variant.

enum Shape:
  case Circle(r: Double)
  case Rect(w: Double, h: Double)

object Main:
  def area(s: Shape): Double = s match
    case Shape.Circle(r)  => 3.14159 * r * r
    case Shape.Rect(w, h) => w * h

  def main(args: Array[String]): Unit =
    println(area(Shape.Rect(2, 3)))

Recursive ADTs

ADTs can refer to themselves, modeling trees, lists, and expressions naturally.

enum Expr:
  case Num(value: Int)
  case Add(left: Expr, right: Expr)

object Main:
  def eval(e: Expr): Int = e match
    case Expr.Num(v)    => v
    case Expr.Add(l, r) => eval(l) + eval(r)

  def main(args: Array[String]): Unit =
    val e = Expr.Add(Expr.Num(2), Expr.Num(3))
    println(eval(e))

Optionality with Option

Use Option instead of null to model a possibly-absent field. The type makes the absence explicit and forces handling.

case class Profile(name: String, nickname: Option[String])

object Main:
  def main(args: Array[String]): Unit =
    val p = Profile("Grace", None)
    println(p.nickname.getOrElse("(none)"))

Modeling Quantities Precisely

Replace primitive obsession. Instead of raw Ints and Strings, wrap meaningful quantities so the types document intent and prevent mixups.

case class Quantity(value: Int)
case class Price(cents: Long)
case class LineItem(qty: Quantity, price: Price)

object Main:
  def main(args: Array[String]): Unit =
    val item = LineItem(Quantity(3), Price(500))
    println(item)

Why ADTs Matter

ADTs give you correctness and clarity.

  • Invalid states cannot be built.
  • Exhaustive matching catches missed cases at compile time.
  • The shape of the data documents the domain.
enum OrderStatus:
  case Pending
  case Shipped(tracking: String)
  case Delivered(at: Long)

object Main:
  def main(args: Array[String]): Unit =
    val s: OrderStatus = OrderStatus.Shipped("TRK1")
    println(s)

Quick Check

Test your understanding of ADT-based modeling.

Recap

You learned domain modeling with ADTs.

  • Product types (case class) combine fields with AND.
  • Sum types (enum) offer a choice with OR.
  • Nest them to model rich domains and recursive structures.
  • Use Option instead of null.
  • Design so illegal states cannot be represented.
enum Event:
  case Created(id: Int)
  case Deleted(id: Int, reason: String)

object Main:
  def describe(e: Event): String = e match
    case Event.Created(id)         => s"created $id"
    case Event.Deleted(id, reason) => s"deleted $id: $reason"

  def main(args: Array[String]): Unit =
    println(describe(Event.Deleted(1, "spam")))

Frequently asked questions

Is the “Modeling with ADTs” lesson free?

Yes — the full text of “Modeling with ADTs” is free to read here on the web, and the Scala for Backend Engineering & Functional Programming course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Scala for Backend Engineering & Functional Programming course, upgrade to CoddyKit PRO.

What will I learn in “Modeling with ADTs”?

Make illegal states unrepresentable. You practise Scala for Backend Engineering & Functional Programming with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Scala for Backend Engineering & Functional Programming?

No prior experience is required. Scala for Backend Engineering & Functional Programming on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Modeling with ADTs” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Scala for Backend Engineering & Functional Programming lesson?

Yes. Every Scala for Backend Engineering & Functional Programming lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Modeling with ADTs
  2. Smart Constructors
  3. Newtypes
  4. Composing Domains
← Back to Scala for Backend Engineering & Functional Programming