Skip to content

Builder - A Design Pattern for Complex Object Construction

The Builder is a creational design pattern that lets you construct complex objects step by step. It allows you to produce different types and representations of an object using the same construction code, separating construction from representation.


The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It’s especially useful when an object has many optional parameters or requires multiple steps to create.


Complex Configuration

Build objects with many optional parameters (HTTP clients, database configs).

Document Generation

Construct complex documents with multiple sections and formatting.

Query Builders

Build complex database queries step by step.

UI Component Trees

Construct complex nested UI components hierarchically.

Advantages ✅Disadvantages ❌
Avoids telescoping constructors.Increases overall code complexity.
Creates objects step by step.Requires creating multiple builder classes.
Reuses construction code for different representations.Can be overkill for simple objects.
Follows Single Responsibility Principle.More code to maintain.

The pattern consists of four main components:

  1. Product: The complex object being built
  2. Builder Interface: Declares construction steps common to all builders
  3. Concrete Builders: Provide different implementations of construction steps
  4. Director (Optional): Defines the order of construction steps

// Product
class House {
walls?: number;
doors?: number;
windows?: number;
roof?: string;
garage?: boolean;
garden?: boolean;
describe(): string {
return `House with ${this.walls} walls, ${this.doors} doors, ` +
`${this.windows} windows, ${this.roof} roof` +
`${this.garage ? ', garage' : ''}${this.garden ? ', garden' : ''}`;
}
}
// Builder Interface
interface HouseBuilder {
reset(): void;
setWalls(count: number): this;
setDoors(count: number): this;
setWindows(count: number): this;
setRoof(type: string): this;
setGarage(hasGarage: boolean): this;
setGarden(hasGarden: boolean): this;
build(): House;
}
// Concrete Builder
class ConcreteHouseBuilder implements HouseBuilder {
private house: House;
constructor() {
this.house = new House();
}
reset(): void {
this.house = new House();
}
setWalls(count: number): this {
this.house.walls = count;
return this;
}
setDoors(count: number): this {
this.house.doors = count;
return this;
}
setWindows(count: number): this {
this.house.windows = count;
return this;
}
setRoof(type: string): this {
this.house.roof = type;
return this;
}
setGarage(hasGarage: boolean): this {
this.house.garage = hasGarage;
return this;
}
setGarden(hasGarden: boolean): this {
this.house.garden = hasGarden;
return this;
}
build(): House {
const result = this.house;
this.reset();
return result;
}
}
// Director (Optional)
class HouseDirector {
constructor(private builder: HouseBuilder) {}
buildMinimalHouse(): House {
return this.builder
.setWalls(4)
.setDoors(1)
.setWindows(2)
.setRoof("flat")
.build();
}
buildLuxuryHouse(): House {
return this.builder
.setWalls(4)
.setDoors(3)
.setWindows(10)
.setRoof("pitched")
.setGarage(true)
.setGarden(true)
.build();
}
}
// Usage
const builder = new ConcreteHouseBuilder();
// Direct building
const customHouse = builder
.setWalls(4)
.setDoors(2)
.setWindows(6)
.setRoof("pitched")
.setGarden(true)
.build();
console.log(customHouse.describe());
// Using director
const director = new HouseDirector(builder);
const minimalHouse = director.buildMinimalHouse();
const luxuryHouse = director.buildLuxuryHouse();

FormBuilder.ts
interface FormField {
name: string;
type: string;
label: string;
required: boolean;
validation?: string;
}
interface FormConfig {
fields: FormField[];
submitUrl: string;
method: string;
showLabels: boolean;
submitText: string;
}
class FormBuilder {
private config: FormConfig;
constructor() {
this.config = {
fields: [],
submitUrl: '',
method: 'POST',
showLabels: true,
submitText: 'Submit'
};
}
addTextField(name: string, label: string, required = false): this {
this.config.fields.push({
name,
type: 'text',
label,
required
});
return this;
}
addEmailField(name: string, label: string, required = false): this {
this.config.fields.push({
name,
type: 'email',
label,
required,
validation: 'email'
});
return this;
}
addPasswordField(name: string, label: string, required = false): this {
this.config.fields.push({
name,
type: 'password',
label,
required
});
return this;
}
setSubmitUrl(url: string): this {
this.config.submitUrl = url;
return this;
}
setMethod(method: 'GET' | 'POST' | 'PUT' | 'DELETE'): this {
this.config.method = method;
return this;
}
hideLabels(): this {
this.config.showLabels = false;
return this;
}
setSubmitText(text: string): this {
this.config.submitText = text;
return this;
}
build(): FormConfig {
const result = { ...this.config };
this.config = {
fields: [],
submitUrl: '',
method: 'POST',
showLabels: true,
submitText: 'Submit'
};
return result;
}
}
export { FormBuilder, FormConfig };
<script setup lang="ts">
import { ref } from 'vue';
import { FormBuilder } from './FormBuilder';
const formConfig = ref(
new FormBuilder()
.addTextField('username', 'Username', true)
.addEmailField('email', 'Email Address', true)
.addPasswordField('password', 'Password', true)
.setSubmitUrl('/api/register')
.setSubmitText('Create Account')
.build()
);
</script>
<template>
<form :action="formConfig.submitUrl" :method="formConfig.method">
<div v-for="field in formConfig.fields" :key="field.name">
<label v-if="formConfig.showLabels">{{ field.label }}</label>
<input
:type="field.type"
:name="field.name"
:required="field.required"
/>
</div>
<button type="submit">{{ formConfig.submitText }}</button>
</form>
</template>

class SQLQuery {
select: string[] = [];
from: string = '';
where: string[] = [];
orderBy: string[] = [];
limit?: number;
offset?: number;
toString(): string {
let query = `SELECT ${this.select.join(', ')} FROM ${this.from}`;
if (this.where.length > 0) {
query += ` WHERE ${this.where.join(' AND ')}`;
}
if (this.orderBy.length > 0) {
query += ` ORDER BY ${this.orderBy.join(', ')}`;
}
if (this.limit) {
query += ` LIMIT ${this.limit}`;
}
if (this.offset) {
query += ` OFFSET ${this.offset}`;
}
return query;
}
}
class QueryBuilder {
private query: SQLQuery;
constructor() {
this.query = new SQLQuery();
}
select(...columns: string[]): this {
this.query.select.push(...columns);
return this;
}
from(table: string): this {
this.query.from = table;
return this;
}
where(condition: string): this {
this.query.where.push(condition);
return this;
}
orderBy(column: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
this.query.orderBy.push(`${column} ${direction}`);
return this;
}
limit(count: number): this {
this.query.limit = count;
return this;
}
offset(count: number): this {
this.query.offset = count;
return this;
}
build(): SQLQuery {
const result = this.query;
this.query = new SQLQuery();
return result;
}
execute(): string {
return this.build().toString();
}
}
// Usage
const query = new QueryBuilder()
.select('id', 'name', 'email')
.from('users')
.where('age > 18')
.where('status = "active"')
.orderBy('created_at', 'DESC')
.limit(10)
.offset(0)
.execute();
console.log(query);
// SELECT id, name, email FROM users WHERE age > 18 AND status = "active"
// ORDER BY created_at DESC LIMIT 10 OFFSET 0

interface RequestConfig {
url: string;
method: string;
headers: Record<string, string>;
body?: any;
timeout?: number;
auth?: { username: string; password: string };
}
class RequestBuilder {
private config: RequestConfig;
constructor(url: string) {
this.config = {
url,
method: 'GET',
headers: {}
};
}
method(method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'): this {
this.config.method = method;
return this;
}
header(key: string, value: string): this {
this.config.headers[key] = value;
return this;
}
headers(headers: Record<string, string>): this {
this.config.headers = { ...this.config.headers, ...headers };
return this;
}
json(data: any): this {
this.config.body = JSON.stringify(data);
this.header('Content-Type', 'application/json');
return this;
}
timeout(ms: number): this {
this.config.timeout = ms;
return this;
}
auth(username: string, password: string): this {
this.config.auth = { username, password };
const credentials = btoa(`${username}:${password}`);
this.header('Authorization', `Basic ${credentials}`);
return this;
}
bearer(token: string): this {
this.header('Authorization', `Bearer ${token}`);
return this;
}
build(): RequestConfig {
return { ...this.config };
}
async send(): Promise<Response> {
const config = this.build();
const options: RequestInit = {
method: config.method,
headers: config.headers,
body: config.body
};
if (config.timeout) {
const controller = new AbortController();
options.signal = controller.signal;
setTimeout(() => controller.abort(), config.timeout);
}
return fetch(config.url, options);
}
}
// Usage
const response = await new RequestBuilder('https://api.example.com/users')
.method('POST')
.bearer('your-token-here')
.json({ name: 'John Doe', email: 'john@example.com' })
.timeout(5000)
.send();
const data = await response.json();
console.log(data);

  1. Identify Complex Object Find an object with many parameters or complex construction logic.
  2. Extract Construction Steps Break down the construction process into clear, distinct steps.
  3. Create Builder Class Implement a builder with methods for each construction step.
  4. Implement Fluent Interface Make each method return this for method chaining.
  5. Add Build Method Create a final method that returns the constructed object.
  6. Optional: Add Director Create a director class for common construction sequences.

Use Fluent Interface

Return this from builder methods to enable method chaining.

Validate Before Build

Check that all required fields are set in the build() method.

Reset After Build

Reset the builder state after calling build() to avoid reuse issues.

Provide Defaults

Set sensible default values for optional parameters.



// Bad: Telescoping constructors
class Pizza {
constructor(
size: string,
cheese: boolean,
pepperoni: boolean,
bacon: boolean,
mushrooms: boolean,
olives: boolean
) { }
}
// Good: Builder pattern
class PizzaBuilder {
private pizza = {
size: 'medium',
toppings: [] as string[]
};
size(size: string): this {
this.pizza.size = size;
return this;
}
addTopping(topping: string): this {
this.pizza.toppings.push(topping);
return this;
}
build() {
return { ...this.pizza };
}
}

  • When an object has many optional parameters
  • When you want to create different representations of an object
  • When construction requires multiple steps
  • When you want to avoid telescoping constructors
  • When object construction is complex

  • For simple objects with few parameters
  • When object construction is straightforward
  • When immutability is not required
  • When a simple factory or constructor suffices