Design Patterns · Creational Patterns
Builder
Construct a complex object in named steps. Reuse the recipe while changing the representation it produces.
This lesson follows Abstract Factory. That pattern chooses a family. Builder separates a construction recipe from the steps that carry it out.
Builder creates a complex object one step at a time. It keeps the unfinished product inside the builder. That boundary costs another object and a construction API. Earn it with a real assembly process.
Start from a constructor nobody can read
new Report("Weekly sales", true, false, "UTC", 7, true, false);
The arguments hide their meaning. Adding a chart changes calls across the application. A subclass for each combination creates another problem: a weekly report with charts, a weekly report without charts, and a monthly report with charts all need names.
Named options often solve this first. Builder becomes useful when construction has several steps, nested parts, or multiple output representations. A fluent method chain alone does not establish the pattern.
Separate the recipe from the representation
A weekly report has a title and two sections. One builder renders HTML. Another collects an outline for a preview panel. Both accept the same construction steps. Their results do not need a common interface.
interface ReportBuilder {
reset(): void;
title(text: string): void;
section(text: string): void;
}
class HtmlReportBuilder implements ReportBuilder {
private parts: string[] = [];
reset() { this.parts = []; }
title(text: string) {
this.parts.push("<h1>" + escapeHtml(text) + "</h1>");
}
section(text: string) {
this.parts.push("<p>" + escapeHtml(text) + "</p>");
}
result(): string { return this.parts.join(""); }
}
type Outline = { title: string; sections: string[] };
class OutlineBuilder implements ReportBuilder {
private draft: Outline = { title: "", sections: [] };
reset() { this.draft = { title: "", sections: [] }; }
title(text: string) { this.draft.title = text; }
section(text: string) { this.draft.sections.push(text); }
result(): Outline {
return { title: this.draft.title, sections: [...this.draft.sections] };
}
}
class ReportDirector {
weekly(builder: ReportBuilder) {
builder.reset();
builder.title("Weekly sales");
builder.section("Revenue: 1200 dollars");
builder.section("Orders: 40");
}
}
function escapeHtml(text: string): string {
return text.replace(/&/g, "&")
.replace(/</g, "<").replace(/>/g, ">")
.replace(/"/g, """).replace(/'/g, "'");
}
const builder = new OutlineBuilder();
new ReportDirector().weekly(builder);
const report = builder.result();
Try it
Run the same recipe with a different builder
title → an h1 tag
section → a paragraph tag
result → one HTML string
title → a title field
section → one array entry
result → an Outline object
The construction sequence stayed the same. The product type changed.
Keep the unfinished object private
A director is optional. A client can call the builder directly for a custom report. Add a director when several callers need the same recipe. Do not add one only to give the pattern every role from a diagram.
Step through
Trace a second report
Reset before another recipe
Without reset, the second report can include sections from the first. Decide whether reset happens at recipe start or after retrieving a result.
Assemble inside the builder
If a real report needs a title before it is valid, check that rule in result(). Keep the incomplete draft away from callers.
Return an independent product
OutlineBuilder copies its array. Adding another section later cannot change an already returned report. Copy or transfer ownership deliberately.
Map the roles
| Role | In this example | Job |
|---|---|---|
| Builder | ReportBuilder | Declares the construction steps shared by every builder |
| Concrete builders | HtmlReportBuilder, OutlineBuilder | Implement the steps for one representation and expose their own result() |
| Products | an HTML string, an Outline object | The finished objects. Products from different builders need no common interface. |
| Director | ReportDirector | Calls the steps in a known order so a recipe can be reused. Optional. |
| Client | the code that calls weekly(builder) | Picks a builder, passes it to the director, and reads the result from the builder |
Check yourself
Why does result() live on each concrete builder instead of on ReportBuilder?
Correct. The HTML builder returns a string and the outline builder returns an object. Put result() on the interface only when every product shares one type.
Not quite. They can. The problem is that there is no single return type to declare.
Reach for it when construction itself is the hard part
- to retire a telescoping constructor. Ten optional parameters lead to overloads such as
Report(title),Report(title, charts), and so on. A builder lets each caller run only the steps it needs. - to build different representations through the same steps. An HTML report and a preview outline follow the same title-then-sections process and differ only in details.
- to build trees and other nested objects. Steps can be deferred or called recursively, which suits Composite trees. The builder never hands out a half-built product, so callers cannot read an incomplete result.
Check yourself
A menu editor builds nested submenus by calling the same addSection() step recursively. Which use of Builder is that?
Correct. Recursive steps that assemble a nested structure are one of Builder's three main uses.
Not quite. Nothing here has a long parameter list. The difficulty is the nested assembly.
Implement it in six steps
Refactor existing code toward the pattern in this order:
- Define the construction steps that every required representation shares. If no common steps exist, Builder does not fit.
- Declare those steps in the builder interface.
- Write one concrete builder per representation. Give each its own method to fetch the result, because unrelated products cannot share one return type.
- Consider a director that encapsulates the common recipes, such as
weekly()andmonthly(). - In the client, create the builder and the director, then pass the builder to the director through its constructor or its construction method.
- Read the result from the director only when every product shares one interface. Otherwise read it from the builder.
Check yourself
Two builders produce unrelated types. Where should the client read the finished product?
Not quite. That works only when all products share an interface the director can return.
Correct. The director only knows the builder interface, so it cannot return a type it does not know.
Name what it costs
| You gain | You pay |
|---|---|
| Build step by step, defer steps, or run them recursively | More classes than a constructor with named options |
| Reuse one assembly sequence across representations | Every builder must support every shared step, even awkwardly |
| Keep complex construction out of the product's business logic (single responsibility) | Reset, validation, and result ownership need explicit rules |
Check yourself
Two builders do not have any meaningful construction steps in common. What should change?
Not quite. That increases ceremony without creating a real shared process.
Correct. The pattern needs a common assembly vocabulary. Forced no-op steps hide incompatible products.
Do not confuse it with its neighbors
| Pattern | How it relates |
|---|---|
| Factory Method | Many designs start with Factory Method and move to Builder, Abstract Factory, or Prototype when they need more flexibility. |
| Abstract Factory | Abstract Factory returns each related product immediately. Builder lets you run several steps before you fetch one product. |
| Composite | Builder is a natural way to assemble a composite tree, because its steps can recurse. |
| Bridge | They combine well: the director plays the abstraction and the builders play the implementations. |
| Singleton | A builder, like an abstract factory or a prototype registry, can be implemented as a singleton when one instance is enough. |
Check yourself
A screen needs a matching button, checkbox, and menu, each returned right away. Which pattern fits better than Builder?
Correct. The pressure is a family of matching products, each returned immediately.
Not quite. Builder assembles one product over several steps. Nothing here needs staged assembly.
Retrieve and apply
Check yourself
An endpoint accepts three independent optional fields and creates one plain configuration object. What earns its cost first?
Correct. There is no staged assembly or second representation. Named fields remove the confusing argument order with less machinery.
Not quite. Those roles need a construction problem. Three optional fields alone do not provide one.
Sketch a builder that produces both a notification payload and a preview. Name the common steps, the two result types, and the reset rule. Continue to Prototype when an existing object already contains the configuration worth reusing.
Source: Alexander Shvets, Dive Into Design Patterns (深入设计模式), Chinese edition v2021-1.25. Builder, printed pages 101–118. Explanations, examples, and exercises are adapted for this course.