BUILDER / QUICK REFERENCE
The pattern,
at a glance.
Accumulate construction steps. Return a completed product. Keep draft and result separate.
- What changes?
- The parts and steps used to construct an object.
- What stays stable?
- The product’s rules and the named construction operations.
- Who chooses the steps?
- The caller, or an optional recipe that coordinates them.
The roles
- Product
Report - The finished object. This example returns validated, independent snapshots.
- Builder
ReportBuilder - Owns unfinished state and implements named construction operations.
- Caller
- Chooses the parts, or asks a recipe to apply a known sequence.
- Optional director
weeklyRecipe - Coordinates a reusable construction sequence. It can be a function; a separate class is not required.
This is a single concrete, fluent builder. The broader pattern can use a builder interface and multiple implementations to produce different representations; this lesson does not need that extra layer.
The shape in TypeScript
1. Own the construction state
export class ReportBuilder {
private draft: ReportDraft = { title: "", body: "" };
withTitle(title: string): this {
this.draft.title = title;
return this;
}
withBody(body: string): this {
this.draft.body = body;
return this;
}
withSummary(summary: string): this {
this.draft.summary = summary;
return this;
}
withChart(values: readonly number[]): this {
this.draft.chart = [...values];
return this;
}
withFooter(footer: string): this {
this.draft.footer = footer;
return this;
}
build(): Report {
return createReport(this.draft);
}
// Lesson inspection only: the unfinished draft is not a valid Report.
inspect(): ReportDraft {
return {
...this.draft,
chart: this.draft.chart ? [...this.draft.chart] : undefined,
};
}
}2. Name the parts you need
export function buildFooterReport() {
return new ReportBuilder()
.withTitle("Weekly update")
.withBody("We shipped a new lesson.")
.withFooter("Prepared for the team")
.build();
}Optional: reuse a recipe
// An optional director-like recipe reuses a construction sequence.
export function weeklyRecipe(
builder: ReportBuilder,
title: string,
body: string,
) {
return builder
.withTitle(title)
.withBody(body)
.withSummary("Progress at a glance")
.withChart([12, 18, 15])
.withFooter("Prepared for the team")
.build();
}Product types and shared validation
export type ReportDraft = {
title: string;
body: string;
summary?: string;
chart?: readonly number[];
footer?: string;
};
export type Report = Readonly<ReportDraft>;
export function createReport(draft: ReportDraft): Report {
const title = draft.title.trim();
const body = draft.body.trim();
if (!title || !body) throw new Error("A report needs a title and body.");
if (title.length > 60 || body.length > 240)
throw new Error(
"Keep the title within 60 characters and the body within 240.",
);
if (
draft.chart &&
(!draft.chart.length ||
[...draft.chart].some(
(value) => !Number.isInteger(value) || value < 0 || value > 100,
))
)
throw new Error(
"Chart values must be a nonempty list of whole numbers from 0 to 100.",
);
return Object.freeze({
title,
body,
summary: draft.summary?.trim() || undefined,
chart: draft.chart ? Object.freeze([...draft.chart]) : undefined,
footer: draft.footer?.trim() || undefined,
});
}These excerpts come from the TypeScript implementation used by the workshop.
Decide what build() means.
The pattern does not prescribe validation, immutability or resetting. This implementation makes those choices explicit:
- Title and body must be nonblank, within 60 and 240 characters. Validation runs at build time and is shared with the before design.
- Each named step replaces one part. Optional sections can be omitted; there are no automatic additions.
- Chart inputs are copied on entry. Each product copies the chart again, so later steps and caller mutations cannot change an earlier report.
- build() keeps the draft for reuse and returns a new product each time. The UI’s Reset creates a fresh builder; the weekly recipe also starts fresh in the workshop.
- A failed build returns no product. The workshop keeps the last successful report visible and labels it separately from the draft.
TypeScript freezes the report and its chart array. Kotlin uses val properties and a defensive, unmodifiable chart copy. Those are example guarantees, not automatic benefits of every Builder.
Use it when construction deserves its own place.
A good fit
Staged assembly, multiple optional parts, reusable construction sequences, or different products behind a common set of steps.
Start simpler when possible
A TypeScript options object or Kotlin named/default arguments already make optional inputs readable. A Builder adds mutable draft state, methods and lifecycle choices.
Common mistakes
- Confusing chaining with the pattern. The point is separating construction, not returning this everywhere.
- Leaking the draft. Returning the builder’s mutable object lets later steps rewrite old products.
- Assuming an automatic reset. Reuse/reset behavior must be documented; this builder retains its state.
- Sharing a builder between independent jobs. Their steps can interfere. Prefer a fresh builder per independent construction.
- Claiming validation requires Builder. Constructors and factories can enforce the same rules.