Technology  /  Scala

๐Ÿ”ด Scala 21 guides ยท updated 2026

The JVM's hybrid OOP/functional language โ€” immutability, pattern matching, and the type system that powers Spark and Akka, from first principles to production.

Traits in Scala: Mixins and Multiple Inheritance Done Safely

Java famously forbids extending more than one class, specifically because unrestricted multiple inheritance creates the โ€œdiamond problemโ€ โ€” if two parent classes both define a method with the same signature, which implementation wins? Scalaโ€™s answer isnโ€™t to avoid the problem by forbidding multiple inheritance entirely (as Java originally did) โ€” itโ€™s to define a precise, deterministic rule for resolving conflicts, called linearization, and let you mix in as many traits as you want.


Traits as Interfaces โ€” The Simple Case

trait Greetable {
def name: String
def greet(): String = s"Hello, $name!"
}
class Person(val name: String) extends Greetable
val alice = new Person("Alice")
println(alice.greet()) // "Hello, Alice!"

At their simplest, traits look like Java interfaces with default methods โ€” name is abstract (declared, no implementation), greet() is concrete (fully implemented, shared by every mixing-in class). The difference from a Java interface starts becoming visible once you use more than one trait at once.


Multiple Traits โ€” The Feature Java Deliberately Doesnโ€™t Have

trait Swimmer {
def swim(): String = "Swimming"
}
trait Flyer {
def fly(): String = "Flying"
}
class Duck extends Swimmer with Flyer {
def describe(): String = s"${swim()} and ${fly()}"
}
val duck = new Duck()
println(duck.describe()) // "Swimming and Flying"

extends Swimmer with Flyer mixes in both traits โ€” Duck gets both swim() and fly() without needing either trait to know about the other, and without Javaโ€™s restriction to a single parent. This is the practical value of traits over classes for shared behavior: a Duck genuinely is both a swimmer and a flyer, and modeling that with single-parent inheritance would force an awkward choice about which capability becomes the โ€œrealโ€ parent class and which becomes a bolted-on interface.

trait Swimmer

(swim: concrete)

class Duck

extends Swimmer with Flyer

trait Flyer

(fly: concrete)

Duck has BOTH swim() and fly(),

with zero coupling between the two traits


What Happens When Traits Conflict

trait A {
def greet(): String = "Hello from A"
}
trait B {
def greet(): String = "Hello from B"
}
class C extends A with B {
override def greet(): String = s"${super.greet()} (and C says hi too)"
}
val c = new C()
println(c.greet()) // "Hello from B (and C says hi too)" โ€” B, not A!

Both A and B define greet() with the same signature โ€” this is exactly the diamond problem Java sidesteps by forbidding the situation outright. Scala resolves it deterministically using linearization: the order traits are mixed in matters, and super.greet() inside C resolves to whichever trait comes last in the extends ... with ... chain โ€” here, B, because itโ€™s mixed in after A.

class D extends B with A { // swapped order
override def greet(): String = s"${super.greet()} (and D says hi too)"
}
val d = new D()
println(d.greet()) // "Hello from A (and D says hi too)" โ€” now A wins, because order was reversed

This is the single most important rule to internalize about trait conflicts: the rightmost trait in the mixin chain takes precedence when super is called, and reversing the order of with clauses genuinely changes runtime behavior. This is deterministic and specified precisely by the language (not an implementation quirk that could change between compiler versions), but itโ€™s still easy to get backwards the first several times you encounter it.


Linearization โ€” The Full Rule

trait Base {
def message: String = "Base"
}
trait Logger extends Base {
override def message: String = s"Logger -> ${super.message}"
}
trait Validator extends Base {
override def message: String = s"Validator -> ${super.message}"
}
class Service extends Logger with Validator
val s = new Service()
println(s.message)
// "Validator -> Logger -> Base" โ€” read right to left through the "with" chain, each super calling the next

Scala computes a single, linear order of all traits and classes in the hierarchy (this is the โ€œlinearizationโ€), and super calls walk through that order one step at a time โ€” not toward each traitโ€™s own literal parent, but toward the next trait in the linearized chain, which depends on the full mixin order at the point of instantiation, not just the traitโ€™s own declaration. This is genuinely one of the more subtle parts of Scalaโ€™s type system, and the mental model that helps: read the linearization order right-to-left through the extends ... with ... list, and super inside any given trait always means โ€œwhatever comes next in that specific order,โ€ not โ€œmy own textual parent.โ€


Traits With Abstract and Concrete Members Together

trait Repository[T] {
def findById(id: String): Option[T] // abstract
def findByIdOrThrow(id: String): T = findById(id).getOrElse( // concrete, built on the abstract member
throw new NoSuchElementException(s"No entity with id $id")
)
}
class UserRepository extends Repository[String] {
private val users = Map("1" -> "Alice", "2" -> "Bob")
def findById(id: String): Option[String] = users.get(id)
}
val repo = new UserRepository()
repo.findByIdOrThrow("1") // "Alice"
repo.findByIdOrThrow("99") // throws NoSuchElementException

This is the pattern traits are genuinely built for: findByIdOrThrow is written once, in terms of the abstract findById every implementer must supply โ€” any class mixing in Repository[T] gets that convenience method for free, correctly, without duplicating its logic. This mirrors the abstract-class pattern from the previous guide, but with the added flexibility of being mixable alongside other traits, which a single abstract class parent cannot offer.


Traits vs Abstract Classes vs Java Interfaces

TraitAbstract classJava interface (with default methods)
Can hold constructor parametersNo (Scala 3 traits can have limited parameters, but itโ€™s uncommon)YesNo
Multiple per classYes โ€” mix in as many as neededNo โ€” single inheritance onlyYes โ€” a class can implement many
Can hold concrete state (fields with values)YesYesNo โ€” interfaces canโ€™t hold instance state
Linearization / diamond resolutionYes โ€” deterministic, order-dependentN/A โ€” no multiple inheritance to resolveRequires explicit override if two interfaces conflict

When to choose a trait: the default choice for shared, mixable behavior in Scala โ€” this covers the overwhelming majority of โ€œwhat would be an interface in Javaโ€ cases, plus cases needing actual shared implementation and even instance state, which Java interfaces canโ€™t provide at all.

When to choose an abstract class: when you specifically need constructor parameters taking real initialization logic, or when modeling a hierarchy that is genuinely single-inheritance by nature and you want that constraint enforced (a Shape hierarchy where every subtype truly is-a Shape and nothing else, for instance).


Self-Types โ€” Declaring a Traitโ€™s Dependency Without Extending

trait UserService {
def getUser(id: String): String
}
trait Auditable {
self: UserService => // "self-type": this trait can only be mixed into something that IS a UserService
def auditLog(id: String): String = s"Audit: accessed user ${getUser(id)}"
}
class RealUserService extends UserService with Auditable {
def getUser(id: String): String = s"User-$id"
}

A self-type (self: UserService =>) declares โ€œthis trait requires whatever itโ€™s mixed into to also be a UserServiceโ€ without Auditable itself extending UserService โ€” this lets Auditable call getUser (defined on the required type) while remaining independently mixable into anything satisfying that requirement, a more flexible dependency-declaration mechanism than inheritance alone would allow. This pattern shows up frequently in the โ€œcake patternโ€ for dependency injection in older Scala codebases, though Scala 3โ€™s given/using (covered in its own guide later in this series) has become the more common modern approach to the same underlying problem.


Sealed Traits โ€” A Preview

sealed trait PaymentMethod
case class CreditCard(number: String) extends PaymentMethod
case class PayPal(email: String) extends PaymentMethod
case object Cash extends PaymentMethod

sealed restricts a traitโ€™s implementations to only those defined in the same file โ€” this is what enables the compiler to verify pattern-match exhaustiveness (covered fully in the pattern matching guide): since every possible subtype is known at compile time, the compiler can warn if a match expression doesnโ€™t handle one of them. This combination โ€” sealed trait plus case class/case object subtypes โ€” is one of the most common and most powerful patterns in idiomatic Scala, previewed here because it depends on both traits and case classes (covered next) working together.


Stackable Modifications โ€” Composing Behavior in Layers

trait Logging {
def process(input: String): String = input
}
trait Timestamped extends Logging {
override def process(input: String): String =
s"[${java.time.Instant.now()}] ${super.process(input)}"
}
trait Uppercased extends Logging {
override def process(input: String): String =
super.process(input).toUpperCase
}
class Pipeline extends Logging with Timestamped with Uppercased
val p = new Pipeline()
println(p.process("hello world"))
// "[2026-...] HELLO WORLD" โ€” both modifications applied, in linearization order

This โ€œstackable traitโ€ pattern uses super calls that chain through the linearization order to layer independent modifications onto a base behavior โ€” each trait adds its own transformation and delegates to super for the rest, without any trait needing to know about the others. Itโ€™s a genuinely common real-world use of linearization: logging, timing, and formatting concerns layered onto a base operation, each as an independently reusable trait that can be mixed in (or left out) per use case, in whatever order produces the desired layering.

Frequently Asked Questions

Which trait wins when two mixed-in traits define the same method? The one that appears last (rightmost) in the extends ... with ... chain, per Scalaโ€™s linearization rules โ€” reversing the order of with clauses changes which implementation super resolves to.

Can a trait have a constructor like a class does? Not in the same way โ€” traits in Scala 2 cannot take constructor parameters at all (only abstract/concrete members). Scala 3 relaxed this somewhat, allowing parameterized traits in limited cases, but the far more common and portable pattern remains defining any needed values as abstract members that implementers must supply, rather than constructor parameters.

Is a trait basically the same as a Java interface with default methods? Close, but traits are meaningfully more powerful โ€” they can hold real instance state (fields with actual values, not just method declarations), participate in deterministic multiple-inheritance resolution via linearization, and use self-types to declare dependencies. Java interfaces with default methods cover a subset of what traits do.

When should I reach for a self-type instead of just extending another trait? When you want to declare โ€œthis trait requires some other capability to functionโ€ without actually establishing an is-a inheritance relationship โ€” useful for keeping traits independently composable while still expressing a real dependency between them.

What does sealed actually restrict, precisely? It limits every direct subtype/implementation of the sealed trait to being defined in the same source file. This is specifically what allows the compiler to know the complete, closed set of possible subtypes and check pattern-match exhaustiveness against that known set โ€” covered in full in the pattern matching guide.

Does mixing in many traits hurt runtime performance? Not meaningfully โ€” trait method resolution happens at compile time via linearization, producing ordinary virtual method calls on the JVM, the same mechanism backing regular class method dispatch. The cost of traits is entirely a design-complexity one (tracking linearization order across a deep mixin hierarchy), not a runtime one.


Whatโ€™s Next

Traits are Scalaโ€™s core mechanism for composing behavior across multiple sources safely, with linearization resolving conflicts deterministically rather than forbidding the situation outright. Next: Case Classes in Scala, which combine everything from this guide and the previous one โ€” classes, traits, and pattern matching โ€” into Scalaโ€™s standard idiom for modeling immutable data.