Builder Pattern
Construct complex objects step by step. The same construction process can create different representations.
The Problemβ
You're building a chart library. Charts have many optional parameters: type, title, labels, multiple datasets, legend, axis labels, grid lines.
The naive approach:
function createChart(
type: string,
title?: string,
labels?: string[],
data?: number[],
data2?: number[],
color?: string,
color2?: string,
showLegend?: boolean,
yAxisLabel?: string
) {
// 9+ parameters, most optional, order matters
}
// Calling is awkward
createChart("bar", "Revenue", months, revenue, expenses, "#3b82f6", "#ef4444", true);
Too many parameters. Adding a second dataset requires passing all previous ones. Easy to mess up order.
The Solutionβ
Build the chart step by step with a fluent API:
import { createBuilder } from "@pithos/core/eidos/builder/builder";
// TS infers all step names β typos are compile errors
const chartBuilder = createBuilder({
type: "bar",
title: "",
labels: [],
datasets: [],
})
.step("type", (s, type: "bar" | "line") => ({ ...s, type }))
.step("title", (s, title: string) => ({ ...s, title }))
.step("labels", (s, labels: string[]) => ({ ...s, labels }))
.step("data", (s, data: number[], color: string, label: string) => ({
...s,
datasets: [{ label, data, color }],
}))
// Key builder feature: composing with previous state
.step("addDataset", (s, label: string, data: number[], color: string) => ({
...s,
datasets: [...s.datasets, { label, data, color }],
}))
.step("legend", (s, show: boolean) => ({ ...s, showLegend: show }))
.done();
// Fluent, readable, composable
const chart = chartBuilder()
.type("bar")
.title("Monthly Revenue")
.labels(["Jan", "Feb", "Mar", "Apr", "May", "Jun"])
.data([120, 340, 220, 510, 480, 390], "#3b82f6", "Revenue")
.addDataset("Expenses", [80, 150, 120, 200, 180, 160], "#ef4444")
.legend(true)
.build();
Each step is named. Order is flexible. .addDataset() composes with previous data β a simple options object couldn't do this.
Live Demoβ
Toggle each builder step and watch the chart update in real-time. Notice how .addDataset() stacks on top of the first dataset β that's the builder composing state internally.
- builder.ts
- Usage
/**
* Chart builder using the Builder pattern.
*
* Fluent API: createChart().type("bar").title("...").labels([...]).data({...}).build()
* The key insight: .addDataset() composes with previous state β something
* a simple options object can't do.
*/
import { createBuilder } from "@pithos/core/eidos/builder/builder";
import type { ChartConfig, Dataset } from "./types";
const defaultConfig: ChartConfig = {
type: "bar",
title: "",
labels: [],
datasets: [],
showLegend: false,
yAxisLabel: "",
gridLines: true,
};
export const chartBuilder = createBuilder(defaultConfig)
.step("type", (state, type: "bar" | "line") => ({ ...state, type }))
.step("title", (state, title: string) => ({ ...state, title }))
.step("labels", (state, labels: string[]) => ({ ...state, labels }))
.step("data", (state, dataset: Dataset) => ({ ...state, datasets: [dataset] }))
.step("addDataset", (state, dataset: Dataset) => ({ ...state, datasets: [...state.datasets, dataset] }))
.step("legend", (state, show: boolean) => ({ ...state, showLegend: show }))
.step("yAxis", (state, label: string) => ({ ...state, yAxisLabel: label }))
.step("gridLines", (state, show: boolean) => ({ ...state, gridLines: show }))
.done();
export const createChart = () => chartBuilder();
import { useState, useMemo, useCallback } from "react";
import { createChart } from "@/lib/builder";
import { INITIAL_STEPS, MONTHS, REVENUE_DATA, EXPENSES_DATA } from "@/data/charts";
import type { ChartConfig } from "@/lib/types";
export function useChartBuilder() {
const [steps, setSteps] = useState(INITIAL_STEPS);
const [chartType, setChartType] = useState<"bar" | "line">("bar");
const toggleStep = useCallback((id: string) => {
setSteps((prev) => prev.map((s) => (s.id === id ? { ...s, enabled: !s.enabled } : s)));
}, []);
const isEnabled = useCallback((id: string) => steps.find((s) => s.id === id)?.enabled ?? false, [steps]);
const chartConfig = useMemo((): ChartConfig => {
let builder = createChart().type(chartType);
if (isEnabled("title")) builder = builder.title("Monthly Revenue");
if (isEnabled("labels")) builder = builder.labels(MONTHS);
if (isEnabled("data")) builder = builder.data({ label: "Revenue", data: REVENUE_DATA, color: "#3b82f6" });
if (isEnabled("addDataset") && isEnabled("data")) builder = builder.addDataset({ label: "Expenses", data: EXPENSES_DATA, color: "#ef4444" });
if (isEnabled("legend")) builder = builder.legend(true);
if (isEnabled("yAxis")) builder = builder.yAxis("Amount ($)");
return builder.build();
}, [steps, chartType, isEnabled]);
const codePreview = useMemo(() => {
const lines = [`const chart = createChart()\n .type("${chartType}")`];
if (isEnabled("title")) lines.push(' .title("Monthly Revenue")');
if (isEnabled("labels")) lines.push(" .labels([...])");
if (isEnabled("data")) lines.push(' .data({ label: "Revenue", data: [...], color: "#3b82f6" })');
if (isEnabled("addDataset") && isEnabled("data")) lines.push(' .addDataset({ label: "Expenses", data: [...], color: "#ef4444" })');
if (isEnabled("legend")) lines.push(" .legend(true)");
if (isEnabled("yAxis")) lines.push(' .yAxis("Amount ($)")');
lines.push(" .build();");
return lines.join("\n");
}, [steps, chartType, isEnabled]);
return { steps, chartType, setChartType, toggleStep, isEnabled, chartConfig, codePreview };
}
Real-World Analogyβ
Ordering a custom pizza. You start with dough, add sauce, choose cheese, pick toppings. Each step is independent. You can add toppings in any order. The final .build() is putting it in the oven.
When to Use Itβ
- Objects have many optional parameters
- Construction involves multiple steps that compose
- You want a fluent, readable API
- Same process should create different configurations
When NOT to Use Itβ
If your object can be fully described by a single options object with no composable steps, prefer that. Builder adds ceremony that's only worth it when steps compose or when construction is genuinely sequential.
APIβ
- createBuilder β Create an immutable fluent builder
- createValidatedBuilder β Builder with validation on each step