A mixin is a trait used to add a capability to classes it does not define. Scala's version is unusually powerful: a class can mix in many traits, traits can carry state and concrete code, the order of mixing changes behaviour in defined ways, and a trait can be mixed into a single instance at the point where it is created. That power is why mixins appear everywhere in Scala libraries, from collection implementation traits to logging, test fixtures and resilience wrappers, and also why they produce some of the language's most confusing bugs.
The mechanics of traits themselves, including the linearization algorithm computed by hand and what traits compile to, are covered in Scala traits, in depth. This article is about using traits as mixins: how composition at definition time differs from composition at instantiation, the exact rules for combining members, how to stack behaviour safely with abstract override, what Scala 3 changed, and when delegation or type classes are the better tool. A worked example builds a resilient client from four mixins and shows how reordering them changes what it does.
What a mixin is
Single inheritance answers the question what is this thing? A mixin answers a different question: what else can it do? Suppose many unrelated classes need timestamps, equality by identifier or structured logging. Putting those in a common base class forces an artificial hierarchy; copying them into each class duplicates code. A mixin trait holds the capability once and lets each class opt in.
trait Timestamped:
private var _updated: Long = System.currentTimeMillis()
def touch(): Unit = _updated = System.currentTimeMillis()
def updatedAt: Long = _updated
trait HasId:
def id: String // abstract: the host class supplies it
override def equals(o: Any): Boolean = o match
case h: HasId => h.id == id
case _ => false
override def hashCode: Int = id.hashCode
class Order(val id: String, val total: BigDecimal) extends HasId with Timestamped
class Customer(val id: String, val name: String) extends HasId with TimestampedThree things make this a mixin rather than ordinary inheritance. Order has exactly one superclass, AnyRef, and two mixed-in traits. The traits contribute both state and code: each Order instance has its own _updated field, inlined into the object by the compiler. And HasId has an abstract member, id, that the host class fills in; the trait states what it needs and the class provides it. Unlike Java's default methods, the trait can hold fields and override equals from AnyRef.
A trait that is mixed in through many paths is still included once. If A and B both extend Timestamped and a class mixes in both, the object has one _updated field. Linearization places every trait exactly once, so Scala has no C++-style diamond with duplicated state.
Mixing in at definition or at instantiation
You can mix traits in where a class is declared, or where an instance is created.
// HttpBackend, Retrying and Metered are defined in the worked example below.
// Definition-time: every SafeClient has the behaviour.
class SafeClient(val metrics: Metrics) extends HttpBackend with Retrying with Metered
// Instantiation-time: this one object is metered; other HttpBackends are not.
val probe = new HttpBackend with Metered:
val metrics = probeMetricsInstantiation-time mixing looks dynamic, but it is not. The compiler creates an anonymous subclass at that point in the source, and the composition is fixed at compile time. You cannot take an existing object and add a trait to it at runtime; the language has no operation that does that. If you need runtime composition, for example behaviour chosen from configuration, you need delegation: wrap the object in another object that implements the same interface.
Instantiation-time mixing is useful in tests, where you want a real class with one extra capability, and for one-off configurations. In production code prefer named classes: an anonymous class has an unreadable name in stack traces, and every distinct new X with Y site is another class the JVM has to load.
How members combine
When a class and its traits define members with the same name, the compiler applies a small set of rules. Learning them precisely removes most mixin surprises.
| Situation | Result |
|---|---|
| Trait declares an abstract member; class or another trait defines it | Fine: the concrete definition implements it, whatever the order |
| Two traits both define the same concrete method and the class does not override it | Compile error: the class inherits conflicting members |
Same as above, and one trait's method is marked override and that trait extends the other | Fine: the later one in the linearization wins |
| Class overrides the conflicting method | Fine: it can call super[T].m to pick a specific parent's version |
Trait method calls super.m where m is abstract in the trait's own parent | Needs abstract override; resolved at mixing time |
Two traits define the same val differently | Same rules as methods; the winning initializer runs, and order of initialization still matters |
trait Jsonable { def render: String = "{}" }
trait Csvable { def render: String = "" }
// class Report extends Jsonable with Csvable // error: inherits conflicting members
class Report extends Jsonable with Csvable:
override def render: String = super[Jsonable].render // explicit choicesuper[T] can name only a direct parent of the class. It is the tool for ending a conflict, not for reaching arbitrarily up the hierarchy. Plain super inside a trait means something different: it refers to the next implementation in the final object's linearization, which is not known until the trait is mixed into a concrete class. That late binding is what makes stacking possible.
Stacking behaviour with abstract override
A stackable modification is a trait that wraps a method it does not implement. It extends an interface, overrides a method with abstract override, and calls super to reach whatever implementation sits below it. abstract override is legal only in traits, and the compiler accepts a mixin only if something earlier in the linearization supplies a concrete implementation; otherwise the class fails to compile, so a stack with no base is caught early.
The ordering rule to remember is that calls start at the rightmost trait and move left. In class C extends Base with A with B, a call to the wrapped method enters B first, which calls super into A, which calls super into Base. Rightmost is outermost. The full algorithm, including how shared ancestors are placed, is worked through in the traits article; for a flat stack of wrappers over one base, rightmost-outermost is all you need.
Worked example: a resilient client from four mixins
Here is a small client built entirely from mixins. Each trait does one thing and knows nothing about the others.
trait Http:
def get(url: String): String
class HttpBackend extends Http:
def get(url: String): String = Net.fetch(url) // may throw IOException
trait Caching extends Http:
private val cache = scala.collection.mutable.Map.empty[String, String]
abstract override def get(url: String): String =
cache.getOrElseUpdate(url, super.get(url))
trait Retrying extends Http:
def maxAttempts: Int = 3
abstract override def get(url: String): String =
def loop(n: Int): String =
try super.get(url)
catch case e: java.io.IOException if n < maxAttempts => loop(n + 1)
loop(1)
trait Metered extends Http:
def metrics: Metrics
abstract override def get(url: String): String =
val t0 = System.nanoTime()
try super.get(url)
finally metrics.record("http.get", System.nanoTime() - t0)
class Client(val metrics: Metrics)
extends HttpBackend with Caching with Retrying with MeteredTrace client.get(u) for a URL that is not cached and whose first network attempt fails. Metered starts the timer and calls super.get, which is Retrying. Retrying calls Caching, which misses and calls HttpBackend, which throws. The exception propagates out of Caching, so nothing is cached, and Retrying catches it and tries again. The second attempt succeeds, Caching stores the value, and Metered records one sample covering both attempts. A second call for the same URL is answered by Caching; Retrying never sees a failure and Metered records a fast sample.
Now reorder to extends HttpBackend with Retrying with Caching with Metered. Behaviour on a miss is almost the same, but retries now happen beneath the cache. Swap Metered leftward, to with Metered with Caching with Retrying, and the timer sits under both: it records one sample per network attempt, and cache hits are never measured at all, so your latency dashboard silently stops reflecting what users see. Neither version is wrong. The order is part of the design, and it belongs in a comment beside the declaration.
The pieces test in isolation, which is the strongest argument for this style. Mix Retrying over a fake backend that fails twice and check it is called three times:
class FlakyBackend extends Http:
var calls = 0
def get(url: String): String =
calls += 1
if calls < 3 then throw java.io.IOException("boom") else "ok"
val c = new FlakyBackend with Retrying
assert(c.get("/x") == "ok" && c.calls == 3)
What Scala 3 changed
Scala 3 kept the mixin model and tightened it in several places that matter for mixin design.
- Trait parameters. A trait can take constructor parameters, such as
trait Metered(prefix: String), so a mixin no longer needs an abstract member for its configuration. The rule that keeps this sound: only the class that first extends the trait passes the arguments; a trait extending a parameterized trait cannot pass them, and a subclass of a class that already passed them must not pass them again. - Intersection types.
Http & Meteredis a type in its own right, so a function can require exactly the capabilities it uses without a named class that combines them. In Scala 2 the equivalent was the compound typeHttp with Metered. - Transparent traits. Scala 3 already keeps
ProductandSerializableout of inferred types; marking your own marker mixintransparentdoes the same for it, soif cond then a else bdoes not infer an unhelpful type that mentions every shared mixin. - Export clauses.
export backend.*generates forwarders for a delegate's members, making composition by delegation nearly as concise as mixing in, without inheriting anything.
Mixins versus delegation and type classes
Mixins are one of three ways Scala composes behaviour, and choosing well matters more than mastering the rules.
| Approach | Composition fixed at | Best for | Weak at |
|---|---|---|---|
| Mixin traits | Compile time | Cross-cutting behaviour on a known set of classes; stackable wrappers | Runtime configuration; long stacks are hard to follow |
Delegation (export or wrapper) | Runtime | Decorators chosen from config; wrapping types you do not own | Boilerplate in Scala 2; identity changes |
| Type classes | Compile time, per type | Capabilities for types you do not control, such as JSON codecs | Mutable state; ordering of wrappers |
A rough guide: if the behaviour wraps a method call, as logging, retry and metrics do, and the set of wrappers is fixed at build time, a mixin stack is clear and cheap. If the behaviour is a capability of a type, such as encoding, ordering or numeric operations, use a type class, especially for types you cannot modify; tagless final builds a whole architecture on that idea. If composition depends on configuration, delegate. And if a trait needs to declare that it can only be mixed into something else, use a self type rather than inheritance.
Failure modes
- Initialization order nulls. Trait bodies run left to right along the linearization, after the superclass. A trait that reads a
valdefined by a trait or class initialized later seesnullor zero. Make such membersdeforlazy val, or pass them as Scala 3 trait parameters. - Silent order changes. Reordering
withclauses during a refactor compiles cleanly and changes behaviour, as the metering example showed. Test the composed class, not only the parts, and comment the intended order. - Forgotten super call. A stackable trait that forgets to call
superon one branch cuts off every layer beneath it, so caching or authentication silently stops working on that path. - Binary compatibility. Adding a concrete
valorvarto a published trait breaks classes compiled against the old version, because the class itself must hold the field and its accessors; other member changes can break too. Treat library traits meant for mixing as part of your binary API and check them with a compatibility tool. - Mixin soup. Classes mixing ten traits, each with abstract dependencies on the others, recreate the cake pattern's problems: hard to read, slow to compile and hard to test. Keep stacks short and each trait independent.
What to do next
- Find one cross-cutting concern duplicated across classes in your codebase and extract it into a single mixin with an abstract member for what it needs.
- Write the four-trait client above, then reorder the traits and write tests that pin the order you intend.
- Search for traits whose bodies read abstract
vals and change them todef,lazy valor trait parameters. - List every
new X with Yin production code and replace those not in tests with named classes. - For each stack, decide whether delegation or a type class would be clearer, using the table above.
- Read Scala traits and work through one linearization by hand for a class with a shared ancestor.