Library
9 min read

Factory vs Abstract Factory vs Builder

Published

All three creational patterns do the same favour for the caller: it stops writing new SomeClass(...) itself. So they blur. What separates them is the question each one answers about the object being made.

  • Factory: which class? The restaurant counter from the LLD course. You say "one masala dosa", and the kitchen decides which tawa, which cook, which batter.
  • Abstract Factory: which matching set? The school uniform shop. Buy the shirt, the trousers and the tie from one school's shop and they always match. Buy them from three shops and one day the tie is from the wrong school.
  • Builder: how do I put together one object with many optional parts? The chaat counter. "One pani puri, medium spicy, no onion, extra sev." You name the parts one by one, then it is handed over finished.
Your answer

Your app sells in India and the UAE. Each order needs a money formatter, a tax calculator and an invoice template, and the three must always be for the same country. Which pattern fits, and what bug does it prevent that the others would not?

One tab each#

Each tab makes the one thing that sets the pattern apart something you can operate. In the middle tab, try to build an Indian invoice with UAE tax.

Three ways to make objects
const n = createNotifier('sms')
// n is a SmsNotifier
// the factory knows it needs the DLT entity id, the sender id and the template id
// the 23 files that call createNotifier know none of that
Factory answers one question: which class? The caller names a kind, gets one ready object, and never sees a constructor.

Factory: the caller names a kind, gets one object#

ts
interface Notifier {
  send(to: string, text: string): string
}
 
class SmsNotifier implements Notifier {
  constructor(private entityId: string) {}
  send(to: string, text: string) {
    return `SMS ${to} [${this.entityId}]: ${text}`
  }
}
class EmailNotifier implements Notifier {
  constructor(private from: string) {}
  send(to: string, text: string) {
    return `Email ${to} from ${this.from}: ${text}`
  }
}
 
type Channel = 'sms' | 'email'
 
function createNotifier(channel: Channel): Notifier {
  switch (channel) {
    case 'sms':
      return new SmsNotifier('DLT-1107')
    case 'email':
      return new EmailNotifier('orders@example.com')
  }
}
 
const n = createNotifier('sms')
console.log(n.send('98100', 'Order 42 confirmed'))
// > SMS 98100 [DLT-1107]: Order 42 confirmed

The callers know a channel name and the Notifier interface. Which class that means, and what its constructor needs, lives in one function. When the regulator adds a field to every SMS, one line changes. That is the cold open of the factory chapter, where the same constructor was called from 23 files.

What is shown here is a simple factory, a function with a switch. The textbook Factory Method pattern moves the choice into a method that subclasses override. In TypeScript you will write the simple form far more often.

Abstract Factory: one family makes every part#

A factory picks one class. An abstract factory makes several related objects and guarantees they belong together, because they all come from the same family object.

ts
interface Money {
  format(amount: number): string
}
interface Tax {
  label: string
  percent: number
}
interface RegionKit {
  money(): Money
  tax(): Tax
}
 
const india: RegionKit = {
  money: () => ({ format: (a) => `₹${a}` }),
  tax: () => ({ label: 'GST', percent: 18 }),
}
const uae: RegionKit = {
  money: () => ({ format: (a) => `AED ${a}` }),
  tax: () => ({ label: 'VAT', percent: 5 }),
}
 
function invoice(kit: RegionKit, price: number) {
  const tax = kit.tax()
  const total = price + (price * tax.percent) / 100
  return `${tax.label} ${tax.percent}%, total ${kit.money().format(total)}`
}
 
console.log(invoice(india, 2000)) // GST 18%, total ₹2360
console.log(invoice(uae, 2000)) // VAT 5%, total AED 2100

invoice never sees a country. It is handed one kit, and every part comes from that kit, so an invoice in rupees with UAE VAT cannot be assembled. With three separate factories it can, and each part would pass its own unit tests. The classic textbook version is a UI kit: a dark theme factory that makes a dark button, a dark checkbox and a dark text field, so a light checkbox never ends up on a dark screen.

Builder: one object, many optional parts#

The builder is not about which class. There is exactly one, Order. The problem is that the constructor has six arguments, four of them optional or boolean, and a call to it cannot be read:

ts
class Order {
  constructor(
    readonly dish: string,
    readonly quantity: number,
    readonly spicy: boolean,
    readonly noOnion: boolean,
    readonly contactless: boolean,
    readonly note?: string,
  ) {}
}
 
class OrderBuilder {
  private qty = 1
  private hot = false
  private onionless = false
  private noContact = false
  private text?: string
 
  constructor(private dish: string) {}
 
  quantity(n: number): this {
    this.qty = n
    return this
  }
  spicy(): this {
    this.hot = true
    return this
  }
  noOnion(): this {
    this.onionless = true
    return this
  }
  note(text: string): this {
    this.text = text
    return this
  }
  build() {
    if (this.qty < 1) throw new Error('quantity must be at least 1')
    return new Order(this.dish, this.qty, this.hot,
      this.onionless, this.noContact, this.text)
  }
}
 
// Which true is which? Swap two of them and it still compiles.
const a = new Order('Masala dosa', 2, false, true, false)
 
const b = new OrderBuilder('Masala dosa').quantity(2).noOnion().build()
console.log(a.noOnion, b.noOnion) // true true
console.log(b.spicy) // false

Each step says what it sets, the order of steps does not matter, and build() is the one place that checks the result is valid before anyone can use it.

In TypeScript, an options object often replaces it#

JavaScript and TypeScript have named fields built in. An object argument gets you most of what a builder gives, with no extra class:

ts
interface OrderOptions {
  dish: string
  quantity?: number
  spicy?: boolean
  noOnion?: boolean
  note?: string
}
 
function createOrder(o: OrderOptions) {
  return { quantity: 1, spicy: false, noOnion: false, ...o }
}
 
const order = createOrder({ dish: 'Masala dosa', noOnion: true })
console.log(order.quantity, order.noOnion) // 1 true
 
createOrder({ dish: 'Vada', spicey: true })
// Error: Object literal may only specify known properties

Reach for a real builder when the object is put together in steps across different parts of the code, when the steps have rules between them (no contactless() on a dine in order), or when the chain reads like a sentence. Query builders such as Knex are the famous case: db('orders').where('status', 'paid').orderBy('created_at').limit(20).

Side by side#

FactoryAbstract FactoryBuilder
The questionWhich class?Which matching set?How to assemble one complex object?
MakesOne objectSeveral related objectsOne object
The caller passesA kind ('sms')A family (india)Parts, one step at a time
Bug it preventsnew spread across 23 filesParts from different families mixedUnreadable, swappable constructor arguments
Lighter TS formA function with a switchAn object of factory functionsAn options object

They also combine. An abstract factory's methods are often simple factories, and a factory can use a builder inside to put together what it returns.

A factory for a class with one implementation

createLogger() that only ever returns new Logger() is a function with a longer name. A factory earns its place when there is a real choice to hide (several classes, per environment config) or a constructor that many callers would otherwise get wrong. Until then, new is fine.

ChoiceWhat you gainWhat you payPick it when
FactoryOne place knows which class to build and how to configure it.Another indirection to read, and a switch that grows with every new kind.Several implementations behind one interface, chosen by a name or config.
Abstract FactoryRelated objects always come from the same family. A mismatch cannot be built.Adding a new kind of part means adding it to every family.Regions, themes, platforms, test vs production: sets of parts that must agree.
BuilderReadable construction, validation in one place, any order of steps.A second class to keep in sync with the object it builds.Many optional parts, rules between them, or construction spread over several steps.
TypeScript you just picked up
type Channel = 'sms' | 'email'
Only these two strings are allowed. A switch that covers both lets TypeScript know the function always returns.
quantity(n: number): this
Returns the builder itself, so calls can be chained with dots.
readonly note?: string
Optional and cannot be reassigned. The ? means it may be undefined.
{ quantity: 1, ...o }
Spread: copy every field of o over the defaults. Later fields win.
Object literal may only specify known properties
The typo check you get for free with an options object: spicey is not a field of OrderOptions.

In the interview#

Interviewer
What is the difference between Factory, Abstract Factory and Builder?
You
They answer different questions. A factory decides which class to create: give it a channel name, get back the right notifier. An abstract factory creates a family of related objects that must be used together, like a region kit that makes the money formatter, tax rule and invoice template for one country, so they cannot be mixed. A builder is about one object with many optional parts: you set them step by step with named methods and build validates the result. Factory and abstract factory are about which, builder is about how.
One question per pattern, an example each, then a one line summary.
Interviewer
Would you write a builder in TypeScript?
You
Less often than in Java. An options object with optional fields gives me named arguments, defaults and typo checks for free. I would write a builder when construction happens in steps across the code, when there are rules between the steps, or for something like a query builder where the fluent chain is the interface.
Knows the language-specific lighter form and when the full pattern still wins.
Which one?
createStorage(env) returns S3Storage in production and DiskStorage on a laptop
A test kit that makes a fake clock, fake payments and fake SMS that all share one fake timeline
An HTTP request put together with .url(), .header(), .header(), .timeout(), .build()
A dark theme object with button(), checkbox() and input() methods

Checkpoint#

Checkpoint

1. What can an abstract factory prevent that three separate factories cannot?

2. A class has one implementation and a constructor with eight optional settings. Which pattern addresses the real problem?

3. In TypeScript, what does an options object give you that a long positional constructor does not?

Worth memorising
  • Factory: which class? One object, chosen by a name. The restaurant counter.
  • Abstract Factory: which matching set? Several objects that must agree. The uniform shop.
  • Builder: how to assemble one complex object? Named steps, then build. The chaat counter.
  • Factory and abstract factory hide which. Builder hides how.
  • In TypeScript, an options object replaces most builders.
Say this in 60 seconds

All three creational patterns keep new out of the caller, and each answers a different question. A factory answers which class: the caller passes a name like sms and gets one ready object behind an interface, so the constructor details live in one place. An abstract factory answers which matching set: one family object makes several related parts, like the money formatter, tax rule and invoice template for one country, so parts from different families cannot be combined. A builder answers how to put together one object with many optional parts: named steps in any order, and a build method that validates. Factory and abstract factory are about which, builder is about how. In TypeScript a simple factory is a function with a switch over a union type, and an options object with optional fields replaces most builders, while a real builder still wins for step by step construction with rules, like a query builder.