Builder Pattern — FixIt Pro Series #03
Stop passing 12 arguments to a constructor. Learn how the Builder pattern assembles complex JobCards step by step with optional fields, validation, and readable client code in C# and TypeScript.
Series: Design Patterns with FixIt Pro · Episode 03 / 22 · Creational Pattern
Previous: #02 — Abstract Factory Pattern
The Scenario
A FixIt Pro job card has grown. What started as a title and a category now needs:
- Job title and category
- Priority level (Normal, High, Urgent)
- Scheduled date and time slot
- List of required parts and materials
- Photo attachments
- Special instructions
- Whether the job requires a safety inspection
- Whether the client has a warranty That's a lot. And not all fields apply to every job. A routine carpentry fix won't have safety inspection requirements. An emergency plumbing callout won't have a pre-scheduled time slot.
The naive solution is a constructor with every possible parameter:
var card = new JobCard(
"Burst pipe", "Plumbing", "URGENT",
null, null, new List<string>(),
true, false, null
);
This is unreadable, error-prone, and gets worse with every new field. The Builder pattern solves this.
What Is the Builder Pattern?
Separate the construction of a complex object from its representation, so the same construction process can create different representations.
Instead of one overloaded constructor, you get a fluent step-by-step API:
var card = new JobCardBuilder("Burst pipe in kitchen", "Plumbing")
.WithPriority("URGENT")
.WithParts("pipe wrench", "replacement elbow joint", "plumber's tape")
.RequiresSafetyInspection()
.Build();
Readable. Explicit. Only the fields you need.
The four participants
| Role | FixIt Pro equivalent |
|---|---|
| Product | JobCard — the complex object being built |
| Builder Interface | IJobCardBuilder — defines the building steps |
| Concrete Builder | JobCardBuilder — implements the steps, holds state |
| Director (optional) | JobCardDirector — encodes common build recipes |
The Director is optional but useful — it lets you define pre-set configurations like "Standard Plumbing Job" or "Emergency Electrical Job" without repeating builder chains throughout your codebase.
C# Implementation
// ── Product ────────────────────────────────────────────────
public class JobCard
{
public string JobId { get; init; } = Guid.NewGuid().ToString()[..8];
public string Title { get; init; } = string.Empty;
public string Category { get; init; } = string.Empty;
public string Priority { get; init; } = "NORMAL";
public DateTime? ScheduledAt { get; init; }
public List<string> Parts { get; init; } = new();
public List<string> PhotoUrls { get; init; } = new();
public string? SpecialInstructions { get; init; }
public bool RequiresInspection { get; init; }
public bool HasWarranty { get; init; }
public void PrintSummary()
{
Console.WriteLine($"\n=== Job Card #{JobId} ===");
Console.WriteLine($" Title : {Title}");
Console.WriteLine($" Category : {Category}");
Console.WriteLine($" Priority : {Priority}");
if (ScheduledAt.HasValue)
Console.WriteLine($" Scheduled: {ScheduledAt:dd MMM yyyy HH:mm}");
if (Parts.Any())
Console.WriteLine($" Parts : {string.Join(", ", Parts)}");
if (!string.IsNullOrEmpty(SpecialInstructions))
Console.WriteLine($" Notes : {SpecialInstructions}");
Console.WriteLine($" Inspection Required : {RequiresInspection}");
Console.WriteLine($" Warranty : {HasWarranty}");
}
}
// ── Builder Interface ──────────────────────────────────────
public interface IJobCardBuilder
{
IJobCardBuilder WithPriority(string priority);
IJobCardBuilder ScheduledFor(DateTime dateTime);
IJobCardBuilder WithParts(params string[] parts);
IJobCardBuilder WithPhotos(params string[] urls);
IJobCardBuilder WithInstructions(string instructions);
IJobCardBuilder RequiresSafetyInspection();
IJobCardBuilder WithWarranty();
JobCard Build();
}
// ── Concrete Builder ───────────────────────────────────────
public class JobCardBuilder : IJobCardBuilder
{
private readonly string _title;
private readonly string _category;
private string _priority = "NORMAL";
private DateTime? _scheduledAt;
private List<string> _parts = new();
private List<string> _photoUrls = new();
private string? _instructions;
private bool _requiresInspection;
private bool _hasWarranty;
public JobCardBuilder(string title, string category)
{
_title = title;
_category = category;
}
public IJobCardBuilder WithPriority(string priority)
{
_priority = priority;
return this;
}
public IJobCardBuilder ScheduledFor(DateTime dateTime)
{
_scheduledAt = dateTime;
return this;
}
public IJobCardBuilder WithParts(params string[] parts)
{
_parts.AddRange(parts);
return this;
}
public IJobCardBuilder WithPhotos(params string[] urls)
{
_photoUrls.AddRange(urls);
return this;
}
public IJobCardBuilder WithInstructions(string instructions)
{
_instructions = instructions;
return this;
}
public IJobCardBuilder RequiresSafetyInspection()
{
_requiresInspection = true;
return this;
}
public IJobCardBuilder WithWarranty()
{
_hasWarranty = true;
return this;
}
public JobCard Build()
{
if (string.IsNullOrWhiteSpace(_title))
throw new InvalidOperationException("Job card must have a title.");
return new JobCard
{
Title = _title,
Category = _category,
Priority = _priority,
ScheduledAt = _scheduledAt,
Parts = _parts,
PhotoUrls = _photoUrls,
SpecialInstructions = _instructions,
RequiresInspection = _requiresInspection,
HasWarranty = _hasWarranty,
};
}
}
// ── Director (optional) ────────────────────────────────────
public class JobCardDirector
{
private readonly IJobCardBuilder _builder;
public JobCardDirector(IJobCardBuilder builder)
{
_builder = builder;
}
// Pre-set recipe: emergency callout
public JobCard BuildEmergencyJob(string title, string category) =>
new JobCardBuilder(title, category)
.WithPriority("URGENT")
.RequiresSafetyInspection()
.WithInstructions("Emergency callout — contact homeowner immediately on arrival.")
.Build();
// Pre-set recipe: standard scheduled job
public JobCard BuildScheduledJob(string title, string category, DateTime slot) =>
new JobCardBuilder(title, category)
.WithPriority("NORMAL")
.ScheduledFor(slot)
.Build();
}
// ── Client Code ────────────────────────────────────────────
class Program
{
static void Main()
{
// Manual build — full control
var card1 = new JobCardBuilder("Replace geyser element", "Plumbing")
.WithPriority("HIGH")
.ScheduledFor(new DateTime(2025, 5, 3, 9, 0, 0))
.WithParts("heating element", "thermostat", "sealing tape")
.WithWarranty()
.WithInstructions("Client is elderly — call 30 min before arrival.")
.Build();
card1.PrintSummary();
// Director build — emergency preset
var director = new JobCardDirector(new JobCardBuilder("", ""));
var card2 = director.BuildEmergencyJob("Burst pipe in kitchen", "Plumbing");
card2.PrintSummary();
}
}
Output:
=== Job Card #a3f91c2b ===
Title : Replace geyser element
Category : Plumbing
Priority : HIGH
Scheduled: 03 May 2025 09:00
Parts : heating element, thermostat, sealing tape
Notes : Client is elderly — call 30 min before arrival.
Inspection Required : False
Warranty : True
=== Job Card #d7e02b14 ===
Title : Burst pipe in kitchen
Category : Plumbing
Priority : URGENT
Notes : Emergency callout — contact homeowner immediately on arrival.
Inspection Required : True
Warranty : False
TypeScript Implementation
// ── Product ────────────────────────────────────────────────
const shortId = () => Math.random().toString(36).slice(2, 10);
class JobCard {
readonly jobId: string = shortId();
readonly title: string;
readonly category: string;
readonly priority: string;
readonly scheduledAt?: Date;
readonly parts: string[];
readonly photoUrls: string[];
readonly specialInstructions?: string;
readonly requiresInspection: boolean;
readonly hasWarranty: boolean;
constructor(config: {
title: string;
category: string;
priority: string;
scheduledAt?: Date;
parts: string[];
photoUrls: string[];
specialInstructions?: string;
requiresInspection: boolean;
hasWarranty: boolean;
}) {
this.title = config.title;
this.category = config.category;
this.priority = config.priority;
this.scheduledAt = config.scheduledAt;
this.parts = config.parts;
this.photoUrls = config.photoUrls;
this.specialInstructions = config.specialInstructions;
this.requiresInspection = config.requiresInspection;
this.hasWarranty = config.hasWarranty;
}
printSummary(): void {
console.log(`\n=== Job Card #${this.jobId} ===`);
console.log(` Title : ${this.title}`);
console.log(` Category : ${this.category}`);
console.log(` Priority : ${this.priority}`);
if (this.scheduledAt)
console.log(` Scheduled: ${this.scheduledAt.toLocaleString()}`);
if (this.parts.length)
console.log(` Parts : ${this.parts.join(", ")}`);
if (this.specialInstructions)
console.log(` Notes : ${this.specialInstructions}`);
console.log(` Inspection Required : ${this.requiresInspection}`);
console.log(` Warranty : ${this.hasWarranty}`);
}
}
// ── Builder Interface ──────────────────────────────────────
interface IJobCardBuilder {
withPriority(priority: string): IJobCardBuilder;
scheduledFor(date: Date): IJobCardBuilder;
withParts(...parts: string[]): IJobCardBuilder;
withPhotos(...urls: string[]): IJobCardBuilder;
withInstructions(text: string): IJobCardBuilder;
requiresSafetyInspection(): IJobCardBuilder;
withWarranty(): IJobCardBuilder;
build(): JobCard;
}
// ── Concrete Builder ───────────────────────────────────────
class JobCardBuilder implements IJobCardBuilder {
private _priority = "NORMAL";
private _scheduledAt?: Date;
private _parts: string[] = [];
private _photoUrls: string[] = [];
private _instructions?: string;
private _requiresInspection = false;
private _hasWarranty = false;
constructor(
private readonly _title: string,
private readonly _category: string
) {}
withPriority(priority: string): IJobCardBuilder { this._priority = priority; return this; }
scheduledFor(date: Date): IJobCardBuilder { this._scheduledAt = date; return this; }
withParts(...parts: string[]): IJobCardBuilder { this._parts.push(...parts); return this; }
withPhotos(...urls: string[]): IJobCardBuilder { this._photoUrls.push(...urls); return this; }
withInstructions(text: string): IJobCardBuilder { this._instructions = text; return this; }
requiresSafetyInspection(): IJobCardBuilder { this._requiresInspection = true; return this; }
withWarranty(): IJobCardBuilder { this._hasWarranty = true; return this; }
build(): JobCard {
if (!this._title.trim())
throw new Error("Job card must have a title.");
return new JobCard({
title: this._title,
category: this._category,
priority: this._priority,
scheduledAt: this._scheduledAt,
parts: this._parts,
photoUrls: this._photoUrls,
specialInstructions: this._instructions,
requiresInspection: this._requiresInspection,
hasWarranty: this._hasWarranty,
});
}
}
// ── Director (optional) ────────────────────────────────────
class JobCardDirector {
buildEmergencyJob(title: string, category: string): JobCard {
return new JobCardBuilder(title, category)
.withPriority("URGENT")
.requiresSafetyInspection()
.withInstructions("Emergency callout — contact homeowner immediately on arrival.")
.build();
}
buildScheduledJob(title: string, category: string, slot: Date): JobCard {
return new JobCardBuilder(title, category)
.withPriority("NORMAL")
.scheduledFor(slot)
.build();
}
}
// ── Client Code ────────────────────────────────────────────
const card1 = new JobCardBuilder("Replace geyser element", "Plumbing")
.withPriority("HIGH")
.scheduledFor(new Date("2025-05-03T09:00:00"))
.withParts("heating element", "thermostat", "sealing tape")
.withWarranty()
.withInstructions("Client is elderly — call 30 min before arrival.")
.build();
card1.printSummary();
const director = new JobCardDirector();
const card2 = director.buildEmergencyJob("Burst pipe in kitchen", "Plumbing");
card2.printSummary();
C# vs TypeScript — Key Differences
| Aspect | C# | TypeScript |
|---|---|---|
| Immutable product fields | init accessor on properties |
readonly keyword |
| Optional fields | DateTime? nullable type |
scheduledAt?: Date optional property |
| Variadic params | params string[] parts |
...parts: string[] rest parameters |
| Config object in constructor | Not idiomatic — use init properties |
Common pattern — pass a typed config object |
Validation in Build() |
throw new InvalidOperationException(...) |
throw new Error(...) |
| Fluent return type | IJobCardBuilder interface |
IJobCardBuilder interface (same) |
One notable difference: TypeScript developers often skip the Builder pattern in favour of a plain options object passed to a constructor. The Builder is still valuable when construction involves validation logic, multi-step sequencing, or reusable Director recipes — but it's worth knowing the tradeoff exists.
Builder vs Constructor Overloading vs Options Object
| Approach | Readable? | Validates? | Reusable recipes? |
|---|---|---|---|
| Overloaded constructors | ❌ Gets messy fast | ❌ | ❌ |
| Options object | ✅ | ⚠️ Only if you add it | ❌ |
| Builder pattern | ✅ | ✅ In Build() |
✅ Via Director |
Use the Builder when your object has more than 4–5 optional fields, or when invalid combinations need to be caught at construction time.
When to Use the Builder
Use it when:
- Your object has many optional or conditionally required fields
- Some field combinations are invalid and should be caught early
- You want pre-set construction recipes (use the Director)
- You want readable, self-documenting object creation at the call site Avoid it when:
- The object is simple — a plain constructor or options object is cleaner
- Fields are all required — there's nothing to optionally chain
Real-World Takeaway
You've already used the Builder pattern. In .NET, WebApplication.CreateBuilder() in ASP.NET Core is a Director-style builder. StringBuilder is the classic textbook example. In JavaScript, libraries like knex (query builder) and supertest (HTTP test builder) use the exact same fluent chaining pattern.
In FixIt Pro, the Builder means a dispatcher can construct a simple routine job card in two lines, and a complex emergency job card in eight — with validation built in, and Director recipes ready for the most common cases. No 12-argument constructor in sight.
Source Code
github.com/antonlungameni/fixit-pro-design-patterns
fixit-pro-design-patterns/
├── csharp/Creational/03-Builder/
└── typescript/Creational/03-Builder/
Previous: #02 — Abstract Factory Pattern
Next up: #04 — Prototype Pattern
Cloning a recurring job card template — monthly boiler service, weekly inspection — without re-running the full build chain every time.