Facade: Hiding Complex Subsystems Behind One Unified Interface
Starting from a checkout flow that has to wire together several subsystems, this piece breaks down how the Facade Pattern wraps messy internals behind one simplified interface, and how it differs from the Adapter Pattern.
What calling code looks like without a Facade
Let's start with a concrete example. Say we have four independent subsystems:
class InventoryService {
checkStock(productId, quantity) {
console.log(`檢查庫存:商品 ${productId},數量 ${quantity}`)
return true
}
}
class PaymentService {
charge(userId, amount) {
console.log(`向使用者 ${userId} 收款 ${amount} 元`)
return { success: true, transactionId: 'tx_001' }
}
}
class ShippingService {
arrangeDelivery(orderId, address) {
console.log(`安排訂單 ${orderId} 配送到 ${address}`)
}
}
class NotificationService {
sendOrderConfirmation(userId, orderId) {
console.log(`寄送訂單 ${orderId} 的確認信給使用者 ${userId}`)
}
}With no wrapping at all, calling code that wants to place an order has to know about all four services, know the order to call them in, and know what arguments each method expects:
const inventoryService = new InventoryService()
const paymentService = new PaymentService()
const shippingService = new ShippingService()
const notificationService = new NotificationService()
function placeOrderDirectly(userId, productId, quantity, amount, address) {
const inStock = inventoryService.checkStock(productId, quantity)
if (!inStock) {
throw new Error('庫存不足')
}
const payment = paymentService.charge(userId, amount)
if (!payment.success) {
throw new Error('付款失敗')
}
const orderId = `order_${Date.now()}`
shippingService.arrangeDelivery(orderId, address)
notificationService.sendOrderConfirmation(userId, orderId)
return orderId
}There's nothing wrong with this code by itself. But the moment a second place also needs to place an order — a support-team backoffice tool, a mobile app's checkout page — you're copying this whole block again. And the moment the order flow needs a new step or a reordered sequence, every copy has to be updated in lockstep.
Adding a Facade to absorb the complexity
The Facade Pattern's move here is straightforward: add a class that encapsulates "how these subsystems get wired together," and expose only one simple method to the outside world.
class OrderFacade {
#inventoryService
#paymentService
#shippingService
#notificationService
constructor() {
this.#inventoryService = new InventoryService()
this.#paymentService = new PaymentService()
this.#shippingService = new ShippingService()
this.#notificationService = new NotificationService()
}
placeOrder({ userId, productId, quantity, amount, address }) {
const inStock = this.#inventoryService.checkStock(productId, quantity)
if (!inStock) {
throw new Error('庫存不足')
}
const payment = this.#paymentService.charge(userId, amount)
if (!payment.success) {
throw new Error('付款失敗')
}
const orderId = `order_${Date.now()}`
this.#shippingService.arrangeDelivery(orderId, address)
this.#notificationService.sendOrderConfirmation(userId, orderId)
return orderId
}
}Now calling code just does this:
const orderFacade = new OrderFacade()
const orderId = orderFacade.placeOrder({
userId: 'user_01',
productId: 'product_99',
quantity: 1,
amount: 1200,
address: '台北市信義區',
})
console.log(`訂單建立完成:${orderId}`)The caller doesn't need to know there are four subsystems behind this, let alone the order they get called in — OrderFacade absorbs all of that. When the order flow needs to change later (say, adding an SMS notification step), you only touch the inside of OrderFacade. Every call site stays untouched.
A Facade doesn't hide the subsystems — it just stops forcing you to learn them
There's a common misconception worth calling out here: the Facade Pattern doesn't forbid calling code from touching the subsystems directly. It just offers an easier path. If some advanced case genuinely needs to bypass the Facade and work with PaymentService directly for custom payment logic, that's still fair game.
// 大多數情況:走 Facade,簡單省事
orderFacade.placeOrder({
userId: 'user_01',
productId: 'product_99',
quantity: 1,
amount: 1200,
address: '台北市信義區',
})
// 少數需要客製化的情況:仍然可以直接操作子系統
const paymentService = new PaymentService()
paymentService.charge('user_01', 500)What a Facade gives you is a shortcut for the common case, not a lockdown on what's underneath. That's a different goal from encapsulation, which fully privatizes internal state and shuts external access out entirely — a Facade is about simplifying access, not restricting it.
How Facade differs from Adapter
The Facade Pattern often gets mixed up with the Adapter Pattern — both operate on "interfaces," but they're solving different problems:
- Adapter: converts an interface so two otherwise-incompatible things can work together. Usually a one-to-one conversion — think translating a third-party library's response format into whatever shape your project uses internally.
- Facade: simplifies an interface by wrapping several complex subsystems behind one simple entry point. Usually a many-to-one convergence — the point isn't converting formats, it's shrinking how much the caller needs to understand.
Put simply: Adapter solves "these two things don't fit together." Facade solves "there are too many moving pieces here, and I don't want to learn every one of them."
When to reach for it, and when to be careful
The Facade Pattern is a good fit when:
- A single operation needs to coordinate multiple subsystems, and that coordination logic gets called from more than one place
- You want external modules to use a complex third-party library without touching its clunky raw API directly
- You want a clean boundary between layers — for instance, having the frontend talk to the backend only through a single API module, instead of hitting every underlying endpoint directly
But watch out for a common side effect: if you keep stuffing logic into the Facade, it can easily grow into a God Object that knows everything and does everything — which defeats the whole point of simplifying things. The better approach is to keep the Facade focused purely on coordination, and leave the actual business logic in the subsystems where it belongs.
Wrap-up
The core value of the Facade Pattern is putting a simplified interface in front of complex subsystems, so calling code doesn't need to understand every subsystem's details or rewrite the same coordination logic over and over. The difference from the Adapter Pattern comes down to this: Adapter solves interface incompatibility, Facade solves interface complexity.