Home Design Patterns Builder

Builder

Creational Gang of Four

“Separate the construction of a complex object from its representation so that the same construction process can create different representations.”

Gamma, Helm, Johnson & Vlissides, p. 97

Purpose

Builder splits "what the steps are" from "what the steps produce". One class, the Director, knows the order of assembly and nothing about the result. Another class, the Builder, receives the steps and decides what to make of them. Run the same director over three different builders and you get three different outputs from one traversal.

It is worth saying early that two different patterns share this name. The one in the book is the director-and-builder arrangement above, and it is genuinely uncommon. The one most people mean today is the fluent builder, where an object with many optional fields gets a companion class you chain setter calls on before calling build(). They solve different problems and only the first one is on this page's diagram. I cover the second in the criticisms, because it deserves the space more than it deserves the confusion.

Design

The Builder interface declares one method per part, plus a way to retrieve the finished product. The Director holds a Builder and implements the assembly sequence once, calling those methods in whatever order and however many times the input demands. Concrete builders accumulate whatever internal state they need and answer with their own result type at the end.

UML class diagram: Director holds a Builder; two concrete builders implement the Builder interface, and one of them creates a Product.
Notice that getResult() is the awkward one. Each concrete builder returns a different type, so in most languages that method cannot honestly live on the shared interface, and the client has to know which builder it handed over.

The Director is the part people drop first, and often correctly. It only earns its keep when the assembly sequence is itself non-trivial and shared. If the sequence is "call these four setters", the Director is a class that adds nothing but a layer.

Example

An Autobot Robotics customer writes a job program once and it has to come out three ways: as the opcode stream the embedded controller executes, as a printed travel sheet the operator follows during setup, and as a model the offline simulator runs before anything touches real hardware. Same job, three representations.

These started life as three separate pieces of code that each walked the job definition. They drifted, as three copies of a traversal always do. The one that mattered was dwell: the simulator treated a dwell as time after the move completed, and the controller started the timer when the move command was issued. Programs validated clean in simulation and then collided with a fixture on the floor, because in the real machine the arm was still moving when the clock started.

The traversal is now written once, in a JobWalker, and there are three builders behind it. The disagreement about dwell became impossible to express, because there is no longer more than one place that decides what a dwell step is. That is the property worth paying for here, more than the reduction in code.

interface JobBuilder
{
    void beginJob(string name);
    void addMove(Point target, int speed);
    void addDwell(int milliseconds);
    void endJob();
}

class JobWalker                       // the Director
{
    JobBuilder builder;
    public JobWalker(JobBuilder b) { builder = b; }

    public void walk(JobDefinition job)
    {
        builder.beginJob(job.Name);
        foreach (Step s in job.Steps)
        {
            if (s.IsMove)  builder.addMove(s.Target, s.Speed);
            if (s.IsDwell) builder.addDwell(s.Milliseconds);
        }
        builder.endJob();
    }
}

class OpcodeBuilder : JobBuilder
{
    public void addMove(Point target, int speed) {
        // emit controller opcodes
    }
    // beginJob, addDwell and endJob the same way

    public byte[] getResult() { return new byte[0]; }
}

class TravelSheetBuilder : JobBuilder
{
    public void addMove(Point target, int speed) {
        // append one line an operator can read
    }
    // beginJob, addDwell and endJob the same way

    public string getResult() { return ""; }
}
walk() is the only copy of the traversal, and it names no output format. The two getResult() methods return different types and so are absent from the interface, which is the pattern's one permanent rough edge.

Seen in the Wild

SAX parsing is the book's Builder almost exactly: the parser is the Director, walking the document once, and a ContentHandler is the Builder, turning that single stream of events into whatever representation you need. The fluent variety is far more visible: Protocol Buffers generates a nested Builder class for every message type, Java's DateTimeFormatterBuilder assembles a formatter piece by piece, and OkHttp's Request.Builder is the standard way to construct a request. Java's StringBuilder is the one to be careful about; despite the name it is a mutable buffer, not this pattern.

Comparisons

  • Abstract Factory returns a finished object per call. A Builder is fed over many calls and answers at the end. Abstract Factory is about which family, Builder is about how it is assembled.
  • Template Method is the usual shape of the Director's construct(): a fixed skeleton with the varying parts delegated. A Director delegates to another object, a template method delegates to a subclass.
  • Composite is very often what a Builder is building, since trees are exactly the sort of structure that is painful to assemble in one constructor call.
  • Visitor is the same split viewed from the other side: Visitor walks a structure that already exists, a Director walks input in order to create one.
  • Prototype is the cheaper answer when the variations you need are all small deltas on an existing configured object.

Why It Exists

My take

The Gang of Four version exists because traversal logic is expensive to duplicate and almost impossible to keep in sync. Anything that walks a structure in a particular order accumulates rules: what counts as a step, what order things happen in, what the edge cases are. Copy that walk to produce a second output format and you have not copied code, you have copied a specification, and only one of the copies will get the next amendment. The dwell bug above is what that looks like when it finally lands.

The fluent version exists for an entirely different reason: constructors are positional, unnamed, and all-or-nothing. A class with fifteen fields, ten of them optional, has no good constructor. You either write a telescoping series of overloads, or you make every field settable and give up immutability. The fluent builder is the workaround, and the reason it is everywhere is that most languages still cannot express "named optional arguments to an immutable object". Python and Kotlin can, and it is telling that the pattern is much rarer in both.

My position is that these should have been given different names thirty years ago and the confusion is now permanent. If you are arguing with someone about whether Builder is overused, establish which one you are each talking about first. Half of those arguments evaporate on the spot.

Criticisms

The field list gets written twice. Every field in the product appears again in the builder, plus a setter, and the two drift the moment someone adds a field to one and forgets the other. Some languages generate this and some do not; if yours does not, that duplication is a permanent tax and you should count it before starting.

It also moves validation from compile time to run time, and pretends this is free. A constructor with four required parameters cannot be called wrong. A builder with four required fields can be built after setting three, and the compiler is perfectly happy; you find out at build(), or worse, you find out later when something reads a field that was never set. There are staged builder tricks that restore the compile-time check, and they cost one interface per required field, which nobody does twice.

For the director-and-builder version specifically, the return type problem never really goes away. Because each builder produces a different type, the finished object cannot be collected through the shared interface without generics, casts, or a common supertype that means nothing. The book acknowledges this and moves on. In practice it is the reason the pattern is usually implemented once, admired, and then quietly replaced with three functions.