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"| Construct | One or many | Purpose |
|---|---|---|
class | One superclass | A type you instantiate with new |
trait | Many | A mixin of behaviour, may have parameters in Scala 3 |
object | A single instance | Singleton, entry point, companion |
enum | A fixed set of cases | Algebraic data types and simple enumerations |
abstract class | One | A base with constructor parameters |
case class | One | A 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
applyenforces an invariant. applymakes a value look like a function call:Money(100, "EUR").unapplymakes it destructurable in a pattern match.objectis 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.| Modifier | Effect | Call site |
|---|---|---|
def f: T | Computes a value | x.f |
val f: T | Computed once at construction | x.f |
lazy val f: T | Computed on first access, thread-safe | x.f |
var f: T | Mutable field | x.f = v |
extension | Adds a method to an existing type | x.newMethod |
given | Provides an implicit instance | Resolved 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.Related
Case classes and pattern matching Givens, implicits and type classes
Last refreshed 2026-09-18.