Traits, classes and object-oriented Scala

Compose behaviour with traits, control access with modifiers, use companions and apply, and extend types with given-based syntax.

Classes, traits and mixin composition

trait Greeter:
  def name: String                       // abstract member
  def greeting: String = s"Hello, $name"  // concrete, defined in terms of the abstract

trait Timestamped:
  def createdAt: Long
  def ageSeconds(now: Long): Long = (now - createdAt) / 1000

// a class extends one class or trait, then mixes in more with 'with'
class User(val name: String, val createdAt: Long) extends Greeter, Timestamped

// a trait can require another trait with a self type
trait Persisted:
  this: Timestamped =>                    // this trait needs Timestamped
  def id: String
  def summary: String = s"$id at ${createdAt}"

// linearisation: the last mixin wins for a def with the same signature
trait Loud extends Greeter:
  abstract override def greeting: String = super.greeting.toUpperCase

val u = new User("ada", 1000L) with Loud
// u.greeting == "HELLO, ADA"
ConstructOne or manyPurpose
classOne superclassA type you instantiate with new
traitManyA mixin of behaviour, may have parameters in Scala 3
objectA single instanceSingleton, entry point, companion
enumA fixed set of casesAlgebraic data types and simple enumerations
abstract classOneA base with constructor parameters
case classOneA value type with generated equality and copy

Traits compose linearly, from the superclass outward, so a call to super in a trait reaches the previous implementation in the chain. That is what makes abstract override useful for stackable decorators.

Objects, companions and visibility

final class Money private (val cents: Long, val currency: String):
  def +(other: Money): Money =
    require(other.currency == currency, s"currency mismatch: $currency vs ${other.currency}")
    new Money(cents + other.cents, currency)

  override def toString: String = f"$currency ${cents / 100.0}%.2f"

object Money:                                   // the companion object
  val Zero: Money = new Money(0, "EUR")

  def apply(cents: Long, currency: String): Money =
    require(cents >= 0, "amount must not be negative")
    new Money(cents, currency)

  def apply(amount: Double, currency: String): Money =
    apply(Math.round(amount * 100), currency)

  def unapply(m: Money): Option[(Long, String)] = Some((m.cents, m.currency))

val price = Money(1250, "EUR")                  // apply: no 'new' at the call site
val Money(cents, cur) = price                   // unapply: destructuring in a val
// Zero is a singleton; there is exactly one instance per JVM

// visibility
class Account:
  private var balance: Long = 0                 // class-private
  private[this] var nonce: Long = 0              // instance-private
  protected def onChange(): Unit = ()
  def deposit(n: Long): Unit = balance += n
  • A companion object and its class can access each other's private members, which is how a private constructor plus a validating apply enforces an invariant.
  • apply makes a value look like a function call: Money(100, "EUR"). unapply makes it destructurable in a pattern match.
  • object is lazily initialised on first access and is thread-safe by the JVM's class initialisation rules.
  • private[package] scopes access to an enclosing package, which is the usual way to hide an implementation from the rest of a library.

Extension members and the uniform access principle

case class Temperature(celsius: Double)

// a value and a method look identical at the call site
object Temperature:
  extension (t: Temperature)
    def fahrenheit: Double = t.celsius * 9 / 5 + 32
    def isFreezing: Boolean = t.celsius <= 0
    def +(delta: Double): Temperature = Temperature(t.celsius + delta)

import Temperature.*
val t = Temperature(20)
println(t.fahrenheit)              // 68.0, no parentheses: it reads like a field
println(t + 5.0)                   // extension operators work too

// extensions are resolved statically, by the imports in scope.
// They cannot override a real member and never take part in dynamic dispatch.
ModifierEffectCall site
def f: TComputes a valuex.f
val f: TComputed once at constructionx.f
lazy val f: TComputed on first access, thread-safex.f
var f: TMutable fieldx.f = v
extensionAdds a method to an existing typex.newMethod
givenProvides an implicit instanceResolved by type

The uniform access principle means a caller cannot tell a field from a computation. Use it deliberately: start with a def or a val and change it later without breaking callers, and use lazy val when the computation is expensive and may never be needed.

💡
Mixins and extensions both add behaviour, and the difference matters: a trait is inherited, so it takes part in dynamic dispatch and can be overridden; an extension is resolved statically from the imports in scope. Put behaviour that a subtype should be able to change in a trait, and convenience syntax in an extension.

FAQ

Trait or abstract class?
Prefer a trait: it composes, a class can mix in several, and Scala 3 traits can take parameters. Use an abstract class when you need a constructor with parameters and a single inheritance chain is acceptable.
When should I make a class final?
By default, unless you designed it for extension. Case classes are effectively final for equality purposes, and marking a class final lets the compiler inline and devirtualise calls.

Case classes and pattern matching Givens, implicits and type classes

Last refreshed 2026-09-18.