A trait is Scala's unit of reusable behaviour. It can declare abstract members like a Java interface, carry concrete methods and fields like a class, and be mixed into a class alongside other traits. That combination makes traits the backbone of most Scala code, and it also creates the questions this page answers: which method wins when two traits define it, what super refers to, in what order trait bodies run, and what all of it costs on the JVM.
Examples use Scala 3 syntax, with notes where Scala 2 differs. Related pages cover neighbouring ideas: sealed traits and ADTs for closed hierarchies, self types for requirements without inheritance, and given and using for type classes, which often replace traits used as mixins.
What a trait can hold
A trait can declare abstract methods, values and types, and define concrete methods and fields. A class extends one class and any number of traits; in Scala 3 you separate parents with commas, in Scala 2 with with. Concrete members in a trait are inherited exactly as if the class had written them.
trait Shape:
def area: Double // abstract: implementers must define it
def name: String = getClass.getSimpleName // concrete, may be overridden
def describe: String = f"$name with area $area%.2f"
trait Scalable[S]:
def scale(k: Double): S
final case class Circle(r: Double) extends Shape, Scalable[Circle]:
def area = math.Pi * r * r
def scale(k: Double) = Circle(r * k)Traits can be generic, like Scalable[S], and they can declare abstract type members, which let an implementer choose a type without the caller writing a type parameter. Unlike classes, traits cannot be instantiated alone; new Shape {} creates an anonymous class that extends the trait. Variance, bounds and higher-kinded parameters work on traits as on classes and are covered in the Scala type system.
Trait parameters in Scala 3
Scala 2 traits cannot take constructor parameters, which pushed authors toward abstract vals and the initialization trap described later. Scala 3 allows parameters, with one rule that keeps them unambiguous: arguments are passed only by the class that first brings the trait into the hierarchy. A trait that extends a parameterised trait may not pass arguments to it.
trait Greeting(val prefix: String):
def greet(who: String) = s"$prefix, $who"
trait Polite extends Greeting: // may not pass arguments to Greeting
override def greet(who: String) = super.greet(who) + ", nice to meet you"
class English extends Greeting("Hello"), Polite // the class supplies them
class French extends Greeting("Bonjour"), PoliteThe rule exists because the trait could be mixed into a hierarchy more than once through different paths, and the compiler must find exactly one place where it is initialised. If a class extends Polite without also supplying Greeting's argument, compilation fails with a message saying the parameter is missing.
Linearization, computed by hand
Multiple inheritance raises the diamond question: if two parents override the same method, which one runs? Scala answers with linearization, which flattens the inheritance graph into a single list. Method lookup starts at the left end of the list, and each super call moves one step right.
The rule is short. The linearization of class C is C itself, followed by the linearizations of its parents taken from the rightmost parent to the leftmost, merged so that a type appearing more than once is kept only at its rightmost position. The program below implements the rule directly on the example in the diagram.
// Pseudocode for the linearization rule, written as Scala.
// L(C) = C followed by L(Tn) +> ... +> L(T1), where T1..Tn are the parents left to right
// and a +> b keeps b and drops from a anything that b already contains.
def merge(a: List[String], b: List[String]): List[String] = a.filterNot(b.contains) ++ b
def lin(c: String, parents: Map[String, List[String]]): List[String] =
c :: parents.getOrElse(c, Nil).reverse.map(lin(_, parents)).foldLeft(List.empty[String])(merge)
val parents = Map(
"Service" -> List("Timestamped", "Prefixed"),
"Timestamped" -> List("Logger"),
"Prefixed" -> List("Logger"),
"Logger" -> List("AnyRef"),
"AnyRef" -> List("Any"))
lin("Service", parents) // List(Service, Prefixed, Timestamped, Logger, AnyRef, Any)Two consequences follow. The rightmost trait in the extends clause is consulted first, so it wins conflicts. And a shared ancestor such as Logger appears once, after every trait that extends it, which is what lets each trait's super call reach the next trait instead of skipping straight to the base.
super is dynamic: stackable modifications
In a class, super.m is fixed when the class is compiled. In a trait, it is not: it means "the next implementation in the linearization of whatever class this trait ends up in". That lets a trait wrap behaviour it has never seen. The trait must mark such a method abstract override, which tells the compiler the trait needs a concrete implementation somewhere to its right.
import scala.collection.mutable.ArrayBuffer
abstract class IntQueue:
def get(): Int
def put(x: Int): Unit
class BasicIntQueue extends IntQueue:
private val buf = ArrayBuffer.empty[Int]
def get() = buf.remove(0)
def put(x: Int): Unit = buf += x
trait Incrementing extends IntQueue:
abstract override def put(x: Int): Unit = super.put(x + 1)
trait Filtering extends IntQueue:
abstract override def put(x: Int): Unit = if x >= 0 then super.put(x)
val q = new BasicIntQueue with Incrementing with Filtering
q.put(-1); q.put(0); q.put(1)
(q.get(), q.get()) // (1, 2): Filtering runs first and drops -1
val r = new BasicIntQueue with Filtering with Incrementing
r.put(-1); r.put(0); r.put(1)
(r.get(), r.get(), r.get()) // (0, 1, 2): -1 is incremented before the filter sees itEach trait adds one concern, and the order of mixing decides the order in which they apply. This is useful for decorators such as metrics, retries or validation around a core implementation. It is also easy to get wrong silently, since swapping two traits changes behaviour without a compile error, so test order-sensitive stacks explicitly. When you need a specific parent regardless of order, write super[Incrementing].put(x); the qualifier must name a direct parent.
Initialization order and the null trap
Trait bodies are initialisers, and they run in reverse linearization order: from Any toward the class, so every parent is initialised before its children. A trait that reads an abstract val in its body therefore reads it before the subclass has assigned it.
trait Banner:
val title: String
val line = "=" * title.length // runs before the subclass assigns title
class Report extends Banner:
val title = "Quarterly"
new Report // throws NullPointerException: title is still null inside Banner
// Fixes, best first:
trait Banner1(title: String) { val line = "=" * title.length } // trait parameter
trait Banner2 { def title: String; def line = "=" * title.length } // def, computed on use
trait Banner3 { val title: String; lazy val line = "=" * title.length }In the example title is null while Banner's body runs, so the program throws. With an Int field you would silently get 0 instead. The fixes, in rough order of preference: pass the value as a trait parameter in Scala 3; turn the dependent field into a def; or make it a lazy val, which works but adds a check on every access and can hide cycles. Scala 2's early-definition syntax was removed in Scala 3.
Scala 3 has an opt-in safe initialization checker that reports many of these at compile time. It is enabled with -Wsafe-init, which replaces the deprecated -Ysafe-init. Turn it on in new projects and fix what it reports; it catches cases that only fail on rarely constructed subclasses.
What traits compile to
Since Scala 2.12, a trait compiles to a JVM interface. Concrete methods become default methods, with static implementation methods alongside. The interface cannot hold instance fields, so for each val or var a trait declares, the compiler adds the field to every class that mixes the trait in and gives the interface abstract accessors. A static $init$ method holds the trait's body, and each class constructor calls the $init$ methods of its traits in initialisation order, which is reverse linearization.
Two practical results follow. Calls through a trait type are interface calls, which modern JVMs inline as well as class calls in most hot paths, so traits are not a performance concern by themselves. And Java code can implement a trait that has only abstract methods, or concrete methods it is happy to inherit as defaults, but a trait with fields or abstract override is awkward or impossible to implement from Java. If a library needs Java implementers, expose a trait with only abstract members or an abstract class.
Binary compatibility
Because the fields and the initialisation calls of a trait are compiled into every class that mixes it in, changing a published trait can break classes compiled against the old version, even when the source change looks harmless. Adding a field, changing a val to a def, adding an abstract member or reordering parents can all break downstream binaries in different ways. Rather than memorising rules, run MiMa, the Migration Manager, in the library build: the sbt plugin's mimaReportBinaryIssues task compares the new artifact against the last release and lists the incompatibilities. Make it a CI check on every published module.
Transparent traits
Case classes and case objects silently extend Product and Serializable, and Scala 2 let those markers leak into inferred types. Given sealed trait Kind with case objects Fruit and Veg, the expression Set(Fruit, Veg) was inferred as Set[Kind with Product with Serializable] rather than Set[Kind], and that type then spread through signatures and error messages. Scala 3 lets a trait or class be declared transparent, which tells the type inferencer to drop it from inferred types when another type remains. Product, Serializable, Comparable and a few root types are treated as transparent automatically. Mark your own marker traits transparent when they exist only to tag implementations.
Trait or abstract class
| Question | Trait | Abstract class |
|---|---|---|
| Mix into several hierarchies | yes, any number | no, single superclass |
| Constructor parameters | Scala 3 only, with the first-extender rule | yes, in all versions |
| Implementable from Java | only if members are abstract or defaults suffice | yes |
| Stackable super calls | yes, with abstract override | no |
| Binary evolution | fragile: fields compile into implementers | more forgiving for some changes |
A good default is a trait for capabilities that unrelated types share, and an abstract class for the root of a family with shared state and constructor logic, especially when Java must extend it. For closed families of data, use a sealed trait or a Scala 3 enum.
When not to use a trait
Traits are inheritance, and inheritance couples a type to its parents' state and initialisation order. Three alternatives are often better. Type classes, declared as a trait but supplied as given instances, add behaviour to types you do not own without changing their hierarchy. Plain composition, holding a dependency as a constructor parameter, keeps lifecycles explicit; Scala 3's export clauses forward selected members from a held object with no boilerplate. And functions, passed as parameters, often replace a one-method trait entirely.
Failure modes
- Null or zero from an abstract val read in a trait body. Use parameters, defs or the safe-init checker.
- Order-dependent stacks that change behaviour when someone reorders the extends clause. Test the composed behaviour, not each trait alone.
- Deep mixin towers where finding which implementation runs means computing a twelve-element linearization. Flatten them or switch to composition.
- Accidental override when two traits define a concrete member with the same name; Scala forces an explicit override in the class, but the choice is often made carelessly.
- Broken downstream binaries after a small trait change in a library. Run MiMa in CI.
What to do next
- Pick one class in your codebase with three or more parents and write out its linearization by hand; check it against the overriding behaviour you expect.
- Turn on
-Wsafe-initin a Scala 3 module and fix every warning it reports. - Replace abstract vals read in trait bodies with trait parameters or defs.
- Audit stackable traits and add a test per supported mixing order.
- Mark pure marker traits
transparent. - Add MiMa to every published library and fail the build on unexpected binary issues.
- Where a trait exists only to add behaviour to types you do not own, rewrite it as a type class.