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 boundsTso a type can only compare to itself
Once you see the shape <B extends Base<B>>, you’ll recognise it everywhere.