skip to content

How do you write a generic builder with a recursive self-type so it works correctly across an inheritance hierarchy?

level: seniorimportance: should knowfreq 34%

answer

  1. abstract class Builder<T extends Builder<T>> (CRTP / F-bounded)
  2. Fluent methods return T, not Builder
  3. abstract T self(); subclass returns this
  4. self() avoids the unchecked cast of (T) this
  5. Covariant build() narrows return type per subclass

basics

~20 s

You make the base builder generic in its own subtype: abstract class Builder<T extends Builder<T>>, have each fluent method return that type T via an abstract self() method overridden by subclasses to return this. This lets a subclass keep chaining the parent's methods without losing its own type.

solid answer

~50 s

When a class hierarchy has builders, a naive base builder breaks chaining: after calling a parent method you get back the parent builder type and can no longer call subclass methods. The fix is the recursive generic self-type, sometimes called the 'curiously recurring template pattern'. Declare the base builder as abstract class Builder<T extends Builder<T>>, make every fluent method return T, and add an abstract T self() that each concrete subclass overrides to return this. The parent's methods then return T — the actual subclass builder type — so chaining never loses the subtype, in any order. Each subclass builder declares itself as SubBuilder extends Builder<SubBuilder>. You also typically make the base build() abstract with a covariant return type so each subclass returns its own product type. The cost is genericity and the slightly awkward self() indirection plus an unavoidable unchecked-cast or self() override, but it's the standard way to make inheritable builders type-safe, and Effective Java presents it as the canonical approach.

go deeper

for a junior

Generally not expected; at most recognizes that builders can be reused via inheritance.

for a middle

Understands the problem (inherited fluent methods returning the base type breaks chaining) even if they can't yet write the full generic solution.

for a senior

Implements abstract class Builder<T extends Builder<T>> with self() and covariant build(), and explains why each element is required.

for a principal

Explains F-bounded polymorphism / CRTP precisely, weighs the readability cost against the benefit, knows its limitations (a subclass can pass a wrong T), and decides when the simpler non-generic builder is the right call.

## The problem inheritance creates Suppose `Pizza` is abstract with subclasses `NyPizza` and `Calzone`, and each has a builder. The base `Pizza.Builder` has shared methods like `addTopping(...)`, and `NyPizza.Builder` adds `size(...)`. Now consider: ```java NyPizza p = new NyPizza.Builder() .size(LARGE) // returns NyPizza.Builder .addTopping(HAM) // PROBLEM: if addTopping returns Pizza.Builder, we lost the NY type .build(); ``` If the inherited `addTopping` returns the *base* `Pizza.Builder`, the chain's static type degrades to the base builder after that call, so you can no longer call `size(...)` afterward, and `build()` would have to return the base `Pizza`, not `NyPizza`. Chaining inherited and subclass methods in arbitrary order becomes impossible without casts. The order-dependence is the real pain. ## The recursive self-type fix We want every inherited fluent method to return **the concrete subclass builder type**, whatever it is. We express 'the concrete subclass type' with a recursive type parameter: ```java public abstract class Pizza { final Set<Topping> toppings; abstract static class Builder<T extends Builder<T>> { // T = the eventual concrete builder EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class); public T addTopping(Topping t) { // returns T, not Builder toppings.add(t); return self(); // not `return this` — see below } abstract Pizza build(); // covariant: subclasses narrow the return type // Subclasses must override to return `this`. This simulates a 'self type'. protected abstract T self(); } Pizza(Builder<?> b) { toppings = b.toppings.clone(); } } ``` A concrete subclass plugs itself in as `T`: ```java public class NyPizza extends Pizza { public static class Builder extends Pizza.Builder<Builder> { // T = NyPizza.Builder private final Size size; public Builder(Size size) { this.size = size; } @Override public NyPizza build() { return new NyPizza(this); } // covariant return @Override protected Builder self() { return this; } // resolves T to this } private final Size size; NyPizza(Builder b) { super(b); this.size = b.size; } } ``` Now: ```java NyPizza p = new NyPizza.Builder(SMALL) .addTopping(SAUSAGE) // inherited, returns NyPizza.Builder .addTopping(ONION) .build(); // returns NyPizza ``` ## Why each piece is needed - **`T extends Builder<T>`** is the *recursive* (self-referential) bound: it forces `T` to be a builder type that's parameterized on itself. This is the Java analogue of a real 'self type', which the language lacks. It's also known as the **curiously recurring generic pattern (CRTP / F-bounded polymorphism)**. - **Methods return `T`**, so an inherited method's static return type is the *actual* subclass builder — chaining survives across the hierarchy in any order. - **`self()`** exists because `return this` inside the base class is typed as `Builder<T>`, not `T`. You can't safely cast `this` to `T` in the base without an unchecked warning, so the clean solution is an abstract `self()` that each subclass overrides to `return this` (where `this` *is* `T`). It's a tiny amount of boilerplate that buys type safety with no unchecked cast. - **Covariant `build()`**: Java allows an overriding method to narrow its return type, so `NyPizza.Builder.build()` returns `NyPizza` while the abstract base returns `Pizza`. Clients building an `NyPizza.Builder` get an `NyPizza` with no cast. ## Trade-offs and pitfalls - **Complexity:** the generics are intimidating and the `self()` indirection is non-obvious to readers. Reserve this for genuine builder hierarchies; for a single class, a plain non-generic builder is simpler. - **A subclass could 'lie'** by passing the wrong type as `T` (`class Bad extends Builder<NyPizza.Builder>`), so the pattern relies on convention; it's type-correct but you trust authors to pass their own type. - **Abstract base can't be instantiated** — the base builder is abstract precisely because only concrete subclasses can provide `self()` and `build()`. - **Alternative:** if you don't need inheritance, skip all of this. The recursive self-type is *only* about making fluent chaining type-safe across an inheritance hierarchy. ## Where you see it This is the structure *Effective Java* recommends for builders in class hierarchies, and the same self-type trick appears throughout fluent library APIs (e.g. many builder hierarchies in frameworks) wherever a fluent base type must return the concrete subtype.

  • Why use an abstract self() method instead of just writing `return this` in the base class's fluent methods?
    In the base class `this` is typed as Builder<T>, not T, so returning it would lose the subclass type or require an unchecked (T) cast; an abstract self() overridden in each subclass returns `this` where it genuinely is T, keeping the chain type-safe with no unchecked warning.
  • What does the covariant return type add on top of the self-type?
    It lets each subclass's build() return its own concrete product type (NyPizza instead of Pizza), so callers get the specific object with no cast.

saying these in an interview costs you the question

  • Returning the base builder type from inherited methods, which breaks chaining after the first inherited call.
  • Casting (T) this in the base class and ignoring the unchecked warning instead of using an abstract self().
  • Thinking covariant return types require generics — they don't, but they pair naturally with this pattern.
  • Using the recursive self-type for a single, non-inherited builder where a plain builder would do.

context