A self type lets a trait say: I can only be mixed into something that is also an A. It is a requirement, not a relationship. The trait gets to call A's members on this, the compiler refuses any concrete class that forgets to provide A, and yet the trait does not become a subtype of A. That small distinction is what made self types the backbone of the once-famous cake pattern, and it is also why they confuse people who expect them to behave like extends.

This article explains self types from scratch: the syntax and its variants, exactly what the compiler checks, how they differ from inheritance in subtyping, visibility and cycles, compound requirements in Scala 2 and Scala 3, a self-recursive builder, the cake pattern with a hand-traced initialisation bug, and why most modern code prefers constructor parameters or givens. It closes with failure modes and a checklist.

Advertisement

The problem self types solve

Suppose you write a trait that adds audit logging to something that can persist entities. The logging trait needs to call save, but it is not itself a repository and should not claim to be one. With inheritance you would write trait Auditing extends Repository, and every Auditing value would then be usable anywhere a Repository is expected, which is a promise you did not mean to make. You also force a particular position in the linearisation and you cannot express two traits that need each other, because inheritance cannot be cyclic.

A self type expresses exactly the dependency: this trait needs those members to be present on the final object, and nothing more.

Syntax and variants

The self type annotation is the first thing in the trait body. The name before the arrow is an alias for this; it may be this itself or any identifier.

trait Repository {
  def save(id: String, data: String): Unit
}

trait Auditing {
  this: Repository =>                       // requirement: mixed into a Repository

  def saveAudited(id: String, data: String, who: String): Unit = {
    println(s"$who saving $id")
    save(id, data)                          // allowed: this is Auditing with Repository
  }
}

trait Tree { self =>                        // alias only, no type requirement
  class Node { def owner: Tree = self }     // refers to the outer Tree, not the Node
}

The third form, an alias with no type, adds no requirement at all. It exists to give the outer this a name you can use inside nested classes or anonymous instances, where this would refer to the inner object. You will see self => and outer => used this way in collection and actor code.

Advertisement

What the compiler checks

Two checks happen in two different places. Inside the trait, the type of this is the intersection of the trait and its self type, so members of the required type are visible and type-check. At the point where a concrete class or object is defined, the compiler checks that the class's own type conforms to the self types of every trait it mixes in. If it does not, compilation fails with an error saying that the class's self type does not conform to the trait's required self type.

class InMemoryRepo extends Repository {
  private val store = scala.collection.mutable.Map.empty[String, String]
  def save(id: String, data: String): Unit = store(id) = data
}

class AuditedRepo extends InMemoryRepo with Auditing      // compiles: requirement met

// class Broken extends Auditing                          // rejected: Broken is not a Repository

trait LaterMix extends Auditing                           // compiles: traits may defer
object Fine extends InMemoryRepo with LaterMix            // checked here, at the concrete type

The important subtlety is that an abstract trait extending Auditing is allowed to postpone the requirement; only a concrete instantiation must discharge it. In practice a trait that extends a self-typed trait usually restates the self type so its own body can see the required members too.

Self type versus inheritance

Inheritance: B extends Atrait Adef save()is-atrait B extends Acallers see save()class C extends BB is a subtype of A;A's members are public API of BSelf type: B { this: A => }trait Adef save()trait Brequires Aneedsclass Cextends B with AB is NOT a subtype of A.Inside B, this has type B with A.Outside, a B shows only B's members.Any concrete class must mix in A.
Left: inheritance makes B a subtype of A. Right: a self type makes A a requirement on whoever mixes B in, without making B an A.
Questiontrait B extends Atrait B { this: A => }
Is every B an A?Yes; B can be passed where A is expectedNo; only the concrete class is
Can B call A's members?YesYes, on this inside B
Can a caller holding a B call A's members?YesNo; the requirement is private to B's body
Who must provide A?B itself, by inheritanceWhatever concrete class mixes B in
Can A and B require each other?No; cyclic inheritance is illegalYes; mutual self types are allowed
Does A get initialised before B?Yes, it precedes B in linearisationNo guarantee; depends on mixin order

The third row matters for API design. Self types let you depend on a capability without leaking it to your callers, and that keeps a module's public surface small.

Mutual dependencies and compound requirements

Because a self type is a requirement rather than a parent, two traits may require each other. That is impossible with inheritance and is exactly what lets a set of components refer to one another while living in separate files.

trait UserComponent  { this: OrderComponent => def userOrders(u: String) = orders.byUser(u) }
trait OrderStore     { def byUser(u: String): List[String] }
trait OrderComponent { this: UserComponent  => def orders: OrderStore }

// Scala 2: several requirements joined with `with`
trait Reporting { this: UserComponent with OrderComponent => }

// Scala 3: intersection types use `&` (the `with` form is accepted but deprecated)
trait Reporting3 { this: UserComponent & OrderComponent => }

A compound self type behaves like an intersection: everything in every listed type is available on this, and the concrete class must conform to all of them. Scala 3 keeps self types with the same meaning; the main visible change is that intersections are written with &.

A self-recursive builder

Self types also solve a typing problem in fluent APIs. A base builder wants its methods to return the concrete builder type so calls can be chained without losing subclass methods. Declaring a type parameter for that type is not enough, because this is not known to be a Self; a self type closes the gap.

trait Builder[Self <: Builder[Self]] { this: Self =>
  protected var tags: List[String] = Nil
  def tag(t: String): Self = { tags = t :: tags; this }    // `this` is a Self here
}

final class QueryBuilder extends Builder[QueryBuilder] {
  private var limitN = 100
  def limit(n: Int): QueryBuilder = { limitN = n; this }
  def build: String = s"tags=${tags.reverse.mkString(",")} limit=$limitN"
}

new QueryBuilder().tag("a").limit(10).tag("b").build      // tag returns QueryBuilder

Without the this: Self => line, tag would not compile, because returning this where a Self is required is a type mismatch. With it, a class that wrongly writes extends Builder[SomethingElse] is rejected at its definition.

Worked example: the cake pattern

The cake pattern, popularised in the early 2010s, used self types for dependency injection without a framework. Each component is a trait holding an abstract dependency and a concrete service; the application object stacks the layers together.

trait UserRepoComponent {
  val userRepo: UserRepo
  trait UserRepo { def find(id: Long): Option[String] }
}

trait UserServiceComponent { this: UserRepoComponent =>
  val userService = new UserService                 // eager val: see next section
  class UserService { def greet(id: Long) = userRepo.find(id).map(n => s"Hello, $n") }
}

object App extends UserServiceComponent with UserRepoComponent {
  val userRepo = new UserRepo { def find(id: Long) = Some("Asha") }
}

App.userService.greet(1)                            // Some("Hello, Asha")

It works, and it is type-safe: forget to define userRepo in App and compilation fails. For tests, a different object mixes in a fake repository component. The appeal was compile-time wiring with no reflection and no container.

The initialisation trap, traced

Now change the service to capture the repository eagerly: class UserService(repo: UserRepo) and val userService = new UserService(userRepo). The program compiles and the service holds null. Here is why.

Trait and class bodies run in reverse linearisation order. The linearisation of App is App, UserRepoComponent, UserServiceComponent, AnyRef, Any, so the bodies run as: UserServiceComponent first, then UserRepoComponent, then App. When UserServiceComponent's body evaluates new UserService(userRepo), App's body has not yet assigned userRepo, so the field still holds its default, null. The first real call then throws a NullPointerException far from the cause.

The original version escaped only because UserService reads userRepo lazily, at call time. The fixes are to declare dependent members as lazy val or def, or to pass dependencies as constructor parameters (Scala 3 trait parameters are initialised before the trait body). Note that the self type gave no ordering guarantee at all: that is the price of not being inheritance. Scala 3's experimental safe-initialisation checker can flag some of these cases, but do not rely on it being enabled.

Why the cake pattern fell out of favour

Teams that adopted it widely reported the same problems. Every component's members end up in one giant object's namespace, so names collide. Error messages for a missing layer mention self type conformance rather than the missing dependency. Initialisation order bugs like the one above appear whenever someone uses a strict val. Compile times grow because everything is one large type. And replacing a single dependency in a test requires building a whole alternative cake.

The alternatives are simpler. Plain constructor injection passes dependencies as class parameters and wires them in a main method; it has no ordering surprises and is obvious to read. Givens in Scala 3, or implicits in Scala 2, pass context such as an execution context or a configuration automatically where explicit plumbing is noisy; see Scala 3 givens. Effect libraries offer their own dependency layers. Self types remain the right tool for small mixin requirements, not for wiring an application.

Good uses that remain

  • Mixins that only make sense on a specific base: a logging or stashing helper that requires being mixed into an actor or a service base class, without becoming one.
  • Hiding capabilities: a trait that needs persistence internally but should not expose save to its callers.
  • Mutually dependent modules split across files, where inheritance would be cyclic.
  • Self-recursive generic APIs like the builder above, where this must be typed as the concrete subtype.
  • Outer aliases (self =>) in nested classes and anonymous instances, purely for readability.

For the wider type-system context, including variance and higher-kinded types, see the Scala type system; for type members that depend on an enclosing instance, see path-dependent types.

Failure modes

SymptomCauseFix
Callers cannot see a method that the trait usesIt comes from the self type, which is not inheritedExtend the type if it is genuinely part of the API
NullPointerException at startupStrict val reads a dependency not yet initialisedUse lazy val or def; prefer constructor parameters
Error about self type conformance at a classConcrete class did not mix in a required traitAdd the missing trait to the class definition
Name clashes in a big cakeAll components share one object namespaceNest services in component traits or stop using cake
Builder method returns the base typeMissing this: Self =>Add the self type and bound the type parameter

What to do next

  1. Find traits in your code base that extend a type only to call its members, and decide whether a self type would express the dependency more honestly.
  2. Write the Auditing and Repository example, then try to instantiate Auditing alone and read the compiler's complaint.
  3. Reproduce the cake initialisation bug with a strict val, then fix it with lazy val and again with constructor parameters; compare readability.
  4. If you maintain a cake-pattern application, migrate one component to constructor injection and measure compile time and test setup size.
  5. Use a self-recursive builder for a fluent API that has subclasses, and add a test that chains subclass methods after base methods.
  6. On Scala 3, write compound self types with & and try the safe-initialisation checker on a module with mixins.
Key takeaway: A self type, written this: A => at the top of a trait, turns A into a requirement on any concrete class that mixes the trait in, without making the trait a subtype of A. The trait can use A's members, callers cannot, and traits can require each other, which inheritance forbids. That made self types the engine of the cake pattern, whose namespace sprawl and initialisation-order traps pushed most teams back to constructor injection and givens. Keep self types for small mixin requirements, hidden capabilities and self-recursive APIs.