Case Classes Explained
A case class is Scala's concise way to define a class optimized for holding immutable data: prefixing a class with case, as in case class Point(x: Int, y: Int), automatically generates a companion object with an apply factory (no new needed), structural equals/hashCode, a readable toString, an unapply extractor for pattern matching, and a copy method -- all from one line of code. Every constructor parameter of a case class is a val by default, making instances immutable unless you explicitly mark a parameter var.
Cricket analogy: Declaring case class Delivery(bowler: String, speed: Double, result: String) is like the ball-by-ball commentary system auto-generating a complete record for every delivery Jasprit Bumrah bowls -- speed, result, and a readable summary are all produced automatically without a commentator manually typing each field.
Structural Equality and toString
Because case classes generate equals and hashCode based on their field values rather than object identity, two separately constructed case class instances with the same field values are considered equal with ==, unlike ordinary classes where == defaults to reference equality (are they the same object in memory). The auto-generated toString similarly prints the class name and all field values, for example Point(3,4), which makes debugging and logging dramatically easier than the default MyClass@1a2b3c hash-based output of a plain class.
Cricket analogy: Two Point(3,4) case class instances comparing equal is like two scorecards for the same over -- even though they're written on separate pieces of paper by two scorers, if every ball's runs match, the scorecards are considered the same record, not judged by which physical paper they're on.
case class Point(x: Int, y: Int)
val p1 = Point(3, 4)
val p2 = Point(3, 4)
println(p1 == p2) // true, structural equality
println(p1) // Point(3,4)
val p3 = p1.copy(x = 10) // Point(10,4)
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
def area(s: Shape): Double = s match {
case Circle(r) => math.Pi * r * r
case Rectangle(w, h) => w * h
}The copy Method and Immutability
Because case class fields default to val, the idiomatic way to produce a modified version of an instance is copy, which creates a new instance with all fields identical to the original except the ones you explicitly override by name, e.g., original.copy(salary = 60000). This pattern avoids mutation entirely -- the original instance is untouched -- which makes case classes especially safe to share across threads and easy to reason about, since nothing can silently change a case class value out from under you once it's created.
Cricket analogy: Using .copy(runs = 50) on a Score case class is like a scorer producing a fresh scorecard for a new innings that reuses the same team names and venue but updates just the runs -- the original first-innings card is never overwritten.
Sealed Traits with Case Classes (Algebraic Data Types)
Case classes are most powerful when combined with a sealed trait to model algebraic data types: a sealed trait Shape with case classes Circle, Rectangle, and Triangle extending it defines a closed set of possible shapes that the compiler knows about completely, because sealed forbids extending the trait from outside the file. This lets the compiler warn you if a match expression on a Shape fails to handle every case, catching an entire class of bugs at compile time that would otherwise only surface as a runtime MatchError.
Cricket analogy: A sealed trait DismissalType with cases Bowled, Caught, RunOut, LBW is like the ICC maintaining a closed, official list of how a batter can get out -- a scorer's match report using match on dismissal type gets flagged by the umpire's checklist if a new, unlisted dismissal type sneaks in.
Case classes are the natural fit for domain modeling with pattern matching -- combining sealed trait and case class hierarchies is Scala's version of algebraic data types found in Haskell or F#.
A case class cannot extend another case class in Scala -- inheritance between case classes is disallowed because it would break the auto-generated equals/hashCode contract. Extend a plain trait or abstract class instead, and make the leaves case classes.
- case class auto-generates apply, unapply, structural equals/hashCode, toString, and copy -- all constructor parameters default to val.
- Structural equality means two case class instances with identical field values are == regardless of whether they're the same object in memory.
- The auto-generated toString prints a readable ClassName(field1,field2,...) instead of a hash-based default.
- copy produces a new instance with selected fields overridden, leaving the original instance completely untouched.
- Combining sealed trait with case class subtypes models a closed algebraic data type the compiler can exhaustiveness-check.
- A case class cannot extend another case class, to preserve the correctness of generated equality logic.
- Case classes pair naturally with match expressions via their auto-generated unapply extractor.
Practice what you learned
1. What does declaring case class Point(x: Int, y: Int) automatically generate?
2. How does == behave for two case class instances with identical field values?
3. What does calling .copy(x = 10) on a case class instance do?
4. Why combine sealed trait with case classes?
5. Which of these is disallowed in Scala?
Was this page helpful?
You May Also Like
Classes and Objects in Scala
Learn how Scala classes bundle state and behavior, how primary and auxiliary constructors work, and how singleton objects and companion objects replace Java's static members.
Pattern Matching in Depth
Go beyond basic match expressions to guards, type patterns, list deconstruction, custom extractors, and compiler-checked exhaustiveness over sealed hierarchies.
Traits in Scala
Learn how Scala traits provide composable, disciplined multiple inheritance, how linearization resolves overriding conflicts, and when to reach for a trait versus an abstract class.
Option and Null Safety
Learn how Scala's Option[T] replaces null with a compiler-checked container type, covering map/flatMap/getOrElse, for-comprehensions, and safe Java interop.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics