Design Patterns & LLD

Creational Patterns: Factory, Builder, Singleton and Friends

Factories, builder, the module singleton and its traps, prototype, and dependency injection as the practical replacement.

Lesson 0120 min read#creational-patterns#singleton#dependency-injection

On this page

  1. Words used in this lesson
  2. Factory: let one place decide what to build
  3. Builder: many options, one valid result
  4. Singleton: one instance, and why people argue
  5. Prototype: copy instead of build
  6. Dependency injection: the practical replacement
  7. Which one do I pick?
  8. Interview questions
  9. Key takeaways
  10. What's next

In one line: creational patterns answer one question, "who builds this object, and how?". Use a factory when the kind of object depends on input, a builder when there are many options, a prototype when copying is easier than building, a singleton almost never (a plain module or an injected object is better), and dependency injection as the everyday way to hand objects to the code that needs them.

Lesson 0 gave you the routine and SOLID. Now come the patterns. A design pattern is a named, common shape of solution to a problem that keeps coming back. The names matter in an interview, because "I would use a factory here" tells the interviewer a lot in four words. The classic catalogue (the book "Design Patterns", 1994) sorts patterns into creational, structural and behavioural. This lesson covers the first group. Lesson 2 covers structural and Lesson 3 covers behavioural patterns.

Read Lesson 0 first for SOLID, because the "why" of every pattern below is one of those five rules.

What I ran / checked. I ran every TypeScript block with Node 22.22.0 (node --experimental-transform-types) and pasted the real output. All files also pass tsc --strict (TypeScript 7.0.2). The singleton module test used three real files and a dynamic import(). The pattern names and the 1994 book are general knowledge; I quote no text from it. I could not open the popular "design patterns for humans" repository on GitHub from this sandbox, so descriptions are my own. I also checked structuredClone and the JSON copy trick in a one-line Node run. What I did not test: a Next.js or other dev server that reloads modules, so my remark on that is from common experience and marked as such.

Words used in this lesson

  • A creational pattern is a pattern about how objects get created.
  • A factory is code whose job is to create objects, so callers do not use new on concrete classes.
  • A product is the thing a factory returns. A family is a set of products that must match (all light-theme or all dark-theme).
  • A builder is an object that collects options step by step and then produces the final object.
  • Fluent means each method returns this so calls chain: a().b().c().
  • A singleton is a class (or module) with exactly one instance for the whole program.
  • Global state is data any code can read and change from anywhere.
  • A prototype (the pattern) is a ready object that you copy to make new ones. (JavaScript also has a built-in "prototype chain"; it is related but not the same idea.)
  • A shallow copy copies the top level only. A deep copy copies nested objects too.
  • Dependency injection (DI) means a class receives what it needs from outside. A composition root is the one place at the start of the program where you build and connect all the objects.

Factory: let one place decide what to build

Simple factory

The problem: caller code says new EmailNotifier() in ten places. Adding SMS means editing ten places. A simple factory is just a function that takes a kind and returns the right object behind one interface. It is not in the 1994 catalogue, but it is the most used form.

JavaScript
interface Notifier { send(to: string, msg: string): string; }class EmailNotifier implements Notifier { send(to: string, msg: string) { return `email to ${to}: ${msg}`; } }class SmsNotifier implements Notifier { send(to: string, msg: string) { return `sms to ${to}: ${msg}`; } }// Simple factory: one place that knows how to build each kind.type Kind = "email" | "sms";function createNotifier(kind: Kind): Notifier {  switch (kind) {    case "email": return new EmailNotifier();    case "sms": return new SmsNotifier();    default: { const _never: never = kind; throw new Error("unknown kind " + _never); }  }}console.log(createNotifier("sms").send("555-0100", "hi"));// Registry factory: add a kind without editing the factory (open/closed).const registry = new Map<string, () => Notifier>([  ["email", () => new EmailNotifier()],  ["sms", () => new SmsNotifier()],]);registry.set("push", () => ({ send: (to, msg) => `push to ${to}: ${msg}` }));function create(kind: string): Notifier {  const make = registry.get(kind);  if (!make) throw new Error("unknown notifier: " + kind);  return make();}console.log(create("push").send("device-9", "hi"));try { create("fax"); } catch (e) { console.log((e as Error).message); }

Output:

TEXT
sms to 555-0100: hipush to device-9: hiunknown notifier: fax

Two versions are shown. The switch one has a never check, so TypeScript fails to compile if you add a new Kind and forget a case. The registry one is a Map from name to a function that builds the object. Adding push took one line and no edit of create. That is the open/closed idea from Lesson 0. The cost: names are plain strings, so a typo is found at run time (the fax error).

Factory Method

The classic Factory Method is about inheritance. A parent class owns the steps of an algorithm and leaves one step, "create the object", to the child.

JavaScript
// Factory Method: the parent has the steps, the child chooses what to create.interface Writer { write(line: string): string; }class CsvWriter implements Writer { write(line: string) { return line.split(" ").join(","); } }class JsonWriter implements Writer { write(line: string) { return JSON.stringify(line.split(" ")); } }abstract class Exporter {  protected abstract createWriter(): Writer;           // the factory method  export(rows: string[]): string[] {    const w = this.createWriter();    return rows.map(r => w.write(r));  }}class CsvExporter extends Exporter { protected createWriter() { return new CsvWriter(); } }class JsonExporter extends Exporter { protected createWriter() { return new JsonWriter(); } }const rows = ["ann 30", "bob 25"];console.log(new CsvExporter().export(rows));console.log(new JsonExporter().export(rows));

Output:

TEXT
[ 'ann,30', 'bob,25' ][ '["ann","30"]', '["bob","25"]' ]

Exporter.export is written once. CsvExporter and JsonExporter only choose the writer. In modern TypeScript I often use a plain function parameter instead (pass a createWriter function), which is the same idea without a class tree. Name the pattern, then show the simpler version.

Abstract Factory

When objects come in families that must match, one factory makes the whole family. Mixing a dark button with a light input is a bug that the factory makes impossible.

JavaScript
// Abstract Factory: one factory makes a whole family that must match.interface Button { render(label: string): string; }interface Input { render(name: string): string; }interface ThemeFactory { button(): Button; input(): Input; }const light: ThemeFactory = {  button: () => ({ render: l => `[ ${l} ]` }),  input: () => ({ render: n => `${n}: ______` }),};const dark: ThemeFactory = {  button: () => ({ render: l => `[# ${l} #]` }),  input: () => ({ render: n => `${n}: ######` }),};function loginForm(theme: ThemeFactory): string[] {  return [theme.input().render("user"), theme.input().render("pass"), theme.button().render("Login")];}console.log(loginForm(light));console.log(loginForm(dark));

Output:

TEXT
[ 'user: ______', 'pass: ______', '[ Login ]' ][ 'user: ######', 'pass: ######', '[# Login #]' ]

loginForm never says "light" or "dark". It only asks the theme. A new theme is one new factory. A real-life use is a family of database objects (connection, query builder, migrations) for one vendor. Be careful: adding a new product type (a Checkbox) means touching every factory. That is the main trade-off.

PatternUse it whenWatch out for
Simple factoryKind depends on input; one place should decideA growing switch (use a registry)
Factory MethodA fixed algorithm, one step creates a varying objectClass trees; a function parameter is often enough
Abstract FactorySeveral objects must come from the same familyAdding a new product touches all factories

Builder: many options, one valid result

A constructor with seven parameters is hard to read (new Req("POST", url, h, b, 2000, true, false)). A builder names each option and checks all the rules in one build() call.

JavaScript
class HttpRequest {  constructor(    readonly method: string,    readonly url: string,    readonly headers: Readonly<Record<string, string>>,    readonly body: string | undefined,    readonly timeoutMs: number,  ) {}}class RequestBuilder {  private method = "GET";  private headers: Record<string, string> = {};  private body?: string;  private timeoutMs = 5000;  constructor(private url: string) {}  post() { this.method = "POST"; return this; }  header(k: string, v: string) { this.headers[k] = v; return this; }  json(data: unknown) {    this.body = JSON.stringify(data);    return this.header("content-type", "application/json");  }  timeout(ms: number) { this.timeoutMs = ms; return this; }  build(): HttpRequest {                       // all rules checked in one place    if (this.method === "GET" && this.body) throw new Error("GET cannot have a body");    if (this.timeoutMs <= 0) throw new Error("timeout must be positive");    return new HttpRequest(this.method, this.url, { ...this.headers }, this.body, this.timeoutMs);  }}const req = new RequestBuilder("https://api.example.com/users").post().json({ name: "Ann" }).timeout(2000).build();console.log(req);try { new RequestBuilder("https://x.test").json({}).build(); }catch (e) { console.log("error:", (e as Error).message); }// In TypeScript an options object is often enough for simple cases:function makeRequest(url: string, opts: { method?: string; timeoutMs?: number } = {}) {  return { url, method: opts.method ?? "GET", timeoutMs: opts.timeoutMs ?? 5000 };}console.log(makeRequest("https://x.test", { timeoutMs: 100 }));

Output:

TEXT
HttpRequest {  method: 'POST',  url: 'https://api.example.com/users',  headers: { 'content-type': 'application/json' },  body: '{"name":"Ann"}',  timeoutMs: 2000}error: GET cannot have a body{ url: 'https://x.test', method: 'GET', timeoutMs: 100 }

Things to notice. The HttpRequest is immutable (readonly), so after build() nobody can break it. The rules (a GET has no body) live in build(), so a half-built object cannot escape. At the end you see the honest alternative: in TypeScript an options object with defaults is often enough, and it is much shorter. I use a builder when there are rules between options, many steps, or when the build has an order. For a plain list of optional settings I use an options object.

You may know the telescoping constructor problem: many constructors with more and more parameters. Named options (an object) and builders both solve it. In an interview, say which you pick and why.

Singleton: one instance, and why people argue

A singleton guarantees one instance and gives global access to it. The classic form hides the constructor:

JavaScript
// The classic form: private constructor + getInstance().class Registry {  private static instance: Registry | undefined;  private items = new Map<string, string>();  private constructor() { console.log("Registry created"); }  static get(): Registry { return (Registry.instance ??= new Registry()); }  set(k: string, v: string) { this.items.set(k, v); }  size() { return this.items.size; }}Registry.get().set("a", "1");Registry.get().set("b", "2");console.log(Registry.get() === Registry.get(), Registry.get().size());

Output:

TEXT
Registry createdtrue 2

It works: Registry created printed once and both calls return the same object. In Node and TypeScript you rarely need this class. An ES module is already a singleton: Node runs a module file once and then reuses the exported values.

JavaScript
// config.ts: a module-level singleton. The module is evaluated once and cached.console.log("config.ts loaded");export const config = { calls: 0, env: "dev" };
JavaScript
import { config } from "./config.ts";export const touchA = () => ++config.calls;
JavaScript
import { config } from "./config.ts";import { touchA } from "./a.ts";touchA();touchA();console.log("same object, calls =", config.calls);// The trap: global state leaks between "tests".function testOne() { config.env = "prod"; }function testTwo() { return config.env === "dev"; }testOne();console.log("testTwo sees a clean config:", testTwo());// Another module URL makes another instance (this can happen with duplicate packages).const copyUrl = "./config.ts?copy=2";const other = await import(copyUrl);console.log("other copy is a different object:", other.config !== config, other.config.calls);

Output:

TEXT
config.ts loadedsame object, calls = 2testTwo sees a clean config: falseconfig.ts loadedother copy is a different object: true 0

Read the output in three steps.

  1. config.ts loaded printed once, and touchA and main shared the same object (calls = 2). So a module-level export const is the cheap singleton.
  2. The testTwo line shows the danger. testOne changed config.env, and testTwo now sees prod. State leaks between tests and between callers. That is global state.
  3. Loading the same file with another URL (?copy=2) created a second instance (config.ts loaded again, and calls is 0). With Node and ES modules, "one instance" really means "one instance per resolved module URL". Duplicate copies of a package in node_modules, or a dev server that reloads modules, can also give you two. (I did not run a dev server, so treat that part as experience, not a test.)

Why it is controversial:

ProblemWhat goes wrong
Hidden dependencyA function uses config without saying so in its signature
Hard to testTests share state; you cannot give each test a fresh copy
Hard to change"Only one" becomes "one per tenant" and every caller breaks
Not truly oneDuplicate module copies; many processes; many servers

A singleton is one process only. If you run four Node processes or four servers, you have four "singletons". Anything that must be unique across the whole system (a counter, a lock, a cache) belongs in a shared store, not in an object in memory. For the bigger picture of why state in memory does not scale across servers see System Design, Lesson 7.

When one instance is fine: a read-only config loaded at start, a logger, a connection pool. Even then, create it once in the composition root and pass it in. You get one instance and keep testability. The dependency injection section later in this lesson shows how.

Prototype: copy instead of build

Use a prototype when building from scratch is costly or fiddly, and a copy of a ready object is easier. The danger is the shallow copy.

JavaScript
interface Doc { title: string; tags: string[]; }const template: Doc = { title: "Report", tags: ["draft"] };const shallow = { ...template };            // copies the top level onlyshallow.tags.push("shared");console.log("template tags after shallow copy edit:", template.tags);const deep = structuredClone(template);     // copies the nested array toodeep.tags.push("private");console.log("template tags after deep copy edit:", template.tags, "| deep:", deep.tags);// Prototype pattern as a method: an object knows how to copy itself.class Shape {  constructor(public color: string, public points: number[][]) {}  clone(): Shape { return new Shape(this.color, this.points.map(p => [...p])); }}const s1 = new Shape("red", [[0, 0], [1, 1]]);const s2 = s1.clone();s2.color = "blue";s2.points[0][0] = 9;console.log(s1.color, s1.points[0], "|", s2.color, s2.points[0]);// JavaScript's own prototype chain: Object.create links to a prototype object.const base = { greet() { return "hi " + this.name; } } as { name?: string; greet(): string };const ann = Object.create(base) as typeof base;ann.name = "Ann";console.log(ann.greet(), Object.getPrototypeOf(ann) === base);

Output:

TEXT
template tags after shallow copy edit: [ 'draft', 'shared' ]template tags after deep copy edit: [ 'draft', 'shared' ] | deep: [ 'draft', 'shared', 'private' ]red [ 0, 0 ] | blue [ 9, 0 ]hi Ann true

Read the first two lines. Spreading { ...template } copied the top level, but tags is still the same array, so editing the "copy" changed the template (shared appeared in the template). structuredClone (built into Node 17+ and modern browsers) made a real deep copy: the private tag appears only in the clone. In a quick check, structuredClone threw a DataCloneError for an object holding a function, and a cloned class instance came back as a plain object without its methods. So for class instances give the class its own clone(), as Shape does. The Object.create lines show the JavaScript built-in: an object can use another object as its prototype and look things up there. Interviewers sometimes use "prototype" to test whether you mix up the pattern and the language feature.

Dependency injection: the practical replacement

Most of the time, when someone reaches for a singleton, they want "this code needs the logger/database/clock". Dependency injection gives it to them as a constructor argument. No global, no framework.

JavaScript
interface Clock { now(): number; }interface UserRepo { save(name: string): number; }class SignupService {                              // depends on interfaces only  constructor(private repo: UserRepo, private clock: Clock) {}  register(name: string) {    const id = this.repo.save(name);    return `user ${id} (${name}) at t=${this.clock.now()}`;  }}// Production partsclass MemoryRepo implements UserRepo {  private next = 1;  save(_name: string) { return this.next++; }}const realClock: Clock = { now: () => 1_700_000_000 };// Composition root: the ONE place that builds the object graph.function createApp() {  return { signup: new SignupService(new MemoryRepo(), realClock) };}console.log(createApp().signup.register("ann"));// Test: swap parts, no globals touched.const fixed: Clock = { now: () => 42 };const stubRepo: UserRepo = { save: () => 7 };console.log(new SignupService(stubRepo, fixed).register("bob"));

Output:

TEXT
user 1 (ann) at t=1700000000user 7 (bob) at t=42

SignupService knows two small interfaces and nothing about where they come from. createApp is the composition root: the one function that builds the real graph. The test passes a stub repo and a fixed clock and gets a repeatable answer (t=42) without touching any global. A full DI container (like those in NestJS or Angular) only automates the wiring. In a small LLD answer, passing arguments is enough, and it is the right default. Lesson 4 shows how Express and React already use this idea.

Which one do I pick?

You see thisReach for
new X() for many classes in many files, kind chosen at run timeFactory (registry form)
Objects of a matching set (theme, vendor)Abstract Factory
Many options, rules between themBuilder (or an options object)
"There must be one"Create one in the composition root and inject it
Big template object to duplicatePrototype (structuredClone or clone())
Code needs a serviceConstructor injection

Interview questions

What is the difference between Factory Method and Abstract Factory? (Must know)

Factory Method is one overridable step that creates one product, usually inside a parent class (createWriter). Abstract Factory is an object that creates a family of related products that must match (button() and input() of one theme). The first uses inheritance, the second is usually an interface with several implementations.

When would you use a factory instead of new? (Must know)

When the concrete class depends on input or configuration, or when I want callers to depend on an interface only. One place then knows how to build things, and adding a new kind is a small change. If there is only one class and it never varies, plain new is simpler, and I do not add a factory.

Follow-up: "How do you stop the factory's switch from growing?" Use a registry: a Map of name to builder function. New kinds register themselves. The cost is that names become strings, so I validate them and throw a clear error for unknown names.

When is a builder better than an options object? (Senior signal)

When there are rules between options (a GET has no body), when steps have an order, or when I want an immutable result that cannot exist half-built. For a list of optional settings with defaults, an options object is shorter and just as clear. In TypeScript, I choose the options object first.

Implement a singleton in TypeScript. (Must know)

Two ways. The classic: a private constructor, a private static field, and getInstance() using ??=. The simpler: export one object from a module, because Node evaluates a module once and caches it. I then say what is wrong with both: global state, hidden dependencies, hard tests. I add that the better design is to create one instance in the composition root and inject it.

Why are singletons controversial? (Must know)

Four reasons. They hide dependencies (a function uses them without saying so). They make tests share state. They are hard to change when "one" becomes "one per tenant". And they are not really one: several processes, servers, or duplicate module copies each have their own. I showed this in the demo: a second URL for the same file created a second instance.

Is a module in Node a singleton? (Senior signal)

Practically yes: Node loads a module once per resolved path and caches it. But "one" means one per resolved module URL, per process. A different URL (?copy=2 in my test), a duplicated package version, or another process gives another instance. So I never rely on it for something that must be unique across a system.

What is the difference between a shallow and a deep copy, and how do you deep copy in JavaScript? (Must know)

A shallow copy duplicates the top-level fields, so nested objects are shared. { ...obj } and Object.assign are shallow. For a deep copy I use structuredClone, which handles nested objects, arrays, dates, maps and sets but not functions. For a class with behaviour, I write a clone() method. A JSON.parse(JSON.stringify(x)) trick also exists, but it drops undefined, functions and turns dates into strings.

How is the Prototype pattern related to JavaScript prototypes? (Trap)

The names match, but the ideas differ. The pattern is "copy a ready object to make a new one". The language feature is that an object can inherit from another object through a prototype link. Object.create(base) uses that feature. A clone() method uses the pattern. Mixing them up is a common slip.

What is dependency injection, and do you need a framework? (Must know)

It means a class receives its dependencies from outside, usually by constructor. I build the real objects once in a composition root and pass them down. For tests I pass fakes. A framework or container only automates the wiring, and it is useful in a large app. In a small design or an interview, constructor arguments are enough and easier to read.

Constructor injection, setter injection or a service locator? (Senior signal)

Constructor injection by default: the object is complete when it exists, and dependencies are visible. Setter injection is for optional parts, but it allows half-built objects. A service locator (ask a global registry for what you need) hides dependencies again, so it is a singleton in disguise. I avoid it.

You need a "payment provider" chosen by country. How do you design it? (Senior signal)

A PaymentProvider interface (charge, refund), one class per provider, and a registry factory keyed by country or provider id. Business code asks the factory once, then talks to the interface. I put retries, timeouts and logging in wrappers (the decorator pattern, Lesson 2), not inside each provider. A new provider means a new class and one registry line.

Where do you see over-use of creational patterns? (Trap)

A factory that only calls new on one class, a builder for an object with two fields, and a singleton for something that could be a function argument. Each adds a name to learn and a place to look. I ask what change the pattern makes cheap. If I cannot name one, I use plain code.

Key takeaways

  • Creational patterns answer "who builds this, and how". Pick the smallest one that fixes a real pain.
  • Factory: one place decides the class. A registry keeps it open for new kinds. Abstract Factory keeps a family consistent.
  • Builder: for options with rules between them. In TypeScript, an options object often wins for simple cases.
  • Singleton: a module already is one, per process and per module URL. The danger is hidden global state. Prefer one instance built in the composition root and injected.
  • Prototype: copy with structuredClone or a clone() method, and remember that spread is shallow.
  • Dependency injection is the everyday tool. A container is optional.

What's next

Next: Structural patterns: Adapter, Decorator, Facade, Proxy and Composite, Lesson 2. Practise: pick a class of yours that calls new on a concrete dependency, give it an interface, and pass the dependency through the constructor.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series