CTCaliberTek
Course: Scala 3 Developer Path: From First Program to a Tested Project · Lesson 6 of 8Log in to track your progress and earn a certificate when you finish the course.
Programming

Scala Pattern Matching and Algebraic Data Types

Published November 9, 2026·Updated November 9, 2026·10 min read·1 view

Edited by , Senior Data Analyst. How we make our content

Like SaveLog in to like, save, and track progress.

Introduction

Pattern matching is where Scala's design pays off. You describe a closed set of possibilities with an enum or a sealed trait, and then use match to handle each case. The compiler checks that you covered all of them, so adding a new case later points you to every place that must change. This combination, called algebraic data types, is a reliable way to model things like payments, commands, results, and parsed expressions.

Prerequisites

  • Case classes and traits from the previous lesson

Step-by-Step Guide

Step 1: Match on values, types, and structure

A match expression tries each case from the top and uses the first that fits. Patterns can be constants, variables that bind a value, alternatives joined with a vertical bar, and typed patterns that check the runtime type. A guard adds an if condition to a case for finer control. The underscore matches anything and is usually last as a catch-all. Because match is an expression, the whole thing produces a value you can assign or return.

def describe(x: Any): String = x match
  case 0 => "zero"
  case n: Int if n < 0 => "negative integer"
  case n: Int => s"integer $n"
  case s: String => s"text of length ${s.length}"
  case _ => "something else"

println(describe(-4))      // negative integer
println(describe("scala")) // text of length 5

Step 2: Define a closed set of cases with an enum

A Scala 3 enum lists all the allowed variants of a type. Each variant can carry its own data, and in that form the enum is an algebraic data type. A shape is either a circle with a radius or a rectangle with width and height, and nothing else. Because the set is closed, the compiler knows every possibility. Simple enums without data also work, and they come with helpful methods such as values and valueOf.

enum Shape:
  case Circle(radius: Double)
  case Rectangle(width: Double, height: Double)

enum Level:
  case Low, Medium, High

println(Level.values.toList)   // List(Low, Medium, High)

Step 3: Take data apart and get exhaustiveness checking

Matching on an enum or sealed type destructures each case, binding its fields to names. The compiler then checks that the match is exhaustive. If you later add a Triangle to Shape and forget to update this function, you get a compile-time warning pointing at the match instead of a runtime failure in production. That is a big benefit over a chain of instance checks. Avoid a catch-all case when matching on your own closed types, because it silences exactly this check.

import Shape.*

def area(shape: Shape): Double = shape match
  case Circle(r) => math.Pi * r * r
  case Rectangle(w, h) => w * h

println(area(Rectangle(3, 4)))   // 12.0
println(area(Circle(1)))         // 3.141592653589793

Step 4: Use sealed traits and case classes when you need more control

An enum covers most cases, but a sealed trait with case classes is the more flexible form and the one you will see in existing code. Sealed means all subtypes must be in the same file, so the compiler still knows the full set. Case objects are good for variants without data. This version can mix in other traits, extend other classes, and have richer members, which makes it a good fit for larger models such as an order lifecycle.

sealed trait OrderStatus
case object Pending extends OrderStatus
case class Shipped(trackingId: String) extends OrderStatus
case class Cancelled(reason: String) extends OrderStatus

def message(status: OrderStatus): String = status match
  case Pending => "Waiting to ship"
  case Shipped(id) => s"On its way, tracking $id"
  case Cancelled(reason) => s"Cancelled: $reason"

Step 5: Match on lists, tuples, and nested structures

Patterns can be nested to any depth. A list matches the pattern head :: tail, which splits off the first element, with Nil for the empty list, and this is the classic way to write a recursive function over a list. Tuples match by position. You can also match on the contents of a case class inside another structure, binding only the parts you need and ignoring the rest with underscores. The result is code that reads like a description of the shape of the data.

def sum(xs: List[Int]): Int = xs match
  case Nil => 0
  case head :: tail => head + sum(tail)

println(sum(List(1, 2, 3, 4)))   // 10

val point = (3, 0)
val where = point match
  case (0, 0) => "origin"
  case (_, 0) => "on the x axis"
  case (0, _) => "on the y axis"
  case _ => "elsewhere"

Step 6: Match on Option and use it in assignments

Option has two cases, Some with a value and None, so it is itself an algebraic data type, and matching on it is the most explicit way to handle a missing value. You can also destructure directly in a val definition or a for loop, which is handy for tuples and case classes. When a pattern in an assignment might not match, use a match expression or a for comprehension instead, so a mismatch does not become a runtime error.

def describeAge(age: Option[Int]): String = age match
  case Some(n) if n >= 18 => "adult"
  case Some(_) => "minor"
  case None => "unknown"

val (name, score) = ("Asha", 91)
case class Point(x: Int, y: Int)
val Point(px, py) = Point(2, 5)

FAQs

What does 'exhaustive' mean in pattern matching?
It means the match handles every possible case of the type. For enums and sealed types the compiler can check this and warns when a case is missing, which turns many would-be runtime errors into compile-time messages.
Should I choose enum or sealed trait?
Use an enum for a simple set of variants, which is the shortest form in Scala 3. Choose a sealed trait with case classes when you need the extra flexibility, or when you are reading or maintaining Scala 2 code, where enums with data are not available.
Is pattern matching the same as a switch statement?
It does the job of a switch, but goes much further: it can check types, destructure data, bind variables, apply guards, and nest, and it returns a value.

Summary

Enums and sealed traits describe a closed set of possibilities, and match takes them apart while the compiler checks you handled all of them. With guards, nested patterns, and list patterns you can write short, safe functions over rich data. The next lesson applies the same idea to the problem of missing values and failures, using Option, Either, and Try.

Related tutorials

Comments

Log in or create a free account to join the discussion.

  • No comments yet. Be the first to comment.