09 Jan 2026
If you're not familiar with the first two points, this blog might not be suitable for you. Otherwise, you're good to go. The examples are intentionally simple and can be easily replicated in other languages.
Hey devs, welcome back to the design pattern series 👋
In the previous post, we explored the Abstract Factory pattern, where we defined booking families such as Standard, Holiday, and Corporate bookings. Each family encapsulates business rules like pricing strategies, discount policies, and benefits.
In this post, we move one level deeper.
Once a booking family is chosen, we still need to construct an actual booking such as a Room, Cab, Flight, or Movie booking. This is where the Builder pattern fits naturally.
Builder is a creational design pattern that constructs complex objects step by step. It allows you to create different representations of an object using the same construction process.
Instead of forcing all configuration into a large constructor, Builder separates how an object is built from what the object represents.
In our booking system, a booking is not a simple object. A Room booking, for example, can have:
Required parameters
Optional parameters

If we rely on constructors alone, we quickly run into the below problems:
undefined for optional valuesLet's look at what happens without a proper pattern:
class RoomBooking {
constructor(
public customer: Customer,
public room: Room,
public stay: TimeSlot,
public addOns?: string[],
public insurance?: Insurance,
public promoCode?: string,
public specialRequests?: string,
public paymentMethod?: PaymentMethod,
public notificationPreference?: NotificationPreference
) {}
}
const booking = new RoomBooking(
customer,
room,
stay,
undefined,
undefined,
"SUMMER25"
);
The constructor becomes heavy as we add more optional parameters. The client code gets messy with undefined values, and it's hard to tell what each parameter means without looking at the constructor definition.
You might try to solve this with method overloading or default parameters, but that approach has limits
// Using method overloading
class Booking {
// method signatures
constructor(customer: Customer, resource: Resource, timeSlot: TimeSlot);
constructor(customer: Customer, resource: Resource, timeSlot: TimeSlot, addOs?: string[]);
constructor(customer: Customer, resource: Resource, timeSlot: TimeSlot, addOs?: string[], insurance?: Insurance);
// And so on...
constructor(customer: Customer, resource: Resource, timeSlot: TimeSlot, addOns?: string[], insurance?: Insurance, /* more params */) {
// Implementation
}
}
// Using default parameters
class Booking {
constructor(
public customer: Customer,
public resource: Resource,
public timeSlot: TimeSlot,
public addOns: string[] = [],
public insurance?: Insurance,
public promoCode: string = ""
// And so on...
) {}
}
These approaches don't scale well with many optional parameters and don't provide good readability when instantiating those objects.
The Builder pattern says
Extract the object construction code out of its own class and move it to separate objects called builders.
In our Booking System, we can apply this pattern by
BookingBuilder class dedicated to constructing Booking objectsbuild() method that constructs and returns the complete object
How this can fit with our existing structure of Abstract Factory
These two patterns solve different problems and work together without overlap.

RoomBooking)
Booking objectRoomBookingBuilder provides implementations for each construction stepbuild() method that returns a fully constructed RoomBooking instanceNote: In our system, the booking family (Standard, Holiday, Corporate) is selected earlier using the Abstract Factory pattern.
The Builder pattern shown here focuses purely on constructing a specific booking type (Room booking) step by step.

reset, buildStepA, buildStepB, buildStepZ) without knowing the final product details. This ensures the construction algorithm is independent of concrete implementations.Product1, Product2). Each builder decides how the construction steps are applied and exposes a getResult() method to return the final object.You might be confusing the two class diagrams. They are not meant to be identical. Both diagrams are only intended to provide a high-level, class-level understanding of the pattern. The actual implementation will always vary based on the specific requirements and use case.
// Product: RoomBooking
class RoomBooking {
public customer: Customer;
public room: Room;
public stay: TimeSlot;
public addOns: string[];
public insurance?: Insurance;
public promoCode?: string;
public specialRequests?: string;
public paymentMethod?: PaymentMethod;
public notificationPreference?: NotificationPreference;
constructor(builder: RoomBookingBuilder) {
this.customer = builder.customer;
this.room = builder.room;
this.stay = builder.stay;
this.addOns = builder.addOns;
this.insurance = builder.insurance;
this.promoCode = builder.promoCode;
this.specialRequests = builder.specialRequests;
this.paymentMethod = builder.paymentMethod;
this.notificationPreference = builder.notificationPreference;
}
}
The RoomBooking class does not expose a complex constructor. Instead, it relies on the Builder to provide a valid and complete instance.
class RoomBookingBuilder {
// Required
public customer: Customer;
public room: Room;
public stay: TimeSlot;
// Optional
public addOns: string[] = [];
public insurance?: Insurance;
public promoCode?: string;
public specialRequests?: string;
public paymentMethod?: PaymentMethod;
public notificationPreference?: NotificationPreference;
constructor(customer: Customer, room: Room, stay: TimeSlot) {
this.customer = customer;
this.room = room;
this.stay = stay;
}
withAddOns(addOns: string[]): this {
this.addOns = addOns;
return this;
}
withInsurance(insurance: Insurance): this {
this.insurance = insurance;
return this;
}
withPromoCode(code: string): this {
this.promoCode = code;
return this;
}
withSpecialRequests(requests: string): this {
this.specialRequests = requests;
return this;
}
withPaymentMethod(method: PaymentMethod): this {
this.paymentMethod = method;
return this;
}
withNotificationPreference(pref: NotificationPreference): this {
this.notificationPreference = pref;
return this;
}
build(): RoomBooking {
// Validation before construction
if (this.addOns.length > 0 && !this.paymentMethod) {
throw new Error("Payment method is required when add-ons are selected");
}
return new RoomBooking(this);
}
}
This builder:
The Director is optional and represents predefined booking flows.
// Director class (optional)
class RoomBookingDirector {
createBasicBooking(builder: RoomBookingBuilder): RoomBooking {
return builder.build();
}
createPremiumBooking(builder: RoomBookingBuilder): RoomBooking {
return builder
.withAddOns(["breakfast", "spa", "airport transfer"])
.withInsurance(new PremiumInsurance())
.withPaymentMethod(new CreditCardPayment())
.build();
}
createPromotionalBooking(builder: BookingBuilder, promoCode: string): RoomBooking {
return builder
.withPromoCode(promoCode)
.build();
}
}
In many modern systems, this orchestration logic lives in a service layer instead of a dedicated Director class.
Finally the client code
// Client code
function main() {
const customer = new Customer("John Doe");
const room = new Room("Deluxe Suite");
const stay = new TimeSlot(new Date(), 3); // 3-day stay
// Using the builder directly
const booking1 = new RoomBookingBuilder(customer, room, stay)
.withPromoCode("SUMMER25")
.withSpecialRequests("Room with ocean view")
.withPaymentMethod(...)
.build();
// Using the director for common configurations
const director = new BookingDirector();
const builder = new BookingBuilder(customer, room, timeSlot);
const simpleBooking = director.createSimpleBooking(builder);
const premiumBooking = director.createPremiumBooking(
new BookingBuilder(customer, room, timeSlot)
);
const promoBooking = director.createPromotionalBooking(
new BookingBuilder(customer, room, timeSlot),
"FLASH50"
);
}
main();
The code is now much more readable, maintainable, and expressive. You can see clearly what parameters are being set and what they represent.
And remember... the Builder pattern is great for complex objects, but don't overuse it!

RoomBookingBuilder helps create customizable bookings with add-ons, insurance, and promo codes in a clean, readable wayOk devs, that's it for today. I tried my best to explain this pattern. In the next post, we'll explore another pattern in our Design Pattern series. If you have any queries or suggestions, please feel free to reach out to me.
Code, learn, refactor, repeat