Facade¶
Intent¶
Provide one higher-level interface to a subsystem of several collaborating objects, so the common case is a single call. place_order reserves stock, charges the card, stores the order and notifies the customer, in that order, and releases the stock if the card is declined. The parts stay independent and reachable; the facade adds a door, it does not build a wall.
When to use and when not to¶
Use it when
- A use case touches several objects in a fixed order and every caller would otherwise repeat that choreography: checkout, check-in, withdrawal, ride booking.
- The workflow needs compensation. When step three fails, someone has to undo steps one and two; that someone should exist once, with a name, and be tested.
- You want the client (a controller, a CLI, a test) to depend on one interface, so the subsystem can be rearranged behind it.
- You are wrapping a library whose full surface is wide and whose common case is narrow;
requests.getover aSessionis the model.
Leave it out when
- There is one object behind it. A facade over one class is a rename; if the interface must change, that is Adapter.
- The parts must talk to each other through it, with state flowing both ways; that is Mediator, and the difference is who calls whom.
- The "facade" starts holding business rules. Stock arithmetic belongs to the inventory, declines to payments; a facade that validates everything itself becomes the God object the pattern was meant to prevent.
- The operation must be atomic across a database. A facade sequences calls and compensates; it is not a transaction. Reach for Unit of Work when the steps share one store, and name the saga when they do not.
Structure¶
One Facade in front of four independent parts; the client calls the facade, the parts never call each other or the facade.
classDiagram
class CheckoutFacade {
-_inventory: InventoryService
-_payments: PaymentService
-_orders: OrderRepository
-_notifier: Notifier
+place_order(customer_id: str, cart: Sequence~CartLine~, card_token: str) Order
+cancel_order(order_id: str) Order
}
class InventoryService {
+available(sku: str) int
+reserve(lines: Sequence~CartLine~) None
+release(lines: Sequence~CartLine~) None
}
class PaymentService {
+charge(customer_id: str, amount: Money, card_token: str) str
+refund(payment_id: str) Money
}
class OrderRepository {
+save(order: Order) None
+get(order_id: str) Order
}
class Notifier {
+sent: List~str~
+notify(customer_id: str, text: str) None
}
class Order {
+order_id: str
+customer_id: str
+lines: Tuple~CartLine~
+total: Money
+payment_id: str
+status: OrderStatus
+placed_at: float
}
class CartLine {
+sku: str
+quantity: int
+unit_price: Money
}
CheckoutFacade --> InventoryService
CheckoutFacade --> PaymentService
CheckoutFacade --> OrderRepository
CheckoutFacade --> Notifier
CheckoutFacade ..> Order : creates
Order *-- CartLine
Every arrow points away from the facade. Nothing in the subsystem imports it, which is what keeps the parts testable alone and reusable by a second facade (returns, say) later.
Canonical example in Python¶
The subsystem first, because the facade is only as good as the boundaries it sits on (code/patterns/facade.py, tested by code/patterns/tests/test_facade.py):
class OrderStatus(StrEnum):
PLACED = "placed"
CANCELLED = "cancelled"
@dataclass(frozen=True, slots=True)
class CartLine:
sku: str
quantity: int
unit_price: Money
@dataclass(slots=True)
class Order:
order_id: str
customer_id: str
lines: tuple[CartLine, ...]
total: Money
payment_id: str
status: OrderStatus
placed_at: float
class OutOfStockError(HandbookError):
"""Raised by the inventory before any money moves."""
class CardDeclinedError(HandbookError):
"""Raised by the payment service; the facade must compensate for it."""
class InventoryService:
"""Stock per SKU. The lock makes check-and-decrement atomic across threads."""
def __init__(self, stock: Mapping[str, int]) -> None:
self._stock = dict(stock)
self._lock = threading.Lock()
def available(self, sku: str) -> int:
with self._lock:
return self._stock.get(sku, 0)
def reserve(self, lines: Sequence[CartLine]) -> None:
with self._lock:
short = [line.sku for line in lines if self._stock.get(line.sku, 0) < line.quantity]
if short:
raise OutOfStockError(f"insufficient stock for {', '.join(short)}")
for line in lines:
self._stock[line.sku] -= line.quantity
def release(self, lines: Sequence[CartLine]) -> None:
with self._lock:
for line in lines:
self._stock[line.sku] = self._stock.get(line.sku, 0) + line.quantity
class PaymentService:
"""Charges and refunds against a card token; tokens starting with ``declined`` are declined."""
def __init__(self, ids: IdGenerator) -> None:
self._ids = ids
self._charges: dict[str, Money] = {}
def charge(self, customer_id: str, amount: Money, card_token: str) -> str:
if card_token.startswith("declined"):
raise CardDeclinedError(f"card declined for {customer_id}")
payment_id = self._ids.next_id()
self._charges[payment_id] = amount
return payment_id
def refund(self, payment_id: str) -> Money:
try:
return self._charges.pop(payment_id)
except KeyError:
raise NotFoundError(f"unknown payment {payment_id}") from None
@property
def charges(self) -> int:
return len(self._charges)
class OrderRepository:
def __init__(self) -> None:
self._orders: dict[str, Order] = {}
def save(self, order: Order) -> None:
self._orders[order.order_id] = order
def get(self, order_id: str) -> Order:
try:
return self._orders[order_id]
except KeyError:
raise NotFoundError(f"unknown order {order_id}") from None
def __len__(self) -> int:
return len(self._orders)
class Notifier:
"""Stands in for e-mail: remembers what it sent so the demo and the tests can show it."""
def __init__(self) -> None:
self.sent: list[str] = []
def notify(self, customer_id: str, text: str) -> None:
self.sent.append(f"{customer_id}: {text}")
Each part owns one invariant. InventoryService.reserve checks and decrements under a lock, and rejects the whole cart if any line is short, so a reservation is never half applied. PaymentService knows what a decline is. Neither knows that an order exists.
The facade owns the order of events and the undo:
class CheckoutFacade:
"""The Facade: the workflow in one place, the subsystem behind it untouched.
It owns the order of the steps and the compensation when a later step fails.
It owns no business rule that belongs to a part: stock arithmetic stays in the
inventory, declines stay in payments. Clients that need a single part can still
call it directly; the facade adds a door, it does not remove the others.
"""
def __init__(
self,
inventory: InventoryService,
payments: PaymentService,
orders: OrderRepository,
notifier: Notifier,
ids: IdGenerator,
clock: Clock,
) -> None:
self._inventory = inventory
self._payments = payments
self._orders = orders
self._notifier = notifier
self._ids = ids
self._clock = clock
def place_order(self, customer_id: str, cart: Sequence[CartLine], card_token: str) -> Order:
if not cart:
raise ValidationError("cart is empty")
total = sum((line.unit_price * line.quantity for line in cart), Money(0))
self._inventory.reserve(cart) # fails before any money moves
try:
payment_id = self._payments.charge(customer_id, total, card_token)
except CardDeclinedError:
self._inventory.release(cart) # compensate: undo the step that did succeed
raise
order = Order(
self._ids.next_id(), customer_id, tuple(cart), total, payment_id,
OrderStatus.PLACED, self._clock.now(),
)
self._orders.save(order)
self._notifier.notify(customer_id, f"order {order.order_id} placed: {total}")
return order
def cancel_order(self, order_id: str) -> Order:
order = self._orders.get(order_id)
if order.status is not OrderStatus.PLACED:
raise InvalidStateError(f"order {order_id} is already {order.status}")
refunded = self._payments.refund(order.payment_id)
self._inventory.release(order.lines)
order.status = OrderStatus.CANCELLED
self._orders.save(order)
self._notifier.notify(order.customer_id, f"order {order_id} cancelled: {refunded} refunded")
return order
Four decisions to say out loud:
- Cheapest and most reversible step first. Validate the cart, then reserve stock (free to undo), then charge the card (costly to undo), then persist and notify. A decline after the reservation is compensated by one
release; an out-of-stock cart never reaches the card. - Compensate, then re-raise. The facade undoes what it did and lets the original
CardDeclinedErrorthrough. Swallowing it would hide the outcome from the controller; translating it would be the adapter's job. - No rule of its own. The facade never looks at stock counts or card tokens. When the interviewer adds a discount rule, it goes into a pricing part that the facade calls, not into
place_order. - The lock lives in the part. The concurrency test fires 40 checkouts at 5 keyboards through the facade and exactly 5 succeed, because
InventoryServiceowns its invariant. A facade-level lock would serialise every checkout to protect one counter.
Running python -m patterns.facade prints:
--- one call runs reserve, charge, save, notify ---
ord-1 for cust-1: 145.00 USD, pay-1, placed
stock after: keyboard 3, mouse 9
--- the facade compensates: a declined card releases the reservation ---
CardDeclinedError: card declined for cust-1; stock back to keyboard 3, mouse 9; orders saved: 1
--- and it checks before it spends: out of stock means no charge at all ---
OutOfStockError: insufficient stock for keyboard; charges on record: 1
--- cancel reverses the workflow ---
ord-1 cancelled; stock keyboard 5, mouse 10; charges on record: 0
InvalidStateError: order ord-1 is already cancelled
--- the subsystem is still reachable when the facade is too coarse ---
inventory.available('mouse') -> 10
--- what the notifier sent along the way ---
cust-1: order ord-1 placed: 145.00 USD
cust-1: order ord-1 cancelled: 145.00 USD refunded
--- Pythonic variant: the facade as a function, the subsystem bound once ---
ord-2 for cust-2: 25.00 USD; stock keyboard 5, mouse 9
Pythonic variant¶
CheckoutFacade holds references and no state of its own, so the class is optional. A function with keyword-only collaborators is the same facade, and functools.partial plays the constructor:
# The facade holds no state of its own, so a function with keyword-only collaborators is the
# same thing; ``functools.partial`` binds the subsystem once and hands back a one-step callable.
def place_order(
customer_id: str,
cart: Sequence[CartLine],
card_token: str,
*,
inventory: InventoryService,
payments: PaymentService,
orders: OrderRepository,
notifier: Notifier,
ids: IdGenerator,
clock: Clock,
) -> Order:
facade = CheckoutFacade(inventory, payments, orders, notifier, ids, clock)
return facade.place_order(customer_id, cart, card_token)
quick_checkout = partial(place_order, inventory=inventory, payments=payments,
orders=orders, notifier=notifier, ids=ids, clock=clock)
order = quick_checkout("cust-2", [CartLine("mouse", 1, Money.of("25.00"))], "tok-visa")
The stdlib's favourite form goes one step further: a module-level function that builds the subsystem for you. requests.get(url) creates a Session, sends, and closes it; subprocess.run(cmd) builds a Popen, waits and collects. The function is the facade for the common case, and the class stays public for callers who need connection reuse or streaming. Offer both in the room: "a checkout() function for the 80% case, the CheckoutFacade for callers that need to configure the parts".
| Reach for | When |
|---|---|
| A function with keyword-only collaborators | The facade keeps no state; partial binds the subsystem once |
| A module-level convenience function | The subsystem can be built per call cheaply and the common case needs no configuration |
| A class | Two or more entry points share the same collaborators (place_order, cancel_order), or the client must be handed one object to depend on |
Real-world usage¶
requests.get,post, ... overSession,PreparedRequest, adapters and connection pools.subprocess.runoverPopen, pipes andcommunicate().shutil.make_archiveoverzipfileandtarfile.logging.basicConfigand the module-levellogging.infooverLogger,HandlerandFormatter; the objects remain for anyone who outgrows the one-liner.urllib.request.urlopenoverOpenerDirectorand its handler chain;pathlib.Path.read_textoveropen, decode andclose;json.dumpsoverJSONEncoder.- Frameworks: Django's
ModelForm.save()andModel.objects.create(), SQLAlchemy'sSessionover connections and the unit of work, a "service layer" or "application service" in DDD vocabulary, which is a facade over the domain model.
Related patterns and confusions¶
| Looks like Facade | How to tell them apart |
|---|---|
| Adapter | Wraps one object to give it the interface a client expects, one-to-one and interchangeable with other adapters. A facade defines a new, simpler interface over many objects and is not interchangeable with what it hides. |
| Mediator | Both centralise, but the direction differs. A facade is one-way: client to facade to parts, and the parts never know it exists. A mediator sits between colleagues that talk through it, both ways, at runtime. |
| Proxy / Decorator | Same interface as the thing wrapped. A facade's interface is different by design. |
| Abstract Factory | Builds the family of parts; a facade uses them. Often the factory is what the facade's constructor arguments came from. |
| Unit of Work / Saga | The transaction boundary. A facade sequences and compensates in memory; it does not make the steps atomic. |
| Gateway / Anti-corruption layer | A facade over a remote system, usually built from adapters, with translation of the whole model. |
Where it appears in LLD problems¶
- Design Amazon (cart, order, inventory, payment) —
place_orderacross cart, inventory, payment and notification, with compensation on failure. - Design a hotel management system — check-in and check-out as facades over reservations, rooms, billing and housekeeping.
- Design an ATM —
withdrawover card authentication, account, cash dispenser and receipt printer. - Design Uber (LLD) with driver matching —
request_rideover matching, pricing, driver state and notification.
Interview tips¶
Interview tip
Name the steps, their order and the undo in one breath: "place_order validates, reserves, charges, saves, notifies; reserve comes before charge because releasing stock is free and refunding is not; a decline releases the reservation and re-raises." Then show that the subsystem is still usable without the facade, and that the lock sits in the inventory, not in the facade.
Common mistake
Growing a God facade. The moment place_order checks stock counts, computes tax and validates card numbers itself, every rule in the system lives in one class with one test file, and the parts cannot be reused or tested alone. Keep the facade to sequencing and compensation; push every rule into the part that owns the data. Runner-up: calling the sequence a transaction. It is not; say "compensation" and name Unit of Work or a saga for atomicity.
Related¶
- Adapter — one object, matching interface; the facade is many objects, new interface
- Mediator — colleagues talking through a hub, both ways
- Unit of Work — the atomic boundary a facade's compensation approximates
- Abstract Factory — where the facade's parts come from
- Design Amazon (cart, order, inventory, payment) — checkout as a facade in a full problem
- Design a hotel management system — check-in and check-out facades
- Gamma, Helm, Johnson and Vlissides, Design Patterns (1994), Facade
- Python documentation:
subprocess— therunfunction and thePopenconstructor - Requests documentation: Session objects