Facade
Placing an order took four calls, and the sequence mattered: reserve the stock, charge the customer, send the ticket to the kitchen, assign a rider. The mobile team got it right. The web team charged first and reserved second. About 40 customers a week paid for a dish that had sold out a few seconds earlier, and support refunded each one by hand.
Both teams had the same four classes and the same documentation. Only one had read it.
Placing an order requires calling Inventory, Payments, Kitchen and Riders in a specific order, with stock released if the payment fails. Three client teams need to do this. How do you make it impossible for one of them to get the order wrong?
The idea
You dial 9 from your room and say “towels, dinner at eight, and a taxi at six tomorrow”. You did not call housekeeping, the kitchen and the travel desk separately, and you do not know their extension numbers. The desk knows who does what and in which order. The departments still exist. You have been given one number to deal with all of them.
Before: every client is an expert
class Inventory {
reserve(dish: string): string {
return `reserved ${dish}`
}
}
class Payments {
charge(amount: number): string {
return `charged ${amount}`
}
}
class Kitchen {
ticket(dish: string): string {
return `kitchen ticket for ${dish}`
}
}
class Riders {
assign(address: string): string {
return `rider assigned to ${address}`
}
}
// The web client's version. Spot the bug.
const inventory = new Inventory()
const payments = new Payments()
const steps = [payments.charge(120), inventory.reserve('Masala dosa')]
console.log(steps.join(', ')) // charged 120, reserved Masala dosaIt ran, it printed, and it is wrong: money was taken before stock was confirmed. Every client has to know four classes and the one correct order to call them in. That knowledge is copied into each client, and a copy can drift.
After: one method that knows
class Inventory {
reserve(dish: string): string {
return `reserved ${dish}`
}
}
class Payments {
charge(amount: number): string {
return `charged ${amount}`
}
}
class Kitchen {
ticket(dish: string): string {
return `kitchen ticket for ${dish}`
}
}
class Riders {
assign(address: string): string {
return `rider assigned to ${address}`
}
}
class OrderFacade {
constructor(
private readonly inventory: Inventory,
private readonly payments: Payments,
private readonly kitchen: Kitchen,
private readonly riders: Riders,
) {}
placeOrder(dish: string, amount: number, address: string): string[] {
return [
this.inventory.reserve(dish),
this.payments.charge(amount),
this.kitchen.ticket(dish),
this.riders.assign(address),
]
}
}
const orders = new OrderFacade(
new Inventory(),
new Payments(),
new Kitchen(),
new Riders(),
)
for (const step of orders.placeOrder('Masala dosa', 120, 'HSR Layout')) {
console.log(step)
}
// > reserved Masala dosa
// > charged 120
// > kitchen ticket for Masala dosa
// > rider assigned to HSR LayoutThe sequence is written once, inside placeOrder. A client calls one method with three
values. It cannot charge before reserving, because it never calls either of them.
When the business adds a fifth step, say a fraud check before the charge, it goes into
placeOrder and every client has it the same day.
What a facade is, and is not
A facade adds no new ability. Everything placeOrder does, a client could do by hand.
It exists to make the common task short and hard to get wrong.
It also does not lock anything away. The four classes are still there, and an admin tool
that needs to release stock manually can call Inventory directly. A facade is a
convenient door. It is not a wall.
You already use several:
- A
fetchwrapper in a frontend that adds the auth header, parses JSON and maps errors. - The service layer in a web backend, where a controller calls
orderService.place()and never sees the repositories. - Any cloud SDK. A single
upload(file)call is a facade over signing, chunking, retrying and several HTTP requests.
Handling the failure path
The facade is also the right home for the “what if step two fails” logic, which is where hand written client code goes wrong most often.
class Inventory {
reserve(dish: string): string {
return `reserved ${dish}`
}
release(dish: string): string {
return `released ${dish}`
}
}
class Payments {
charge(amount: number): string {
if (amount > 5000) throw new Error('Card declined')
return `charged ${amount}`
}
}
class OrderFacade {
constructor(
private readonly inventory: Inventory,
private readonly payments: Payments,
) {}
placeOrder(dish: string, amount: number): string[] {
const log = [this.inventory.reserve(dish)]
try {
log.push(this.payments.charge(amount))
} catch {
log.push(this.inventory.release(dish))
}
return log
}
}
const orders = new OrderFacade(new Inventory(), new Payments())
console.log(orders.placeOrder('Family feast', 9000).join(', '))
// > reserved Family feast, released Family feastThe payment failed, so the stock went back. Three client teams did not each have to remember that.
A facade grows. OrderFacade gets cancelOrder, then refund, then updateAddress,
then exportMonthlyReport, and two years on it has 45 methods and every team edits it.
That is the single responsibility problem
again. Keep a facade to one use case or one closely related group, and add a second
facade when a second group appears.
A facade coordinates. It decides the order of calls and what to undo on failure. The
rule for how much to charge belongs in Payments, and the rule for what counts as in
stock belongs in Inventory. If you find if statements about prices inside the
facade, a subsystem is missing a method.
Both sit in front of other code. An adapter makes one existing class fit an interface the caller already has, and it changes nothing about how much work the caller does. A facade defines a new interface, usually smaller, over several classes, so the caller does less. Adapter is about compatibility. Facade is about simplicity.
constructor(private readonly inventory: Inventory, ...)placeOrder(...): string[]const log = [this.inventory.reserve(dish)]steps.join(', ')try { ... } catch { ... }Checkpoint
1. After OrderFacade exists, can an admin tool still call Inventory.release() directly?
2. What is the main benefit of putting the reserve, charge, ticket, assign sequence in one facade method?
3. OrderFacade has grown to 45 methods covering orders, refunds, reports and address changes. What is the fix?
A facade is one simple entry point in front of a set of classes that are complicated to use together. Placing an order might need inventory, payments, the kitchen and rider assignment called in a specific order, with stock released if the payment fails. A facade puts that sequence in a single method, so each client makes one call and cannot get the order wrong, and a new step is added in one place. It adds no new capability and it does not hide the subsystems, they can still be used directly. The risks are that it grows into a god object, so I keep one facade per use case, and that business rules creep into it, when it should only coordinate. Compared with an adapter: an adapter makes one class fit an existing interface, a facade makes many classes easier to use.
