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.
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.
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 typeThe 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
| Question | trait B extends A | trait B { this: A => } |
|---|---|---|
| Is every B an A? | Yes; B can be passed where A is expected | No; only the concrete class is |
| Can B call A's members? | Yes | Yes, on this inside B |
| Can a caller holding a B call A's members? | Yes | No; the requirement is private to B's body |
| Who must provide A? | B itself, by inheritance | Whatever concrete class mixes B in |
| Can A and B require each other? | No; cyclic inheritance is illegal | Yes; mutual self types are allowed |
| Does A get initialised before B? | Yes, it precedes B in linearisation | No 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 QueryBuilderWithout 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
saveto its callers. - Mutually dependent modules split across files, where inheritance would be cyclic.
- Self-recursive generic APIs like the builder above, where
thismust 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
| Symptom | Cause | Fix |
|---|---|---|
| Callers cannot see a method that the trait uses | It comes from the self type, which is not inherited | Extend the type if it is genuinely part of the API |
NullPointerException at startup | Strict val reads a dependency not yet initialised | Use lazy val or def; prefer constructor parameters |
| Error about self type conformance at a class | Concrete class did not mix in a required trait | Add the missing trait to the class definition |
| Name clashes in a big cake | All components share one object namespace | Nest services in component traits or stop using cake |
| Builder method returns the base type | Missing this: Self => | Add the self type and bound the type parameter |
What to do next
- 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.
- Write the Auditing and Repository example, then try to instantiate Auditing alone and read the compiler's complaint.
- Reproduce the cake initialisation bug with a strict val, then fix it with lazy val and again with constructor parameters; compare readability.
- If you maintain a cake-pattern application, migrate one component to constructor injection and measure compile time and test setup size.
- Use a self-recursive builder for a fluent API that has subclasses, and add a test that chains subclass methods after base methods.
- On Scala 3, write compound self types with & and try the safe-initialisation checker on a module with mixins.