Abstract Factory Pattern - Creating Families of Related Objects
21 Sept 2025
Table of contents
- Prerequisites
- Introduction
- What is Abstract Factory Pattern
- Solution
- Basic Implementation
- Pros and Cons
- When not to use it
- Conclusion
- Reference
Prerequisites
- Basic understanding of OOP concepts
- Basic understanding of UML diagrams
- Basic Typescript knowledge
These are the basic prerequisites for this blog. If you’re not familiar with first two points. This blog might not be suitable for you. Otherwise, you’re good to go! The examples are really simple and can be easily replicated in other languages.
Introduction
Hey devs, welcome back to the design pattern series! 👋
Last time, we looked at the Factory Method pattern. Today, we’re leveling up to its big sibling the Abstract Factory pattern. As always, we’re sticking with our Booking System example so everything feels connected.
What is Abstract Factory Pattern
Abstract Factory
Abstract Factory is a creational design pattern that lets you produce families of related objects without specifying their concrete classes.
The Problem
In our Booking system the customers can reserve a hotel room, theatre seat, or even a vehicle for a specific timeslot based on the availability status. For a simple booking flow we need three related things:
- Booking object (Resource + Time + Customer)
- Invoice (Payment information, Discounts, Taxes and etc.,)
- Template (Confirmation email / PDF design)
Let’s say we are currently support three booking themes
- Standard - normal flow, simple invoice, plain template.
- Holiday - festive discount rules, decorative template, invoice with promo codes.
- Corporate - corporate bulk booking rules, corporate invoice, corporate template.
Depending on the theme (Standard, Holiday, Corporate), the booking flow must work together as a family.
Initial Implementation
This is what many of us would write at first glance:
type Theme = "standard" | "holiday" | "corporate";
class BookingService {
createBooking(theme: Theme, resource: Resource, customer: Customer, timeSlot: Timestamp) {
// Booking
let booking;
if (resource.type === "room") {
booking = new RoomBooking(customer, timeSlot);
} else if (resource.type === "seat") {
booking = new SeatBooking(customer, timeSlot);
} else {
booking = new VehicleBooking(customer, timeSlot);
}
// Invoice
let invoice;
if (theme === "holiday") {
invoice = new HolidayInvoice(booking);
} else if (theme === "corporate"){
invoice = new CorporateInvoice(booking);
} else {
invoice = new StandardInvoice(booking);
}
// Template
let template;
if (theme === "holiday") {
template = new HolidayTemplate();
} else {
template = new StandardTemplate();
}
// Processing
booking.reserve();
invoice.generate();
template.render();
}
// other CRUD methods
}
It works without any errors believe me…. but you can already feel the cracks.
- Adding a new theme (Luxury, Student Discount) means editing this class everywhere.
BookingService
is struggling with too many responsibilities like what resource to book, what template to render and what invoice to generate. It should have the only responsibility of booking a resource for a specific timeslot without worrying about the theme- This code also breaks the Open/Closed Principle (for every new theme we’re modifying the entire code instead of extending).
How do we make sure that when someone books under the Holiday theme, they also get the correct Holiday invoice and Holiday template without these if (theme === "holiday")
checks everywhere?
Solution
The Abstract Factory pattern says:
Group related objects into families, and provide an interface for creating them without specifying their exact classes.
In our Booking System, a family is:
- Booking (reservation details)
- Invoice (billing)
- Template (customer-facing document style)
The key idea here is each family stays consistent. For example, a HolidayFactory always gives you a holiday booking, holiday invoice, and holiday template. A StandardFactory produces the standard versions of those same products.
Abstract Factory Diagram
Solution steps
- Define abstract products
- Interfaces for the family of products.
- Each defines the contract (methods/properties) but not the implementation.
- Define the abstract factory interface
- A
AbstractFactory
interface that declares set of related factory methods.createProductA(): ProductA
createProductB(): ProductB
createProductC(): ProductC
- A
- Create concrete factories by implementing the
AbstractFactory
ConcreteFactory1
ConcreteFactory2
- Client code
- Works only with the
AbstractFactory
interface. - Doesn’t care which concrete factory is being used. It just knows it will get a consistent family of products.
- Follows the Single Responsibility Principle and Open/Closed Principle
- Works only with the
Solution Diagram
This diagram highlights:
- The abstract products (
Booking
,Invoice
,Template
) at the left. - The abstract factory (
BookingFactory
) defining creation methods. - The concrete factories (
HolidayFactory
,StandardFactory
) that implement the abstract factory and ensure consistency across product families. - The
HolidayFactory
returns holiday-themedBooking
,Invoice
, andTemplate
,StandardBookingFactory
returns standard-themed ones andCorporateFactory
returns corporate-themedBooking
,Invoice
, andTemplate
- The client code
BookingService
, which depends only onBookingFactory
, and not on concrete classes.
Basic Implementation
// abstract products
interface Booking {
reserve(): void;
}
interface Invoice {
generate(): void;
}
interface Template {
render(): void;
}
// concrete products
class RoomBooking implements Booking {
constructor(private customer: Customer, private timeSlot: Timestamp) {}
reserve() {
// room booking logic
}
}
class SeatBooking implements Booking {
constructor(private customer: Customer, private timeSlot: Timestamp) {}
reserve() {
// seat booking logic
}
}
class VehicleBooking implements Booking {
constructor(private customer: Customer, private timeSlot: Timestamp) {}
reserve() {
// vehicle booking logic
}
}
class StandardInvoice implements Invoice {
constructor(private booking: Booking) {}
generate() {
// standard invoice logic
}
}
class HolidayInvoice implements Invoice {
constructor(private booking: Booking) {}
generate() {
// holiday invoice logic
}
}
class StandardTemplate implements Template {
render() {
// standard template logic
}
}
class HolidayTemplate implements Template {
render() {
// holiday template logic
}
}
// abstract factory
interface BookingFactory {
createBooking(resource: Resource, customer: Customer, timeSlot: Timestamp): Booking;
createInvoice(booking: Booking): Invoice;
createTemplate(): Template;
}
// concreate factories
class StandardFactory implements BookingFactory {
createBooking(resource: Resource, customer: Customer, timeSlot: Timestamp): Booking {
switch (resource.type) {
case "room": return new RoomBooking(customer, timeSlot);
case "seat": return new SeatBooking(customer, timeSlot);
case "vehicle": return new VehicleBooking(customer, timeSlot);
}
}
createInvoice(booking: Booking): Invoice {
return new StandardInvoice(booking);
}
createTemplate(): Template {
return new StandardTemplate();
}
}
class HolidayFactory implements BookingFactory {
createBooking(resource: Resource, customer: Customer, timeSlot: Timestamp): Booking {
switch (resource.type) {
case "room": return new HolidayRoomBooking(customer, timeSlot);
case "seat": return new HolidaySeatBooking(customer, timeSlot);
case "vehicle": return new HolidayVehicleBooking(customer, timeSlot);
}
}
createInvoice(booking: Booking): Invoice {
return new HolidayInvoice(booking);
}
createTemplate(): Template {
return new HolidayTemplate();
}
}
...
BookingService.ts
// Client code
class BookingService {
constructor(private factory: BookingFactory) {}
book(resource: Resource, customer: Customer, timeSlot: TimeStamp) {
const booking = this.factory.createBooking(resource, customer, timeSlot);
const invoice = this.factory.createInvoice(booking);
const template = this.factory.createTemplate();
booking.reserve();
invoice.generate();
template.render();
}
// other methods
}
index.ts
import { BookingService } from "./BookingService";
import { StandardFactory } from "./factories/StandardBookingFactory";
import { HolidayFactory } from "./factories/HolidayBookingFactory";
// Utility: check if given timestamp falls on weekend
function isWeekend(timestamp: number): boolean {
const date = new Date(timestamp);
const day = date.getDay();
return day === 0 || day === 6;
}
// Factory selector
function getBookingFactory(timestamp: number): BookingFactory {
if (isWeekend(timestamp)) {
return new HolidayBookingFactory();
} else {
return new StandardBookingFactory();
}
}
// Example usage
function main() {
const date = new Date().getTime();
const bookingService = new BookingService(getBookingFactory(date));
bookingService.book(resource, customer, date);
}
main();
Now the BookingService
doesn’t care about concrete classes at all. It just uses the abstract factory. The code using BookingService
simply books based on the date. It doesn’t need to worry about how the booking is created, which type of booking it is, or the underlying logic.
Instead, the focus remains only on when to book and who/what to book, while the factories handle the rest.
Pros and Cons
Pros
- Everything you pull out of the factory just works together no weird compatibility issues till it implements the base product interfaces.
- You’re not tightly coupling client code with specific product classes.
- It let’s you group related stuff (think Booking + Invoice + Template) as one consistent family.
- SRP and OCP friendly
- Super easy to plug in new product types when business asks for it, without touching old code.
- Great for testing and extension.
Cons
- Comes with extra boilerplate (factories, interfaces, etc.,).
- Can feel over-engineered if you only have one or two theme.
When not to use it
- In our example, Abstract Factory makes sense only if you expect to have several booking themes (Standard, Holiday, Corporate, maybe more in the future). But if you’re confident it’s just going to stay at Standard vs Holiday, then choosing Abstract Factory would make the code over-engineered. A clean conditional or a couple of direct classes will keep things much simpler.
- If only one piece of the flow varies — let’s say invoices change while bookings and templates stay the same — then Abstract Factory is the wrong tool. In that case, something like a Strategy for invoices will do the job neatly without forcing every part of the system into a family.
- More generally, avoid Abstract Factory when the variations are few, stable, or isolated.
- And remember… you might need an Abstract Factory to deal with family.
There's nothing stronger than a family - Vin Diesel :)
Conclusion
- Abstract Factory = a factory of factory methods.
- Perfect when you need families of related objects that should stick together.
- We saw how HolidayFactory and StandardFactory cleanly generate consistent bundles of Booking + Invoice + Template.
- Compared to Factory Method, Abstract Factory is one step higher instead of creating one object, you’re creating a family.
Woo huh! we did it. 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 are suggestions please feel free to reach me out.
Code, learn, refactor, repeat