Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Choosing Between Builder and Factory Patterns

Hello! In the previous lesson, you used Strategy to move alternative shipping calculations out of a service. The strategies already existed when the service selected one. This lesson asks a different question: how should an object be created in the first place?

You’ll compare Builder and Factory using Java backend examples, then develop an interview-ready way to choose between them. Allow about 40 minutes, including time to read, type, and trace the examples.


When configuring one object is the hard part

Imagine constructing an outbound HTTP request. Every request needs a path and body, but a caller might also specify a timeout and several headers. A constructor with many optional arguments makes call sites hard to read; a collection of overloaded constructors becomes harder to maintain as options grow. Setters avoid the long constructor but allow the request itself to exist in a partially configured state.

Watch the opening of JRH’s Java Builder Pattern Explained in 3 Minutes. It illustrates these trade-offs and the structure of a fluent Java builder.

Java Builder Pattern Explained in 3 Minutes

JRH’s Java Builder Pattern Explained in 3 Minutes introduces telescoping constructors, setters, and a nested Builder. Watch it to identify which object is mutable during construction and which object is returned by build().

Watch the construction problem, from the many-constructor example through the first nested Builder. Then watch required fields, focusing on validation versus passing mandatory values to the builder’s constructor. Stop before the later, more elaborate interface-chaining technique.

Here is a builder for that request. The builder holds changeable settings; build() produces a value whose fields cannot subsequently be reassigned.

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

final class OutboundRequest {
    private final String path;
    private final String body;
    private final Duration timeout;
    private final Map<String, String> headers;

    private OutboundRequest(Builder builder) {
        if (builder.path.isBlank() || builder.body.isBlank()) {
            throw new IllegalArgumentException(
                    "Path and body must not be blank");
        }

        this.path = builder.path;
        this.body = builder.body;
        this.timeout = builder.timeout;
        this.headers = Map.copyOf(builder.headers);
    }

    static Builder builder(String path, String body) {
        return new Builder(path, body);
    }

    String path() { return path; }
    Duration timeout() { return timeout; }
    Map<String, String> headers() { return headers; }

    static final class Builder {
        private final String path;
        private final String body;
        private Duration timeout = Duration.ofSeconds(2);
        private final Map<String, String> headers = new HashMap<>();

        private Builder(String path, String body) {
            this.path = Objects.requireNonNull(path, "path");
            this.body = Objects.requireNonNull(body, "body");
        }

        Builder timeout(Duration value) {
            Objects.requireNonNull(value, "timeout");
            if (value.isZero() || value.isNegative()) {
                throw new IllegalArgumentException(
                        "Timeout must be positive");
            }
            this.timeout = value;
            return this;
        }

        Builder header(String key, String value) {
            headers.put(
                    Objects.requireNonNull(key, "key"),
                    Objects.requireNonNull(value, "value"));
            return this;
        }

        OutboundRequest build() {
            return new OutboundRequest(this);
        }
    }
}

A call site can name only the options it needs:

OutboundRequest request = OutboundRequest.builder("/jobs", "{}")
        .timeout(Duration.ofSeconds(3))
        .header("X-Trace", "demo")
        .build();

If you type this example, trace what happens when you call header() after build(): the builder’s map changes, but the previously built request’s headers do not. Map.copyOf makes a separate, unmodifiable map. This works as an immutable value object here because the other stored values—strings and Duration—are also immutable. Placing required arguments at the builder’s entry point makes their omission impossible at the call site; the null and blank checks still protect against invalid values.

Builder is about managing construction of one result. Its fluent methods can collect optional values, supply defaults, and enforce invariants before exposing the finished object. It does not require that every object be assembled over a long period of time: in this example, all the calls may happen in one expression.


When choosing the kind of object is the hard part

Now consider a retention rule. A backend might retain some records indefinitely and expire others after a configured duration. Both kinds of rule answer the same question, but they use different classes.

import java.time.Duration;
import java.time.Instant;
import java.util.Objects;

enum RetentionMode {
    FOREVER, TTL
}

interface RetentionPolicy {
    boolean expired(Instant storedAt, Instant now);
}

record KeepForever() implements RetentionPolicy {
    @Override
    public boolean expired(Instant storedAt, Instant now) {
        return false;
    }
}

record ExpireAfter(Duration ttl) implements RetentionPolicy {
    ExpireAfter {
        Objects.requireNonNull(ttl, "ttl");
        if (ttl.isZero() || ttl.isNegative()) {
            throw new IllegalArgumentException("TTL must be positive");
        }
    }

    @Override
    public boolean expired(Instant storedAt, Instant now) {
        return !storedAt.plus(ttl).isAfter(now);
    }
}

final class RetentionPolicyFactory {
    private RetentionPolicyFactory() {}

    static RetentionPolicy create(RetentionMode mode, Duration ttl) {
        Objects.requireNonNull(mode, "mode");

        return switch (mode) {
            case FOREVER -> {
                if (ttl != null) {
                    throw new IllegalArgumentException(
                            "FOREVER must not have a TTL");
                }
                yield new KeepForever();
            }
            case TTL -> new ExpireAfter(
                    Objects.requireNonNull(ttl, "ttl"));
        };
    }
}

The caller asks for a RetentionPolicy, rather than naming a concrete class:

RetentionPolicy policy = RetentionPolicyFactory.create(
        RetentionMode.TTL, Duration.ofDays(7));

The factory owns which implementation to instantiate and checks configuration that depends on that choice. The returned policy owns how expiry behaves. That distinction connects this lesson to Strategy: KeepForever and ExpireAfter are alternative behaviors, while the factory is one possible way to create the chosen behavior. In a Spring application where long-lived policies are already injected, selecting an existing policy from an injected map may be simpler than manufacturing a new one for every request.

There is a naming distinction worth making in interviews. RetentionPolicyFactory is commonly called a simple factory: one method selects and creates an implementation. The formal Factory Method pattern instead puts an overridable creation method on a creator class, allowing creator subclasses to choose the product type. Both address creation, but the presence of a method named create does not, by itself, establish the formal pattern.

Factory Method structure: concrete creators override `createProduct()` to produce different implementations of the common `Product` interface. The creator can use that product through the interface.

In the diagram, notice that the creator’s someOperation() can work with Product without knowing which concrete product a subclass supplies. Use this subclass-based structure when varying the creator is genuinely useful. For a single selection point like the retention example, a simple factory is often enough.


Make the choice by locating the difficulty

Baeldung’s Builder Pattern vs. Factory Pattern distinguishes a fluent builder for optional fields from a classic builder for structurally complex products. It also separates a simple factory idiom from a polymorphic factory pattern. Read those distinctions before applying this decision rule.

Builder Pattern vs. Factory Pattern

Read Baeldung’s Builder Pattern vs. Factory Pattern to compare the problems these approaches solve, rather than memorizing their class diagrams.

In “3. Builder,” read “3.1. Simple Builder” from builder motivation. Continue through “3.2. Classic Builder,” from structural construction. In “4. Factory,” read all of “4.1. Factory Idiom,” focusing on centralized creation, then “4.2. Factory Pattern,” focusing on polymorphic creation. Finish with “5. Prerequisites and Intentions,” from the comparison.

Our OutboundRequest uses the simple, fluent form of Builder. The classic form is useful when construction itself has reusable stages—for example, when the same sequence of construction operations can produce different representations. Its director, shown below, can coordinate those stages. A director is not required for the fluent request builder we wrote.

Classic Builder structure: a Director invokes construction steps through a Builder interface; different concrete builders can use those steps to produce different products. The client chooses a builder and retrieves its result.

For interview prompts, start with the source of complexity:

Object-creation problem Likely starting point Why
One request object has required fields, many optional settings, and validation at completion Builder Callers configure one result legibly before receiving it.
A mode or configuration determines which implementation of an interface to instantiate Simple factory The caller can depend on the interface without repeating selection and construction code.
Subclasses of a creator must supply different products to the creator’s shared workflow Factory Method An overridable creation method is the intended variation point.
An object has two clear required arguments and no meaningful construction choices Constructor Neither pattern earns its extra code.

These are starting points, not mutually exclusive rules. A factory can select a concrete implementation whose own builder handles many configuration options. Conversely, the mere presence of several fields does not demand a builder: if a small constructor remains clear, use it.

One subtle interview trap is to say, “Factory creates in one step; Builder creates in multiple steps,” and stop there. That describes their interfaces but misses their intent. Ask instead: Am I primarily deciding which type to make, or configuring how this particular result is put together? Then explain what would make you change your answer. For example, if the request gained no optional fields, you could replace its builder with a constructor. If retention stopped having alternative implementations, its factory might no longer be useful.


Key takeaways

Use a Builder when configuring and validating a result makes construction awkward; use a Factory when selecting or centralizing creation of an appropriate product is the main concern. In Java discussions, distinguish a practical simple factory from the subclass-based Factory Method pattern, and do not add either pattern when a constructor is clearer.

Next, you’ll turn from creating objects to handling failures: identifying Java exception-handling and resource-management code that hides errors or leaves resources open.

Can't find a good explanation? Sign up and we'll make it for you

Sign up