Creational Patterns
By the Syncfusion DevTutorials team · Last updated
Learn simple factory, factory method, builder and singleton with QuickBite examples, then replace an if-chain with a notifier factory and add WhatsApp.
Think about how a restaurant makes a dosa. The waiter does not go into the kitchen and cook it. Instead, they write "1 masala dosa" on an order slip and give it to the kitchen. The kitchen decides who will prepare it and how it will be made. The waiter simply receives the finished dosa.
So far, our code has often worked like a waiter who also cooks the food. Every place that needs an object directly creates it using new Card() or new SmsNotifier(). When the implementation changes, every place that creates those objects must also be updated.
Creational patterns provide common and proven ways to create objects, so the rest of the code does not need to know how those objects are built.
In simple words: creational patterns put the "which object should be created, and how should it be built" decision in one central place.
What You Will Learn
In this lesson, you will learn:
- Why creating objects with
neweverywhere can cause problems. - The simple factory: choose a class from one central location.
- The factory method: allow a subclass to decide which object to create.
- The builder: create a complex object step by step.
- The singleton: use one shared object across the application, and understand when it should not be used.
A design pattern is a named solution to a problem that keeps coming up. The names come from the 1994 "Gang of Four" book, and they let a team say "use a builder here" instead of explaining the whole solution.
The Problem With new Everywhere
Here is a payment choice written as an if-chain: if card, new Card(); else if upi, new Upi(); else if cash, new Cash(). It works: upi with 300 gives "upi 300", and cash with 120 gives "cash 120". But QuickBite has the same if-chain in checkout, in refunds and on the admin page. Watch what happens when a new payment type arrives:
The code behind the animation:
The problem: choosing a class with an if-chain
class Card { pay(n: number): string { return `card ${n}`; } }
class Upi { pay(n: number): string { return `upi ${n}`; } }
class Cash { pay(n: number): string { return `cash ${n}`; } }
// This choice is copied into checkout, refunds and the admin page.
function chargeAtCheckout(method: string, amount: number): string {
if (method === "card") return new Card().pay(amount);
else if (method === "upi") return new Upi().pay(amount);
else if (method === "cash") return new Cash().pay(amount);
throw new Error("unknown method");
}
console.log(chargeAtCheckout("upi", 300));
console.log(chargeAtCheckout("cash", 120));
Expected output:
upi 300
cash 120
Why It Hurts
When QuickBite adds a wallet payment option, developers must find and update all three if chains. If even one of them is missed, wallet payments may work in some places but fail in others, such as during refunds.
The solution is to make the object creation decision in a single place. This central place is called a factory.
The Simple Factory
A factory works like a restaurant kitchen. You place an order by name, and the kitchen gives you the finished dish. You do not need to know who prepared it or how it was made.
The Example
The factory keeps a table from name to "how to make it", called MAKERS. Callers only ever call createPayment(name). Step through three calls:
Here is the same idea in code:
A simple factory: one table, one place
interface PaymentMethod {
pay(amount: number): string;
}
class Card implements PaymentMethod { pay(n: number): string { return `card ${n}`; } }
class Upi implements PaymentMethod { pay(n: number): string { return `upi ${n}`; } }
class Cash implements PaymentMethod { pay(n: number): string { return `cash ${n}`; } }
const MAKERS: Record<string, () => PaymentMethod> = {
card: () => new Card(),
upi: () => new Upi(),
cash: () => new Cash(),
};
function createPayment(name: string): PaymentMethod {
const make = MAKERS[name];
if (!make) throw new Error("unknown payment method: " + name);
return make();
}
console.log(createPayment("upi").pay(300));
console.log(createPayment("card").pay(450));
try {
createPayment("cheque");
} catch (e) {
console.log((e as Error).message);
}
Expected output:
upi 300
card 450
unknown payment method: cheque
| Call | Result |
|---|---|
createPayment("upi").pay(300) | upi 300 |
createPayment("card").pay(450) | card 450 |
createPayment("cheque") | error: unknown payment method: cheque |
Why It Helps
To add a wallet payment method, you only need to create one new class and add one entry to the factory. The checkout system, refund system, and admin page can all use it automatically without any additional changes.
If an unknown payment name is provided, the factory can produce one clear error in a single place. Without a factory, every if chain would need to remember to handle unknown values.
The Factory Method
Sometimes the overall process stays the same, but one part of it changes. For example, QuickBite may use bike delivery in the city and drone delivery on a college campus. The order process is the same in both cases. Only the delivery method is different.
The factory method pattern keeps the shared process in a parent class. The parent class leaves one method undefined: the method responsible for creating the part that changes. Each child class provides its own implementation of that method.
The Example
OrderFlow is the parent class. Its place(dish) method holds the shared steps. It calls createDelivery(), which each child class fills in.
The factory method createDelivery() is filled in by each subclass
Now step through two orders, one in the city and one on campus:
The code behind the animation:
Factory method: the subclass decides what to create
interface Delivery {
eta(): string;
}
class BikeDelivery implements Delivery { eta(): string { return "30 min by bike"; } }
class DroneDelivery implements Delivery { eta(): string { return "12 min by drone"; } }
abstract class OrderFlow {
// The factory method. Each subclass fills it in.
protected abstract createDelivery(): Delivery;
// The shared steps never change.
place(dish: string): string {
const delivery = this.createDelivery();
return `${dish}: ${delivery.eta()}`;
}
}
class CityFlow extends OrderFlow {
protected createDelivery(): Delivery { return new BikeDelivery(); }
}
class CampusFlow extends OrderFlow {
protected createDelivery(): Delivery { return new DroneDelivery(); }
}
console.log(new CityFlow().place("Biryani"));
console.log(new CampusFlow().place("Sandwich"));
Expected output:
Biryani: 30 min by bike
Sandwich: 12 min by drone
The results are "Biryani: 30 min by bike" and "Sandwich: 12 min by drone". place() is written only once inside OrderFlow. It never directly creates a BikeDelivery object with new BikeDelivery(). Instead, it calls createDelivery() and lets the subclass decide which delivery type to create.
Easy way to tell them apart: a simple factory is a separate function that creates objects from an input value. A factory method is a method a subclass overrides to choose the object. Both keep new out of the calling code.
The Builder
Some objects contain many pieces of information. For example, an order may include a restaurant, several items, a coupon, a delivery note, and other details. A constructor with many parameters can be difficult to read and understand. It is often unclear what each value represents.
A builder works like filling out an order form. You provide one piece of information at a time, in any order you choose. The actual object is not created until you select "place order" by calling the build method. At that point, the builder validates the information and creates the final object.
The Example
Each builder method sets one part and returns the builder (this), so you can chain the calls: new OrderBuilder().from("Saravana").add("Idli", 2).add("Vada").withNote("less spicy").build(). Step through that chain:
Here is the same idea in code:
A builder: one step at a time, checked at the end
class Order {
constructor(
readonly restaurant: string,
readonly items: { name: string; qty: number }[],
readonly coupon: string,
readonly note: string,
) {}
}
class OrderBuilder {
private restaurant = "";
private items: { name: string; qty: number }[] = [];
private coupon = "";
private note = "";
from(restaurant: string): this { this.restaurant = restaurant; return this; }
add(name: string, qty = 1): this { this.items.push({ name, qty }); return this; }
withCoupon(code: string): this { this.coupon = code; return this; }
withNote(text: string): this { this.note = text; return this; }
build(): Order {
if (!this.restaurant) throw new Error("pick a restaurant first");
if (this.items.length === 0) throw new Error("add at least one item");
return new Order(this.restaurant, this.items, this.coupon, this.note);
}
}
const order = new OrderBuilder()
.from("Saravana")
.add("Idli", 2)
.add("Vada")
.withNote("less spicy")
.build();
console.log(order.restaurant, order.items.length, order.note);
Expected output:
Saravana 2 less spicy
The finished order is from Saravana, has 2 items and the note "less spicy". The chain reads like a sentence. You can skip the parts you do not need, like the coupon here.
Checking at the End
build() is the one place that checks the order is complete. An unfinished order is refused with a clear message. Watch three attempts at a Paradise biryani order:
The code behind the animation:
The builder refuses an unfinished order
class OrderBuilder {
private restaurant = "";
private items: string[] = [];
from(restaurant: string): this { this.restaurant = restaurant; return this; }
add(name: string): this { this.items.push(name); return this; }
build(): string {
if (!this.restaurant) throw new Error("pick a restaurant first");
if (this.items.length === 0) throw new Error("add at least one item");
return `${this.items.length} items from ${this.restaurant}`;
}
}
const attempts = [
() => new OrderBuilder().from("Paradise").add("Biryani").build(),
() => new OrderBuilder().add("Biryani").build(),
() => new OrderBuilder().from("Paradise").build(),
];
for (const attempt of attempts) {
try {
console.log("ok:", attempt());
} catch (e) {
console.log("refused:", (e as Error).message);
}
}
Expected output:
ok: 1 items from Paradise
refused: pick a restaurant first
refused: add at least one item
| Attempt | Result |
|---|---|
.from("Paradise").add("Biryani").build() | ok: 1 items from Paradise |
.add("Biryani").build() | refused: pick a restaurant first |
.from("Paradise").build() | refused: add at least one item |
Without a builder, every place that creates an order would need to remember and perform all of these validation checks on its own.
The Singleton
Some objects should exist only once in an application. For example, QuickBite may have a single set of application settings, such as the delivery fee. Having multiple copies with different values could cause serious problems.
A singleton is a class that allows only one instance to be created. Whenever code asks for that object, it receives the same shared instance.
The Example
The constructor is private, so no one can write new Settings(). The only way in is Settings.get(). Step through two calls to it:
Here is the same idea in code:
A singleton: one object for the whole app
class Settings {
private static only: Settings | null = null;
readonly deliveryFee = 30;
// private: no one outside can write new Settings()
private constructor() {
console.log("settings loaded");
}
static get(): Settings {
if (Settings.only === null) Settings.only = new Settings();
return Settings.only;
}
}
const a = Settings.get();
const b = Settings.get();
console.log(a === b);
console.log(b.deliveryFee);
Expected output:
settings loaded
true
30
"settings loaded" is printed only once. Both a and b refer to the same object, so a === b returns true, and b.deliveryFee is 30.
Be Careful With Singletons
A singleton is easy to access from anywhere in the application. However, that convenience can also create problems.
- Any class can start depending on it without making that dependency clear. You cannot see this dependency in the constructor.
- Tests share the same singleton instance. If one test changes its state, the next test may see those changes.
Before using a singleton, try this: create one Settings object at startup and pass it to the classes that need it. You still share one object, but each dependency is visible and tests can pass their own.
Lab: A Notifier Factory
QuickBite supports SMS, email, and push notifications. Currently, the notification type is selected using an if-chain inside NotificationService. Now QuickBite wants to support WhatsApp as well.
Predict the Output
Here is a singleton question in C#. Read the code, then type what it prints.
Common Mistakes
| Mistake | Wrong | Correct | Why it happens |
|---|---|---|---|
| Repeating the same creation logic | An if-chain based on the payment type appears in both checkout and refund code. | Use a single factory and reuse it everywhere. | Copying an existing if-chain often feels like the fastest solution. |
| Hiding errors inside the factory | Returning null when an unknown name is provided. | Throw a clear and meaningful error from the factory. | Returning null may seem safe until the caller tries to use it. |
| Builder without validation | build() creates an order even when it has no items. | build() should throw an error when a required value is missing. | Validation can feel like extra work during development. |
| Constructors with too many parameters | new Order("Saravana", items, "", "", 0, false, "") | Use a builder with clearly named steps. | New fields are often added one after another to the constructor. |
| Using singletons everywhere | Every service is implemented as a singleton. | Create one shared object at startup and pass it where needed. | Singletons are easy to access from any part of the application. |
Practice
What to Remember
- Creational patterns move the decision of which object to create and how to create it into one central place.
- Simple Factory: a name or type goes in, and a ready-to-use object comes out.
- Factory Method: the parent class defines the process, while a subclass decides what object to create.
- Builder: create an object one step at a time, then validate everything in
build(). - Singleton: ensures there is only one instance of an object for the entire application. Use it carefully, and prefer passing a shared object through constructors when possible.
Further Reading
These are the sources used to check this lesson:
- Refactoring.Guru: Factory Method: the factory method pattern with diagrams.
- Refactoring.Guru: Builder: step-by-step construction.
- Refactoring.Guru: Singleton: the pattern and its problems.
- TypeScript Handbook: Classes: private constructors, static members and abstract classes.
Next: Structural Patterns. You will learn how to fit classes together: adapters, decorators and facades.