Making objects: factory methods and builders
A constructor is the right way to make most objects. This lesson is about the cases where it stops being enough, and the two patterns that take over.
Static factory methods
A constructor has one name — the class name — and that is the whole problem.
public record Plan(String name, long pricePaise, boolean jain) {
public static Plan veg(long pricePaise) {
return new Plan("Veg", pricePaise, false);
}
public static Plan jain(long pricePaise) {
return new Plan("Jain", pricePaise, true);
}
}
Plan a = Plan.veg(8_000);
Plan b = Plan.jain(9_000);
Four things a static factory gives you that a constructor cannot:
It has a name. Plan.jain(9000) versus new Plan("Jain", 9000, true) —
the second needs you to know what the boolean means. A boolean parameter is
almost always a missed factory method.
It need not return a new object. Integer.valueOf(5) returns a cached
instance. List.of() returns the same empty list every time. A constructor is
obliged to allocate.
It can return a subtype. List.of(a, b) gives you some implementation of
List and does not say which — so the JDK can change it without breaking you.
EnumSet.of returns a completely different class for enums with more than 64
constants, and nobody notices.
Two factories can take the same parameters. You cannot have two constructors
both taking a single long, but you can have ofPaise(long) and
ofRupees(long).
The naming conventions are worth learning because the JDK is consistent about them:
| Name | Means |
|---|---|
of |
a short, obvious construction — List.of |
valueOf |
a type conversion — Integer.valueOf |
from |
a conversion from one specific type — Date.from(instant) |
getInstance |
may return a cached or configured instance |
newInstance |
guaranteed to be a new object each call |
parse |
build from text — LocalDate.parse |
The builder
Static factories run out when a type has many fields and most are optional.
Subscriber s = new Subscriber("Priya", "411207", Plan.VEG,
LocalDate.of(2026, 9, 1), null, true, false, 3);
Nobody can read that. Which boolean is which? What is the 3? Adding a ninth
field means changing every call site. And the classic response — a constructor
per combination — grows exponentially and is known, with feeling, as the
telescoping constructor.
public final class Subscription {
private final String customer;
private final String pincode;
private final Plan plan;
private final LocalDate startedOn;
private final boolean paused;
private final int tiffinsPerDay;
private Subscription(Builder b) {
this.customer = b.customer;
this.pincode = b.pincode;
this.plan = b.plan;
this.startedOn = b.startedOn;
this.paused = b.paused;
this.tiffinsPerDay = b.tiffinsPerDay;
}
public static Builder builder(String customer, Plan plan) {
return new Builder(customer, plan);
}
public static final class Builder {
// Required: taken in the builder's own constructor, so they cannot be
// forgotten. Everything else has a sensible default.
private final String customer;
private final Plan plan;
private String pincode = "411001";
private LocalDate startedOn = LocalDate.now();
private boolean paused = false;
private int tiffinsPerDay = 1;
private Builder(String customer, Plan plan) {
this.customer = customer;
this.plan = plan;
}
public Builder pincode(String pincode) {
this.pincode = pincode;
return this;
}
public Builder startedOn(LocalDate startedOn) {
this.startedOn = startedOn;
return this;
}
public Builder paused(boolean paused) {
this.paused = paused;
return this;
}
public Builder tiffinsPerDay(int tiffinsPerDay) {
this.tiffinsPerDay = tiffinsPerDay;
return this;
}
public Subscription build() {
// Validate here, once, rather than in each setter. A half-built
// builder is allowed to be invalid; a built object never is.
if (!pincode.matches("[1-9]\\d{5}")) {
throw new IllegalArgumentException("pincode must be six digits not starting with zero, got [" + pincode + "]");
}
if (tiffinsPerDay < 1 || tiffinsPerDay > 10) {
throw new IllegalArgumentException("tiffinsPerDay must be 1 to 10, got " + tiffinsPerDay);
}
return new Subscription(this);
}
}
}
Subscription s = Subscription.builder("Priya", Plan.VEG)
.pincode("411207")
.tiffinsPerDay(2)
.build();
Every value is labelled at the call site. Optional things can be omitted. Adding a field breaks nobody.
Three details that are easy to get wrong:
Required fields go in the builder's constructor, not as chainable methods.
Otherwise build() has to check for "you forgot the customer", which is a
runtime error where the compiler could have done the job.
Validation lives in build(), not in each setter. A builder mid-chain is
allowed to be inconsistent; the object it produces never is.
build() returns a new object every time. A builder you can call build()
on twice and get the same mutable object back is a source of very confusing
bugs.
Records changed when you need a builder
A record gives you an immutable carrier, a constructor, equals, hashCode and
toString in one line:
public record Delivery(LocalDate date, String customer, int tiffins) { }
For three fields, all required, that is the whole job — no builder needed. The
capstone's Delivery and Subscriber are records for exactly this reason.
Reach for a builder when there are many fields and most are optional. Below about four fields, or when all of them are required, a record or a plain constructor reads better and is less code to maintain.
Records can still have a compact constructor doing the validation:
public record Subscriber(String name, String pincode, Plan plan, LocalDate startedOn) {
public Subscriber {
if (!pincode.matches("[1-9]\\d{5}")) {
throw new IllegalArgumentException("pincode must be six digits not starting with zero, got [" + pincode + "]");
}
}
}
That runs before the fields are assigned, so an invalid Subscriber cannot
exist. It is the single best feature records have, and it is the reason the
capstone uses one.
What about the abstract factory?
The pattern books give a lot of space to abstract factory — a factory that returns a family of related objects, so you can swap the whole family at once.
In practice, in Java, that job is done by dependency injection, which the next lesson covers. You will read about abstract factory; you will rarely write one. Knowing what it is for is enough.
Check your work
Why a static factory beats a constructor sometimes: it has a name, need not allocate, can return a subtype, and two of them can take the same parameters.
What a boolean parameter usually means: a factory method you did not write.
Why the JDK naming conventions matter: of, valueOf, from,
getInstance, newInstance and parse each promise something different.
What problem the builder solves: many fields, most optional, and a constructor call nobody can read.
Why required fields go in the builder's constructor: so forgetting one is a compile error rather than a runtime one.
Why validation lives in build(): a half-built builder may be invalid; the
object it produces may not.
When a record is the better answer: few fields, all required — which is most data types.
What a compact constructor buys you: validation before assignment, so an invalid instance cannot exist.
Practice
- Add a static factory
Delivery.on(date, customer, tiffins)to the capstone and use it in one test. Decide whether it improved anything. - Find a constructor in your own code with a boolean parameter. Replace it with two named factory methods.
- Write a
Subscriptionbuilder as shown and try tobuild()without a pincode. Then move the pincode to the builder's constructor and try again. - Move the validation from
build()into the individual setters. Then build an object setting fields in a different order, and explain the result. - Make
build()return the same instance on the second call. Write the bug that causes. - Count the fields on your capstone's
Subscriber. Argue for and against a builder. - Look up
EnumSet.ofin the JDK source and find where it returns a different class. - Write a compact constructor for
Deliveryrejecting a negativetiffins, and a test proving an invalid one cannot be constructed. - Compare
List.of(...)withnew ArrayList<>(...). Find two behavioural differences. - Write down the last time you needed more than four constructor parameters, and what you did instead.
Next: singleton, and why the industry moved away from it.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship