λ fbounded  :: Blog
All posts
#java #types #patterns

F-Bounded Polymorphism: Type-Safe Builders in Java

How a self-referential type bound solves a real inheritance problem, why Java's own Enum uses the same trick, and what it all means in practice.

The Problem

Fluent builders are everywhere in Java. The idea is simple: every setter returns this, so you can chain calls:

User user = new UserBuilder()
    .name("Alice")
    .email("alice@example.com")
    .build();

Now suppose you want to share common fields across multiple builders using inheritance. You have a base VehicleBuilder and a more specific CarBuilder:

class VehicleBuilder {
    String make;
    String model;

    VehicleBuilder make(String make)   { this.make  = make;  return this; }
    VehicleBuilder model(String model) { this.model = model; return this; }
}

class CarBuilder extends VehicleBuilder {
    int doors;

    CarBuilder doors(int d) { this.doors = d; return this; }
}

Looks reasonable. Now try using it:

Car car = new CarBuilder()
    .make("Toyota")   // returns VehicleBuilder — type is now lost
    .doors(4)         // ❌ compile error: VehicleBuilder has no doors()
    .build();

The moment you call .make(), the return type becomes VehicleBuilder. The compiler forgets it was a CarBuilder. You’ve lost your type.

You could cast everywhere, but then you’ve given up on compile-time safety entirely.


The Fix: F-Bounded Type Parameters

The root issue is that VehicleBuilder says “I return VehicleBuilder” — but what it should say is “I return whatever concrete type this actually is.”

That’s exactly what an F-bounded type parameter expresses:

abstract class VehicleBuilder<B extends VehicleBuilder<B>> {
    String make;
    String model;

    @SuppressWarnings("unchecked")
    public B make(String make)   { this.make  = make;  return (B) this; }

    @SuppressWarnings("unchecked")
    public B model(String model) { this.model = model; return (B) this; }
}

B extends VehicleBuilder<B> is the F-bound. Read it as: “B must be a subtype of VehicleBuilder<B> itself.” The type variable appears on both sides — B is bounded by a parameterized version of the class it’s declared in. That’s the self-referential part.

Now CarBuilder plugs itself in as B:

class CarBuilder extends VehicleBuilder<CarBuilder> {
    int doors;

    public CarBuilder doors(int d) { this.doors = d; return this; }

    public Car build() {
        return new Car(make, model, doors);
    }
}

And usage now works exactly as you’d want:

Car car = new CarBuilder()
    .make("Toyota")  // returns CarBuilder ✓
    .model("Corolla") // returns CarBuilder ✓
    .doors(4)         // returns CarBuilder ✓
    .build();         // returns Car ✓

Every method in the base class returns the concrete subtype. You don’t lose the chain, and you don’t need a single cast at the call site.


Scaling It Up

The pattern extends cleanly. Add a TruckBuilder and it just works:

class TruckBuilder extends VehicleBuilder<TruckBuilder> {
    int payloadKg;

    public TruckBuilder payload(int kg) { this.payloadKg = kg; return this; }

    public Truck build() {
        return new Truck(make, model, payloadKg);
    }
}

Truck truck = new TruckBuilder()
    .make("Volvo")
    .model("FH16")
    .payload(25000)
    .build();

VehicleBuilder never changes. Each subclass just declares itself as its own B.


Java’s Enum Already Does This

You don’t have to look far for a real-world example — Java’s own Enum is defined as:

public abstract class Enum<E extends Enum<E>> implements Comparable<E> {
    private final String name;
    private final int ordinal;

    public final int compareTo(E o) {
        return this.ordinal - o.ordinal;
    }
}

The bound E extends Enum<E> is the same pattern. When you write:

enum Planet { MERCURY, VENUS, EARTH, MARS }

Java desugars it into roughly:

final class Planet extends Enum<Planet> { ... }

Now compareTo has signature compareTo(Planet o) — not compareTo(Enum o). You can only compare a Planet to another Planet. Comparing Planet.EARTH to some Color.RED is a compile-time error:

Planet.EARTH.compareTo(Planet.MARS);  // ✓
Planet.EARTH.compareTo(Color.RED);    // ❌ compile error

Without F-bounds, Enum.compareTo would accept any Enum — and you’d only find the mistake at runtime. The self-referential bound turns that runtime error into a compile-time one.


The One Caveat

F-bounds interact awkwardly with deep inheritance. If ElectricCarBuilder extends CarBuilder and CarBuilder already fills in B = CarBuilder, then ElectricCarBuilder can’t re-specialize B for itself. You’d need another level of indirection.

In practice, one level of inheritance — base builder → concrete builder — covers the vast majority of real use cases, and that’s exactly where F-bounds shine.


Takeaway

F-bounded polymorphism isn’t an exotic type-theory concept — it’s a practical solution to a concrete Java problem: how do you write a base class whose methods return the type of the actual subclass, not the base class?

The pattern shows up in:

  • Fluent builder hierarchies (as shown above)
  • Java’s Enum<E extends Enum<E>> (in the standard library, used every day)
  • Comparable<T>, which bounds T so a type can only compare to itself

Once you see the shape <B extends Base<B>>, you’ll recognise it everywhere.