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 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.
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 thatFactory: the caller names a kind, gets one object#
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 confirmedThe 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.
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 2100invoice 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:
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) // falseEach 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:
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 propertiesReach 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#
| Factory | Abstract Factory | Builder | |
|---|---|---|---|
| The question | Which class? | Which matching set? | How to assemble one complex object? |
| Makes | One object | Several related objects | One object |
| The caller passes | A kind ('sms') | A family (india) | Parts, one step at a time |
| Bug it prevents | new spread across 23 files | Parts from different families mixed | Unreadable, swappable constructor arguments |
| Lighter TS form | A function with a switch | An object of factory functions | An 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.
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.
| Choice | What you gain | What you pay | Pick it when |
|---|---|---|---|
| Factory | One 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 Factory | Related 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. |
| Builder | Readable 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. |
type Channel = 'sms' | 'email'quantity(n: number): thisreadonly note?: string{ quantity: 1, ...o }Object literal may only specify known propertiesIn the interview#
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?
- 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.
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.