Skip to content
QSWEQB
mediumConceptDesignMidSenior

When is a factory the right answer, when is a builder, and when should you just call the constructor?

A constructor is the default and needs no justification. A static factory earns its place when construction can return a cached or alternative implementation, or needs a name; a builder when there are many optional fields. Singleton is usually a testability liability.

7 min readUpdated 2026-07-27

What the interviewer is scoring

  • Does the candidate treat the constructor as the default that alternatives must argue against
  • Whether they can name something a static factory does that a constructor is structurally unable to do
  • That the builder justification rests on optional fields and immutability, not on it looking tidier
  • Whether they raise the builder's real cost, that required-field checks move from compile time to runtime
  • Does the candidate object to singleton on testability and hidden-dependency grounds rather than reciting double-checked locking

Answer

The constructor is the default, and the burden of proof is on the alternative

new Money(1250, Currency.GBP) needs no defence. It is the shortest path, the reader knows exactly which class they are getting, the compiler enforces that both arguments are present, and there is no extra type to maintain. Every other option in this family costs a class or a method that has to be read, tested and kept honest, so the interesting question is never "which creational pattern" but "what does the constructor fail to do here".

Three things it structurally cannot do. It cannot have a meaningful name, because its name is fixed to the class. It cannot decline to create a new object, because new always allocates. And it cannot return a different type from the one named. Every legitimate use of a factory traces back to one of those three.

What a static factory buys

Naming is the cheapest win and matters most when two constructions differ only in how you interpret the arguments. Money.ofMinorUnits(1250, GBP) and Money.ofMajorUnits(12.50, GBP) cannot both be constructors, because they would have distinct parameter types by accident rather than by design, and a reader at the call site would have to know which overload does which.

Returning an existing instance is the win that has no alternative at all. Integer.valueOf returns a cached object for small values, which a constructor cannot do because it has to allocate; this is why Integer.valueOf(100) == Integer.valueOf(100) holds while new Integer(100) == new Integer(100) does not, and why the boxing constructors were deprecated.

Returning a subtype lets the choice of implementation stay private. EnumSet.noneOf returns an implementation sized to the enum's constant count, and callers only ever see EnumSet. That is the version of "programme to an interface" that actually pays: the factory is the single place that knows which concrete class exists, so swapping it is not a source change for callers.

import java.util.List;

public final class Route {
    private final List<String> stops;

    private Route(List<String> stops) { this.stops = stops; }

    /** Named, validating, and free to return something other than a fresh object. */
    public static Route of(List<String> stops) {
        if (stops.size() < 2) throw new IllegalArgumentException("a route needs two stops");
        return new Route(List.copyOf(stops));
    }

    public static Route direct(String from, String to) {
        return new Route(List.of(from, to));   // reads as intent at the call site
    }

    public List<String> stops() { return stops; }
}

Worth distinguishing three things that all get called "factory", because interviewers use the word loosely and then check whether you do. A static factory method is the code above: a named alternative to a constructor on the same class. The factory method pattern is an overridable method that lets a subclass decide the concrete type. An abstract factory is an object with several creation methods that produces a matched family of products — the case for it is that the products must be consistent with each other, and that condition is rarely met in an interview-sized problem.

When a builder earns its place

The signal is a constructor whose parameter list has grown past the point where a reader can match arguments to fields, usually because most of the parameters are optional. The failure mode without one is telescoping constructors: five overloads, each delegating to the next with a default, and a call site reading new Report(data, true, false, null, 30, null) that nobody can review.

import java.time.Duration;

public final class Report {
    private final String dataset;          // required
    private final boolean includeCharts;
    private final Duration timeout;
    private final String watermark;        // nullable: absent means no watermark

    private Report(Builder b) {
        this.dataset = b.dataset;
        this.includeCharts = b.includeCharts;
        this.timeout = b.timeout;
        this.watermark = b.watermark;
    }

    // The required field is a constructor argument of the Builder itself, so it
    // cannot be forgotten. Only genuinely optional fields get setters.
    public static final class Builder {
        private final String dataset;
        private boolean includeCharts = false;
        private Duration timeout = Duration.ofSeconds(30);
        private String watermark;

        public Builder(String dataset) {
            if (dataset == null || dataset.isBlank())
                throw new IllegalArgumentException("dataset is required");
            this.dataset = dataset;
        }

        public Builder includeCharts(boolean v) { this.includeCharts = v; return this; }
        public Builder timeout(Duration v) { this.timeout = v; return this; }
        public Builder watermark(String v) { this.watermark = v; return this; }

        public Report build() { return new Report(this); }
    }

    public String dataset() { return dataset; }
    public boolean includeCharts() { return includeCharts; }
    public Duration timeout() { return timeout; }
    public String watermark() { return watermark; }
}

The builder also gives you immutability with readable construction, which is why it appears throughout the JDK for configuration-shaped objects — HttpClient.newBuilder() and HttpRequest.newBuilder() both exist because those objects have a dozen optional settings and must be safe to share once built.

Its cost is real and candidates rarely mention it: a builder converts missing-required-field errors from compile failures into runtime exceptions. new Report(dataset, ...) cannot be called without a dataset; new Report.Builder(...).build() can be written wrongly and only fail when it runs. The mitigation is exactly what the code above does — take required fields in the builder's own constructor, so the compiler still enforces them, and reserve the fluent setters for the optional ones. A builder with eight fluent setters and eight runtime null checks in build() has moved your type errors from the compiler to production.

Singleton is usually a liability

The pattern has a bad reputation for reasons that go well beyond thread-safety trivia, and reciting double-checked locking is a signal you have learnt the implementation of something you should be arguing against.

The primary objection is that it hides a dependency. A class that calls PricingConfig.getInstance() inside a method has a dependency on PricingConfig that appears in no signature, so no reader of the constructor and no caller can see it. Multiply that across a codebase and the dependency graph exists only in method bodies, which is precisely the graph you need when you want to know what a change breaks.

The second is testability, which follows directly. Because the instance is reached statically, a test cannot substitute a different one without reflection or a static setter, and because it survives between tests, one test's mutation is visible to the next. That produces the specific pathology of a suite that passes and fails depending on ordering, which then costs a day to diagnose.

The third is that the guarantee is weaker than people think. "One instance" means one per classloader, not one per process, and certainly not one per cluster. Any design whose correctness genuinely depends on there being exactly one of something across several machines needs a coordination mechanism, and a static field is not it.

The right answers, in order of preference. Pass the collaborator in as a constructor parameter and let something at the top of the application decide how many exist — with a dependency-injection container this is a scope annotation, and the object is a singleton in the deployment while remaining an ordinary class with an ordinary constructor to every test. If the thing is genuinely a stateless constant with no dependencies, a single-element enum is the safest form in Java, because the language guarantees the instance count and it is immune to the serialisation and reflection attacks that defeat a lazily initialised static field. What is almost never right is the classic lazy getInstance() with a mutable static field, which buys nothing over the two options above and costs you the dependency graph.

Over-fitting is the more common failure than under-using

Interviewers see far more candidates who reach for machinery than candidates who fail to. A builder for a class with three required fields and no optional ones is pure ceremony, and in a language with named or default arguments it is ceremony that the language already removed. A factory that has exactly one implementation to return and always will is an indirection with no decision behind it. An abstract factory in a problem with one product family is a diagram, not a design.

The question that separates a designed answer from a recited one is what varies. A factory pays when the concrete type varies or the instance can be reused; a builder pays when the field set varies per call site; a constructor pays whenever neither is true, which in an interview-sized problem is most of the time. Being willing to say "a constructor, because nothing here varies" is a stronger answer than producing a pattern for it.

Name the thing the constructor could not do before you replace it. If you cannot — no name to disambiguate, no instance to reuse, no subtype to hide, no optional fields to skip — then the pattern is not solving your problem, and singleton in particular is trading a visible dependency for an invisible one.

Likely follow-ups

  • How would you make a builder reject an object missing a required field at compile time?
  • What does a dependency-injection container give you that a singleton does not?
  • Why is an enum the safest way to implement a singleton in Java if you must have one?
  • When does a factory method belong on the type it creates, and when does it belong on a separate class?

Related questions

Further reading

creational-patternsfactorybuildersingletonimmutability