One small interface over a subsystem of several classes. It simplifies the way in. It does not lock the other doors.
Tap Place order in a food app. One tap, and behind it four things must happen in the right order: the pizzas are set aside in stock, the card is charged, a delivery slot is booked, and a message goes to the customer. The phone app wants that. So does the website, the support agent's tool and the "reorder" button. If each of them makes the four calls itself, the sequence exists in four places, and the day one of them forgets what to do when the card is declined, the shop quietly loses stock.
The Facade pattern is the fix: put the sequence in one class with one method, and let every caller say "place this order". This lesson shows the bug first, then the facade in Python, Java, C++ and JavaScript, and then does the part that decides whether you can explain it: what belongs behind a facade and what doesn't, the fact that a facade simplifies but doesn't hide, and how it differs from an Adapter and from a Mediator. Six case studies have a facade, usually called a service: movie booking, Splitwise, the library, ride sharing, food delivery and the file system.
A movie night button runs the steps in order, yet every separate remote still works. A facade is one simple way in that leaves the other doors open.
The first version: every caller repeats the dance#
The first checkout is written for the app. Then the website needs the same thing, and somebody copies it:
# first_version.py: two checkouts each repeat the four-step dance, and one forgets to give the stock back
class Declined(Exception):
pass
class NoSlot(Exception):
pass
class Inventory: # subsystem 1: stock, with its own rule (never sell what is not there)
def __init__(self, stock, prices) -> None:
self.stock, self.prices = dict(stock), dict(prices)
def price(self, sku: str) -> int:
return self.prices[sku]
def count(self, sku: str) -> int:
return self.stock[sku]
def reserve(self, sku: str, qty: int) -> bool:
if self.stock.get(sku, 0) < qty:
return False
self.stock[sku] -= qty
return True
def release(self, sku: str, qty: int) -> None: # undo a reserve
self.stock[sku] += qty
def restock(self, sku: str, qty: int) -> None: # new goods arrive
self.stock[sku] += qty
class Payments: # subsystem 2: money, with its own rule (a charge needs funds)
def __init__(self, funds) -> None:
self.funds, self.charges = dict(funds), {}
def charge(self, customer: str, amount: int) -> str:
if self.funds.get(customer, 0) < amount:
raise Declined(customer)
self.funds[customer] -= amount
ref = f"pay_{len(self.charges) + 1}"
self.charges[ref] = (customer, amount)
return ref
def refund(self, ref: str) -> None:
customer, amount = self.charges.pop(ref)
self.funds[customer] += amount
class Delivery: # subsystem 3: delivery slots, with its own rule (a slot goes to one order)
def __init__(self, slots) -> None:
self.free, self.booked = list(slots), {}
def schedule(self, order_id: str, address: str) -> str:
if not self.free:
raise NoSlot(order_id)
self.booked[order_id] = self.free.pop(0)
return self.booked[order_id]
class Notifier: # subsystem 4: messages to customers
def __init__(self) -> None:
self.sent = []
def send(self, customer: str, text: str) -> None:
self.sent.append((customer, text))
def app_checkout(inventory, payments, delivery, notifier, order_id, customer, sku, qty, address):
if not inventory.reserve(sku, qty):
return "out of stock"
try:
payments.charge(customer, qty * inventory.price(sku))
except Declined:
inventory.release(sku, qty) # give the stock back
return "declined"
slot = delivery.schedule(order_id, address)
notifier.send(customer, f"order {order_id} arrives at {slot}")
return f"placed, {qty} x {sku}, delivery {slot}"
def web_checkout(inventory, payments, delivery, notifier, order_id, customer, sku, qty, address):
if not inventory.reserve(sku, qty):
return "out of stock"
try:
payments.charge(customer, qty * inventory.price(sku))
except Declined:
return "declined" # the same four steps, written again: this copy forgot to give the stock back
slot = delivery.schedule(order_id, address)
notifier.send(customer, f"order {order_id} arrives at {slot}")
return f"placed, {qty} x {sku}, delivery {slot}"
inventory = Inventory({"pizza": 10}, {"pizza": 300})
payments = Payments({"asha": 2000, "ravi": 100})
delivery = Delivery(["18:30", "19:00"])
notifier = Notifier()
for label, checkout, order_id, customer in (("app", app_checkout, "o-1", "asha"), ("app", app_checkout, "o-2", "ravi"), ("web", web_checkout, "o-3", "ravi")):
result = checkout(inventory, payments, delivery, notifier, order_id, customer, "pizza", 2, "12 Park Road")
print(f"{label} {order_id}: {result} (pizza stock {inventory.count('pizza')})")// FirstVersion.java: two checkouts each repeat the four-step dance, and one forgets to give the stock back
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class FirstVersion {
static final class Declined extends RuntimeException {
Declined(String who) { super(who); }
}
static final class NoSlot extends RuntimeException {
NoSlot(String orderId) { super(orderId); }
}
static final class Inventory { // subsystem 1: stock, with its own rule (never sell what is not there)
private final Map<String, Integer> stock;
private final Map<String, Integer> prices;
Inventory(Map<String, Integer> stock, Map<String, Integer> prices) {
this.stock = new HashMap<>(stock);
this.prices = new HashMap<>(prices);
}
int price(String sku) { return prices.get(sku); }
int count(String sku) { return stock.get(sku); }
boolean reserve(String sku, int qty) {
if (stock.getOrDefault(sku, 0) < qty) return false;
stock.merge(sku, -qty, Integer::sum);
return true;
}
void release(String sku, int qty) { stock.merge(sku, qty, Integer::sum); } // undo a reserve
void restock(String sku, int qty) { stock.merge(sku, qty, Integer::sum); } // new goods arrive
}
static final class Payments { // subsystem 2: money, with its own rule (a charge needs funds)
private final Map<String, Integer> funds;
private final Map<String, Map.Entry<String, Integer>> charges = new HashMap<>();
Payments(Map<String, Integer> funds) { this.funds = new HashMap<>(funds); }
String charge(String customer, int amount) {
if (funds.getOrDefault(customer, 0) < amount) throw new Declined(customer);
funds.merge(customer, -amount, Integer::sum);
String ref = "pay_" + (charges.size() + 1);
charges.put(ref, Map.entry(customer, amount));
return ref;
}
void refund(String ref) {
Map.Entry<String, Integer> charge = charges.remove(ref);
funds.merge(charge.getKey(), charge.getValue(), Integer::sum);
}
}
static final class Delivery { // subsystem 3: delivery slots, with its own rule (a slot goes to one order)
private final List<String> free;
private final Map<String, String> booked = new HashMap<>();
Delivery(List<String> slots) { this.free = new ArrayList<>(slots); }
String schedule(String orderId, String address) {
if (free.isEmpty()) throw new NoSlot(orderId);
booked.put(orderId, free.remove(0));
return booked.get(orderId);
}
}
static final class Notifier { // subsystem 4: messages to customers
final List<String> sent = new ArrayList<>();
void send(String customer, String text) { sent.add(customer + ": " + text); }
}
interface Checkout {
String run(Inventory inventory, Payments payments, Delivery delivery, Notifier notifier, String orderId, String customer, String sku, int qty, String address);
}
static String appCheckout(Inventory inventory, Payments payments, Delivery delivery, Notifier notifier, String orderId, String customer, String sku, int qty, String address) {
if (!inventory.reserve(sku, qty)) return "out of stock";
try {
payments.charge(customer, qty * inventory.price(sku));
} catch (Declined e) {
inventory.release(sku, qty); // give the stock back
return "declined";
}
String slot = delivery.schedule(orderId, address);
notifier.send(customer, "order " + orderId + " arrives at " + slot);
return "placed, " + qty + " x " + sku + ", delivery " + slot;
}
static String webCheckout(Inventory inventory, Payments payments, Delivery delivery, Notifier notifier, String orderId, String customer, String sku, int qty, String address) {
if (!inventory.reserve(sku, qty)) return "out of stock";
try {
payments.charge(customer, qty * inventory.price(sku));
} catch (Declined e) {
return "declined"; // the same four steps, written again: this copy forgot to give the stock back
}
String slot = delivery.schedule(orderId, address);
notifier.send(customer, "order " + orderId + " arrives at " + slot);
return "placed, " + qty + " x " + sku + ", delivery " + slot;
}
public static void main(String[] args) {
Inventory inventory = new Inventory(Map.of("pizza", 10), Map.of("pizza", 300));
Payments payments = new Payments(Map.of("asha", 2000, "ravi", 100));
Delivery delivery = new Delivery(List.of("18:30", "19:00"));
Notifier notifier = new Notifier();
String[][] calls = {{"app", "o-1", "asha"}, {"app", "o-2", "ravi"}, {"web", "o-3", "ravi"}};
for (String[] call : calls) {
Checkout checkout = call[0].equals("app") ? FirstVersion::appCheckout : FirstVersion::webCheckout;
String result = checkout.run(inventory, payments, delivery, notifier, call[1], call[2], "pizza", 2, "12 Park Road");
System.out.println(call[0] + " " + call[1] + ": " + result + " (pizza stock " + inventory.count("pizza") + ")");
}
}
}// first_version.cpp: two checkouts each repeat the four-step dance, and one forgets to give the stock back
#include <iostream>
#include <map>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
struct Declined : std::runtime_error {
explicit Declined(const std::string& who) : std::runtime_error(who) {}
};
struct NoSlot : std::runtime_error {
explicit NoSlot(const std::string& order_id) : std::runtime_error(order_id) {}
};
class Inventory { // subsystem 1: stock, with its own rule (never sell what is not there)
std::map<std::string, int> stock_;
std::map<std::string, int> prices_;
public:
Inventory(std::map<std::string, int> stock, std::map<std::string, int> prices) : stock_(std::move(stock)), prices_(std::move(prices)) {}
int price(const std::string& sku) const { return prices_.at(sku); }
int count(const std::string& sku) const { return stock_.at(sku); }
bool reserve(const std::string& sku, int qty) {
if (stock_[sku] < qty) return false;
stock_[sku] -= qty;
return true;
}
void release(const std::string& sku, int qty) { stock_[sku] += qty; } // undo a reserve
void restock(const std::string& sku, int qty) { stock_[sku] += qty; } // new goods arrive
};
class Payments { // subsystem 2: money, with its own rule (a charge needs funds)
std::map<std::string, int> funds_;
std::map<std::string, std::pair<std::string, int>> charges_;
public:
explicit Payments(std::map<std::string, int> funds) : funds_(std::move(funds)) {}
std::string charge(const std::string& customer, int amount) {
if (funds_[customer] < amount) throw Declined(customer);
funds_[customer] -= amount;
std::string ref = "pay_" + std::to_string(charges_.size() + 1);
charges_[ref] = {customer, amount};
return ref;
}
void refund(const std::string& ref) {
auto charge = charges_.at(ref);
charges_.erase(ref);
funds_[charge.first] += charge.second;
}
};
class Delivery { // subsystem 3: delivery slots, with its own rule (a slot goes to one order)
std::vector<std::string> free_;
std::map<std::string, std::string> booked_;
public:
explicit Delivery(std::vector<std::string> slots) : free_(std::move(slots)) {}
std::string schedule(const std::string& order_id, const std::string& /*address*/) {
if (free_.empty()) throw NoSlot(order_id);
booked_[order_id] = free_.front();
free_.erase(free_.begin());
return booked_[order_id];
}
};
class Notifier { // subsystem 4: messages to customers
public:
std::vector<std::string> sent;
void send(const std::string& customer, const std::string& text) { sent.push_back(customer + ": " + text); }
};
std::string app_checkout(Inventory& inventory, Payments& payments, Delivery& delivery, Notifier& notifier, const std::string& order_id,
const std::string& customer, const std::string& sku, int qty, const std::string& address) {
if (!inventory.reserve(sku, qty)) return "out of stock";
try {
payments.charge(customer, qty * inventory.price(sku));
} catch (const Declined&) {
inventory.release(sku, qty); // give the stock back
return "declined";
}
std::string slot = delivery.schedule(order_id, address);
notifier.send(customer, "order " + order_id + " arrives at " + slot);
return "placed, " + std::to_string(qty) + " x " + sku + ", delivery " + slot;
}
std::string web_checkout(Inventory& inventory, Payments& payments, Delivery& delivery, Notifier& notifier, const std::string& order_id,
const std::string& customer, const std::string& sku, int qty, const std::string& address) {
if (!inventory.reserve(sku, qty)) return "out of stock";
try {
payments.charge(customer, qty * inventory.price(sku));
} catch (const Declined&) {
return "declined"; // the same four steps, written again: this copy forgot to give the stock back
}
std::string slot = delivery.schedule(order_id, address);
notifier.send(customer, "order " + order_id + " arrives at " + slot);
return "placed, " + std::to_string(qty) + " x " + sku + ", delivery " + slot;
}
int main() {
Inventory inventory({{"pizza", 10}}, {{"pizza", 300}});
Payments payments({{"asha", 2000}, {"ravi", 100}});
Delivery delivery({"18:30", "19:00"});
Notifier notifier;
struct Call {
std::string label, order_id, customer;
};
for (const Call& call : {Call{"app", "o-1", "asha"}, Call{"app", "o-2", "ravi"}, Call{"web", "o-3", "ravi"}}) {
std::string result = call.label == "app" ? app_checkout(inventory, payments, delivery, notifier, call.order_id, call.customer, "pizza", 2, "12 Park Road")
: web_checkout(inventory, payments, delivery, notifier, call.order_id, call.customer, "pizza", 2, "12 Park Road");
std::cout << call.label << " " << call.order_id << ": " << result << " (pizza stock " << inventory.count("pizza") << ")\n";
}
}// firstVersion.js: two checkouts each repeat the four-step dance, and one forgets to give the stock back
class Declined extends Error {}
class NoSlot extends Error {}
class Inventory { // subsystem 1: stock, with its own rule (never sell what is not there)
constructor(stock, prices) {
this.stock = { ...stock };
this.prices = { ...prices };
}
price(sku) { return this.prices[sku]; }
count(sku) { return this.stock[sku]; }
reserve(sku, qty) {
if ((this.stock[sku] ?? 0) < qty) return false;
this.stock[sku] -= qty;
return true;
}
release(sku, qty) { this.stock[sku] += qty; } // undo a reserve
restock(sku, qty) { this.stock[sku] += qty; } // new goods arrive
}
class Payments { // subsystem 2: money, with its own rule (a charge needs funds)
constructor(funds) {
this.funds = { ...funds };
this.charges = new Map();
}
charge(customer, amount) {
if ((this.funds[customer] ?? 0) < amount) throw new Declined(customer);
this.funds[customer] -= amount;
const ref = `pay_${this.charges.size + 1}`;
this.charges.set(ref, [customer, amount]);
return ref;
}
refund(ref) {
const [customer, amount] = this.charges.get(ref);
this.charges.delete(ref);
this.funds[customer] += amount;
}
}
class Delivery { // subsystem 3: delivery slots, with its own rule (a slot goes to one order)
constructor(slots) {
this.free = [...slots];
this.booked = new Map();
}
schedule(orderId, address) {
if (this.free.length === 0) throw new NoSlot(orderId);
this.booked.set(orderId, this.free.shift());
return this.booked.get(orderId);
}
}
class Notifier { // subsystem 4: messages to customers
constructor() { this.sent = []; }
send(customer, text) { this.sent.push([customer, text]); }
}
function appCheckout(inventory, payments, delivery, notifier, orderId, customer, sku, qty, address) {
if (!inventory.reserve(sku, qty)) return "out of stock";
try {
payments.charge(customer, qty * inventory.price(sku));
} catch (e) {
if (!(e instanceof Declined)) throw e;
inventory.release(sku, qty); // give the stock back
return "declined";
}
const slot = delivery.schedule(orderId, address);
notifier.send(customer, `order ${orderId} arrives at ${slot}`);
return `placed, ${qty} x ${sku}, delivery ${slot}`;
}
function webCheckout(inventory, payments, delivery, notifier, orderId, customer, sku, qty, address) {
if (!inventory.reserve(sku, qty)) return "out of stock";
try {
payments.charge(customer, qty * inventory.price(sku));
} catch (e) {
if (!(e instanceof Declined)) throw e;
return "declined"; // the same four steps, written again: this copy forgot to give the stock back
}
const slot = delivery.schedule(orderId, address);
notifier.send(customer, `order ${orderId} arrives at ${slot}`);
return `placed, ${qty} x ${sku}, delivery ${slot}`;
}
const inventory = new Inventory({ pizza: 10 }, { pizza: 300 });
const payments = new Payments({ asha: 2000, ravi: 100 });
const delivery = new Delivery(["18:30", "19:00"]);
const notifier = new Notifier();
for (const [label, checkout, orderId, customer] of [["app", appCheckout, "o-1", "asha"], ["app", appCheckout, "o-2", "ravi"], ["web", webCheckout, "o-3", "ravi"]]) {
const result = checkout(inventory, payments, delivery, notifier, orderId, customer, "pizza", 2, "12 Park Road");
console.log(`${label} ${orderId}: ${result} (pizza stock ${inventory.count("pizza")})`);
}app o-1: placed, 2 x pizza, delivery 18:30 (pizza stock 8)
app o-2: declined (pizza stock 8)
web o-3: declined (pizza stock 6)The numbers: the shop has 10 pizzas at 300 each. Asha orders 2, which is 600, and the app's checkout reserves them (stock 8), charges her and books the 18:30 slot. Ravi has 100 rupees, so his 600-rupee order is declined: the app's checkout gives the 2 pizzas back (stock stays 8). Then the website's checkout handles Ravi's second try, and its copy of the code forgot to give the stock back: stock drops to 6, and the 2 pizzas are reserved for an order that will never exist. Neither copy handles a full delivery schedule either: the card would be charged and the pizzas reserved, with no delivery booked.
- The sequence is copied. Every caller repeats four calls, so there are two copies here, and the support tool would be a third.
- The undo is easy to forget. One copy has it and one doesn't, and nothing fails loudly: the stock number is just wrong.
- A new step edits every copy. A fraud check or a loyalty-points step goes in four places, in the right order, in each.
- Callers know four classes and their two error types, so every test of a caller needs four fakes.
Before the refactor: what belongs together?
The same four calls appear in two callers, and one has a bug. What should the callers know, and what should live in one place?
One strong answer
Callers should know what they want: place this order. One place should know how: the sequence, the undo in reverse order, and the translation of each subsystem's errors into an outcome. The callers don't need to know which classes are involved or in what order, and a new step then changes one class. That one class is a facade.
The pattern#
The book that named it (Design Patterns, 1994, by Gamma, Helm, Johnson and Vlissides) states the intent of Facade as: "Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use."
| Role | In the order code | Its one job |
|---|---|---|
| Facade | OrderDesk |
Knows which subsystem classes handle a request, delegates to them in order, and hides the order from the caller |
| Subsystem classes | Inventory, Payments, Delivery, Notifier |
Do the real work and keep their own rules. They have no knowledge of the facade |
| Client | The app, the website, the support tool | Calls the facade for the common case |
%% title: Facade: one call in, four subsystems behind it, and the doors stay open
classDiagram
class OrderDesk {
+place_order(order_id, customer, sku, qty, address) str
}
class Inventory
class Payments
class Delivery
class Notifier
class Client
class BackOffice
Client --> OrderDesk : place_order
OrderDesk --> Inventory : reserve, release
OrderDesk --> Payments : charge, refund
OrderDesk --> Delivery : schedule
OrderDesk --> Notifier : send
BackOffice ..> Inventory : restock directlyRead the arrows. Every arrow into the facade comes from a client, and every arrow out of it points at a subsystem class: the traffic is one-way. No subsystem class points back at OrderDesk, which is what lets you change the facade, or add a second one, without touching them. And the dotted arrow is the point of the next sentence: BackOffice still talks to Inventory directly.
Clients call one method, place order, on the desk. The desk calls four subsystems in order, and the arrows point one way: they don't know the desk exists.
The code: one call#
Here is the refactor in all four languages. The four subsystem classes are exactly the ones from the first version, with their own rules (stock can't go below zero, a charge needs funds, a slot goes to one order). The only new class is the desk, and it adds the part the web copy forgot, and one more:
# facade.py: one OrderDesk call runs the four subsystems in order and undoes them in reverse when a step fails
class Declined(Exception):
pass
class NoSlot(Exception):
pass
class Inventory: # subsystem 1: stock, with its own rule (never sell what is not there)
def __init__(self, stock, prices) -> None:
self.stock, self.prices = dict(stock), dict(prices)
def price(self, sku: str) -> int:
return self.prices[sku]
def count(self, sku: str) -> int:
return self.stock[sku]
def reserve(self, sku: str, qty: int) -> bool:
if self.stock.get(sku, 0) < qty:
return False
self.stock[sku] -= qty
return True
def release(self, sku: str, qty: int) -> None: # undo a reserve
self.stock[sku] += qty
def restock(self, sku: str, qty: int) -> None: # new goods arrive
self.stock[sku] += qty
class Payments: # subsystem 2: money, with its own rule (a charge needs funds)
def __init__(self, funds) -> None:
self.funds, self.charges = dict(funds), {}
def charge(self, customer: str, amount: int) -> str:
if self.funds.get(customer, 0) < amount:
raise Declined(customer)
self.funds[customer] -= amount
ref = f"pay_{len(self.charges) + 1}"
self.charges[ref] = (customer, amount)
return ref
def refund(self, ref: str) -> None:
customer, amount = self.charges.pop(ref)
self.funds[customer] += amount
class Delivery: # subsystem 3: delivery slots, with its own rule (a slot goes to one order)
def __init__(self, slots) -> None:
self.free, self.booked = list(slots), {}
def schedule(self, order_id: str, address: str) -> str:
if not self.free:
raise NoSlot(order_id)
self.booked[order_id] = self.free.pop(0)
return self.booked[order_id]
class Notifier: # subsystem 4: messages to customers
def __init__(self) -> None:
self.sent = []
def send(self, customer: str, text: str) -> None:
self.sent.append((customer, text))
class OrderDesk: # the facade: one call over four subsystems, which keep their own rules
def __init__(self, inventory: Inventory, payments: Payments, delivery: Delivery, notifier: Notifier) -> None:
self.inventory, self.payments, self.delivery, self.notifier = inventory, payments, delivery, notifier
def place_order(self, order_id: str, customer: str, sku: str, qty: int, address: str) -> str:
if not self.inventory.reserve(sku, qty):
return f"out of stock, asked {qty}, have {self.inventory.count(sku)}"
try:
ref = self.payments.charge(customer, qty * self.inventory.price(sku))
except Declined:
self.inventory.release(sku, qty)
return "declined"
try:
slot = self.delivery.schedule(order_id, address)
except NoSlot:
self.payments.refund(ref) # undo in reverse order: the charge, then the reservation
self.inventory.release(sku, qty)
return "no delivery slot, charge refunded"
self.notifier.send(customer, f"order {order_id} arrives at {slot}")
return f"placed, {qty} x {sku}, delivery {slot}"
inventory = Inventory({"pizza": 10}, {"pizza": 300})
payments = Payments({"asha": 2000, "ravi": 100})
delivery = Delivery(["18:30", "19:00"])
notifier = Notifier()
desk = OrderDesk(inventory, payments, delivery, notifier)
for order_id, customer, qty in (("o-1", "asha", 2), ("o-2", "ravi", 2), ("o-3", "asha", 9), ("o-4", "asha", 1), ("o-5", "asha", 1)):
print(f"{order_id}: {desk.place_order(order_id, customer, 'pizza', qty, '12 Park Road')} (pizza stock {inventory.count('pizza')})")
inventory.restock("pizza", 5) # the subsystem is still reachable: the back office does not go through the desk
print("back office restocks 5 directly: pizza stock", inventory.count("pizza"))
print("messages sent to customers:", len(notifier.sent))// Facade.java: one OrderDesk call runs the four subsystems in order and undoes them in reverse when a step fails
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Facade {
static final class Declined extends RuntimeException {
Declined(String who) { super(who); }
}
static final class NoSlot extends RuntimeException {
NoSlot(String orderId) { super(orderId); }
}
static final class Inventory { // subsystem 1: stock, with its own rule (never sell what is not there)
private final Map<String, Integer> stock;
private final Map<String, Integer> prices;
Inventory(Map<String, Integer> stock, Map<String, Integer> prices) {
this.stock = new HashMap<>(stock);
this.prices = new HashMap<>(prices);
}
int price(String sku) { return prices.get(sku); }
int count(String sku) { return stock.get(sku); }
boolean reserve(String sku, int qty) {
if (stock.getOrDefault(sku, 0) < qty) return false;
stock.merge(sku, -qty, Integer::sum);
return true;
}
void release(String sku, int qty) { stock.merge(sku, qty, Integer::sum); } // undo a reserve
void restock(String sku, int qty) { stock.merge(sku, qty, Integer::sum); } // new goods arrive
}
static final class Payments { // subsystem 2: money, with its own rule (a charge needs funds)
private final Map<String, Integer> funds;
private final Map<String, Map.Entry<String, Integer>> charges = new HashMap<>();
Payments(Map<String, Integer> funds) { this.funds = new HashMap<>(funds); }
String charge(String customer, int amount) {
if (funds.getOrDefault(customer, 0) < amount) throw new Declined(customer);
funds.merge(customer, -amount, Integer::sum);
String ref = "pay_" + (charges.size() + 1);
charges.put(ref, Map.entry(customer, amount));
return ref;
}
void refund(String ref) {
Map.Entry<String, Integer> charge = charges.remove(ref);
funds.merge(charge.getKey(), charge.getValue(), Integer::sum);
}
}
static final class Delivery { // subsystem 3: delivery slots, with its own rule (a slot goes to one order)
private final List<String> free;
private final Map<String, String> booked = new HashMap<>();
Delivery(List<String> slots) { this.free = new ArrayList<>(slots); }
String schedule(String orderId, String address) {
if (free.isEmpty()) throw new NoSlot(orderId);
booked.put(orderId, free.remove(0));
return booked.get(orderId);
}
}
static final class Notifier { // subsystem 4: messages to customers
final List<String> sent = new ArrayList<>();
void send(String customer, String text) { sent.add(customer + ": " + text); }
}
static final class OrderDesk { // the facade: one call over four subsystems, which keep their own rules
private final Inventory inventory;
private final Payments payments;
private final Delivery delivery;
private final Notifier notifier;
OrderDesk(Inventory inventory, Payments payments, Delivery delivery, Notifier notifier) {
this.inventory = inventory;
this.payments = payments;
this.delivery = delivery;
this.notifier = notifier;
}
String placeOrder(String orderId, String customer, String sku, int qty, String address) {
if (!inventory.reserve(sku, qty)) return "out of stock, asked " + qty + ", have " + inventory.count(sku);
String ref;
try {
ref = payments.charge(customer, qty * inventory.price(sku));
} catch (Declined e) {
inventory.release(sku, qty);
return "declined";
}
String slot;
try {
slot = delivery.schedule(orderId, address);
} catch (NoSlot e) {
payments.refund(ref); // undo in reverse order: the charge, then the reservation
inventory.release(sku, qty);
return "no delivery slot, charge refunded";
}
notifier.send(customer, "order " + orderId + " arrives at " + slot);
return "placed, " + qty + " x " + sku + ", delivery " + slot;
}
}
public static void main(String[] args) {
Inventory inventory = new Inventory(Map.of("pizza", 10), Map.of("pizza", 300));
Payments payments = new Payments(Map.of("asha", 2000, "ravi", 100));
Delivery delivery = new Delivery(List.of("18:30", "19:00"));
Notifier notifier = new Notifier();
OrderDesk desk = new OrderDesk(inventory, payments, delivery, notifier);
String[][] orders = {{"o-1", "asha", "2"}, {"o-2", "ravi", "2"}, {"o-3", "asha", "9"}, {"o-4", "asha", "1"}, {"o-5", "asha", "1"}};
for (String[] o : orders) {
System.out.println(o[0] + ": " + desk.placeOrder(o[0], o[1], "pizza", Integer.parseInt(o[2]), "12 Park Road") + " (pizza stock " + inventory.count("pizza") + ")");
}
inventory.restock("pizza", 5); // the subsystem is still reachable: the back office does not go through the desk
System.out.println("back office restocks 5 directly: pizza stock " + inventory.count("pizza"));
System.out.println("messages sent to customers: " + notifier.sent.size());
}
}// facade.cpp: one OrderDesk call runs the four subsystems in order and undoes them in reverse when a step fails
#include <iostream>
#include <map>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
struct Declined : std::runtime_error {
explicit Declined(const std::string& who) : std::runtime_error(who) {}
};
struct NoSlot : std::runtime_error {
explicit NoSlot(const std::string& order_id) : std::runtime_error(order_id) {}
};
class Inventory { // subsystem 1: stock, with its own rule (never sell what is not there)
std::map<std::string, int> stock_;
std::map<std::string, int> prices_;
public:
Inventory(std::map<std::string, int> stock, std::map<std::string, int> prices) : stock_(std::move(stock)), prices_(std::move(prices)) {}
int price(const std::string& sku) const { return prices_.at(sku); }
int count(const std::string& sku) const { return stock_.at(sku); }
bool reserve(const std::string& sku, int qty) {
if (stock_[sku] < qty) return false;
stock_[sku] -= qty;
return true;
}
void release(const std::string& sku, int qty) { stock_[sku] += qty; } // undo a reserve
void restock(const std::string& sku, int qty) { stock_[sku] += qty; } // new goods arrive
};
class Payments { // subsystem 2: money, with its own rule (a charge needs funds)
std::map<std::string, int> funds_;
std::map<std::string, std::pair<std::string, int>> charges_;
public:
explicit Payments(std::map<std::string, int> funds) : funds_(std::move(funds)) {}
std::string charge(const std::string& customer, int amount) {
if (funds_[customer] < amount) throw Declined(customer);
funds_[customer] -= amount;
std::string ref = "pay_" + std::to_string(charges_.size() + 1);
charges_[ref] = {customer, amount};
return ref;
}
void refund(const std::string& ref) {
auto charge = charges_.at(ref);
charges_.erase(ref);
funds_[charge.first] += charge.second;
}
};
class Delivery { // subsystem 3: delivery slots, with its own rule (a slot goes to one order)
std::vector<std::string> free_;
std::map<std::string, std::string> booked_;
public:
explicit Delivery(std::vector<std::string> slots) : free_(std::move(slots)) {}
std::string schedule(const std::string& order_id, const std::string& /*address*/) {
if (free_.empty()) throw NoSlot(order_id);
booked_[order_id] = free_.front();
free_.erase(free_.begin());
return booked_[order_id];
}
};
class Notifier { // subsystem 4: messages to customers
public:
std::vector<std::string> sent;
void send(const std::string& customer, const std::string& text) { sent.push_back(customer + ": " + text); }
};
class OrderDesk { // the facade: one call over four subsystems, which keep their own rules
Inventory& inventory_;
Payments& payments_;
Delivery& delivery_;
Notifier& notifier_;
public:
OrderDesk(Inventory& inventory, Payments& payments, Delivery& delivery, Notifier& notifier)
: inventory_(inventory), payments_(payments), delivery_(delivery), notifier_(notifier) {}
std::string place_order(const std::string& order_id, const std::string& customer, const std::string& sku, int qty, const std::string& address) {
if (!inventory_.reserve(sku, qty)) return "out of stock, asked " + std::to_string(qty) + ", have " + std::to_string(inventory_.count(sku));
std::string ref;
try {
ref = payments_.charge(customer, qty * inventory_.price(sku));
} catch (const Declined&) {
inventory_.release(sku, qty);
return "declined";
}
std::string slot;
try {
slot = delivery_.schedule(order_id, address);
} catch (const NoSlot&) {
payments_.refund(ref); // undo in reverse order: the charge, then the reservation
inventory_.release(sku, qty);
return "no delivery slot, charge refunded";
}
notifier_.send(customer, "order " + order_id + " arrives at " + slot);
return "placed, " + std::to_string(qty) + " x " + sku + ", delivery " + slot;
}
};
int main() {
Inventory inventory({{"pizza", 10}}, {{"pizza", 300}});
Payments payments({{"asha", 2000}, {"ravi", 100}});
Delivery delivery({"18:30", "19:00"});
Notifier notifier;
OrderDesk desk(inventory, payments, delivery, notifier);
struct Order {
std::string id, customer;
int qty;
};
for (const Order& o : {Order{"o-1", "asha", 2}, Order{"o-2", "ravi", 2}, Order{"o-3", "asha", 9}, Order{"o-4", "asha", 1}, Order{"o-5", "asha", 1}}) {
std::cout << o.id << ": " << desk.place_order(o.id, o.customer, "pizza", o.qty, "12 Park Road") << " (pizza stock " << inventory.count("pizza") << ")\n";
}
inventory.restock("pizza", 5); // the subsystem is still reachable: the back office does not go through the desk
std::cout << "back office restocks 5 directly: pizza stock " << inventory.count("pizza") << "\n";
std::cout << "messages sent to customers: " << notifier.sent.size() << "\n";
}// facade.js: one OrderDesk call runs the four subsystems in order and undoes them in reverse when a step fails
class Declined extends Error {}
class NoSlot extends Error {}
class Inventory { // subsystem 1: stock, with its own rule (never sell what is not there)
constructor(stock, prices) {
this.stock = { ...stock };
this.prices = { ...prices };
}
price(sku) { return this.prices[sku]; }
count(sku) { return this.stock[sku]; }
reserve(sku, qty) {
if ((this.stock[sku] ?? 0) < qty) return false;
this.stock[sku] -= qty;
return true;
}
release(sku, qty) { this.stock[sku] += qty; } // undo a reserve
restock(sku, qty) { this.stock[sku] += qty; } // new goods arrive
}
class Payments { // subsystem 2: money, with its own rule (a charge needs funds)
constructor(funds) {
this.funds = { ...funds };
this.charges = new Map();
}
charge(customer, amount) {
if ((this.funds[customer] ?? 0) < amount) throw new Declined(customer);
this.funds[customer] -= amount;
const ref = `pay_${this.charges.size + 1}`;
this.charges.set(ref, [customer, amount]);
return ref;
}
refund(ref) {
const [customer, amount] = this.charges.get(ref);
this.charges.delete(ref);
this.funds[customer] += amount;
}
}
class Delivery { // subsystem 3: delivery slots, with its own rule (a slot goes to one order)
constructor(slots) {
this.free = [...slots];
this.booked = new Map();
}
schedule(orderId, address) {
if (this.free.length === 0) throw new NoSlot(orderId);
this.booked.set(orderId, this.free.shift());
return this.booked.get(orderId);
}
}
class Notifier { // subsystem 4: messages to customers
constructor() { this.sent = []; }
send(customer, text) { this.sent.push([customer, text]); }
}
class OrderDesk { // the facade: one call over four subsystems, which keep their own rules
constructor(inventory, payments, delivery, notifier) {
this.inventory = inventory;
this.payments = payments;
this.delivery = delivery;
this.notifier = notifier;
}
placeOrder(orderId, customer, sku, qty, address) {
if (!this.inventory.reserve(sku, qty)) return `out of stock, asked ${qty}, have ${this.inventory.count(sku)}`;
let ref;
try {
ref = this.payments.charge(customer, qty * this.inventory.price(sku));
} catch (e) {
if (!(e instanceof Declined)) throw e;
this.inventory.release(sku, qty);
return "declined";
}
let slot;
try {
slot = this.delivery.schedule(orderId, address);
} catch (e) {
if (!(e instanceof NoSlot)) throw e;
this.payments.refund(ref); // undo in reverse order: the charge, then the reservation
this.inventory.release(sku, qty);
return "no delivery slot, charge refunded";
}
this.notifier.send(customer, `order ${orderId} arrives at ${slot}`);
return `placed, ${qty} x ${sku}, delivery ${slot}`;
}
}
const inventory = new Inventory({ pizza: 10 }, { pizza: 300 });
const payments = new Payments({ asha: 2000, ravi: 100 });
const delivery = new Delivery(["18:30", "19:00"]);
const notifier = new Notifier();
const desk = new OrderDesk(inventory, payments, delivery, notifier);
for (const [orderId, customer, qty] of [["o-1", "asha", 2], ["o-2", "ravi", 2], ["o-3", "asha", 9], ["o-4", "asha", 1], ["o-5", "asha", 1]]) {
console.log(`${orderId}: ${desk.placeOrder(orderId, customer, "pizza", qty, "12 Park Road")} (pizza stock ${inventory.count("pizza")})`);
}
inventory.restock("pizza", 5); // the subsystem is still reachable: the back office does not go through the desk
console.log("back office restocks 5 directly: pizza stock", inventory.count("pizza"));
console.log("messages sent to customers:", notifier.sent.length);o-1: placed, 2 x pizza, delivery 18:30 (pizza stock 8)
o-2: declined (pizza stock 8)
o-3: out of stock, asked 9, have 8 (pizza stock 8)
o-4: placed, 1 x pizza, delivery 19:00 (pizza stock 7)
o-5: no delivery slot, charge refunded (pizza stock 7)
back office restocks 5 directly: pizza stock 12
messages sent to customers: 2Check the five orders by hand, against a start of 10 pizzas at 300, Asha with 2,000 rupees and Ravi with 100, and two delivery slots:
- o-1, Asha, 2 pizzas: reserve (stock 8), charge 600, book 18:30, send a message. Placed.
- o-2, Ravi, 2 pizzas: reserve (stock 6), charge 600 fails because he has 100, so the desk releases the reservation (stock 8). Declined, and the stock is right.
- o-3, Asha, 9 pizzas: only 8 left, so the reserve fails before anything else is touched. Out of stock.
- o-4, Asha, 1 pizza: reserve (stock 7), charge 300, book 19:00, send a message. Placed, and both slots are now taken.
- o-5, Asha, 1 pizza: reserve (stock 6), charge 300 succeeds, but there is no slot, so the desk refunds the charge and then releases the pizza (stock 7).
Then the back office restocks 5 pizzas directly on Inventory, without going through the desk, and the count is 12. Two messages were sent, for o-1 and o-4. Each language adds something to the picture of "hiding":
- Python exposes the facade from a package's
__init__and leaves the rest as importable modules. Nothing stops an import ofInventory, which is the pattern's own position. - Java can make the subsystem classes package-private so that only the facade is public. That is a stronger choice than the pattern asks for: you trade the back office's direct route for control, so make it on purpose.
- C++ can put the subsystem behind a pointer-to-implementation, so that callers include only the facade's header and don't recompile when a subsystem changes. It hides at compile time, again by choice.
- JavaScript can re-export only the facade from a package's entry file, and Node's
exportsfield can refuse deep imports. Same trade.
When the delivery booking fails after the charge succeeded, the desk undoes the steps in reverse order: first it refunds the charge, then it releases the reserved pizza.
What belongs behind a facade, and what doesn't#
A facade is easy to write and easy to make worse. What goes behind it:
- The sequence: reserve, then charge, then book, then notify.
- The undo, in reverse order of the steps that succeeded: the refund, then the stock.
- Handing results between subsystems, such as the charge reference that the refund needs.
- Translating each subsystem's errors into the outcomes a caller understands:
declined,out of stock,no delivery slot. The subsystems' own error types,DeclinedandNoSlot, stop at the desk, and no caller needs to import them.
And what does not belong behind it:
- Each subsystem's own rules. "Never sell what isn't there" lives in
Inventory, "a charge needs funds" inPayments, "a slot goes to one order" inDelivery. If the desk checks the stock itself, there are two copies of the rule, and one of them is wrong. - Business decisions that change often, such as prices, tax and promotions. They get their own class, which the desk calls. A facade that grows
ifstatements about promo codes is turning into a god class. - Its own data. The desk holds references to four collaborators and nothing else, so two desks can run side by side.
- Subsystem types in its signatures. Callers get a plain outcome. The moment
place_orderreturns aPaymentsobject, the callers are coupled to the subsystem again.
The book gives a test for the line: a facade makes the subsystem easier to use, and it doesn't define new functionality. The desk's undo passes, because refund and release already exist in the subsystems and the desk only calls them in the right order. A desk that computes something itself, such as a discount or a delivery charge, has crossed the line: it is a service with rules of its own, and those rules need their own class and their own tests.
The facade also isn't one big class for the whole application. One facade per family of use cases (placing and cancelling orders) stays small, and a facade can call another facade.
A facade that grows stock checks, promo codes and tax becomes a god class that copies rules. It should orchestrate and leave each rule with its owner.
Testing a facade with fakes#
Because the desk is handed its four collaborators, a test can hand it fakes that write every call to one log. This is the dependency injection from lesson 4: the desk asks for four small interfaces, written in its own terms, and the Java and C++ versions below declare them. The log is the order of the calls:
# trace.py: the desk with recording fakes in place of the subsystems: the log is the order of the calls
class Declined(Exception):
pass
class NoSlot(Exception):
pass
class FakeStock: # has 10 pizzas at 300 and writes every call to the log
def __init__(self, log) -> None:
self.log = log
def price(self, sku: str) -> int:
return 300
def count(self, sku: str) -> int:
return 10
def reserve(self, sku: str, qty: int) -> bool:
self.log.append(f"reserve {sku} {qty}")
return True
def release(self, sku: str, qty: int) -> None:
self.log.append(f"release {sku} {qty}")
class FakeMoney:
def __init__(self, log, broke) -> None:
self.log, self.broke = log, set(broke)
def charge(self, customer: str, amount: int) -> str:
if customer in self.broke:
self.log.append(f"charge {customer} {amount} declined")
raise Declined(customer)
self.log.append(f"charge {customer} {amount}")
return "pay_1"
def refund(self, ref: str) -> None:
self.log.append(f"refund {ref}")
class FakeSlots:
def __init__(self, log, full) -> None:
self.log, self.full = log, full
def schedule(self, order_id: str, address: str) -> str:
if self.full:
self.log.append(f"schedule {order_id} no slot")
raise NoSlot(order_id)
self.log.append(f"schedule {order_id}")
return "18:30"
class FakeMessages:
def __init__(self, log) -> None:
self.log = log
def send(self, customer: str, text: str) -> None:
self.log.append(f"notify {customer}")
class OrderDesk: # the same facade as before: it asks for four collaborators, whatever they are
def __init__(self, stock, money, slots, messages) -> None:
self.stock, self.money, self.slots, self.messages = stock, money, slots, messages
def place_order(self, order_id: str, customer: str, sku: str, qty: int, address: str) -> str:
if not self.stock.reserve(sku, qty):
return f"out of stock, asked {qty}, have {self.stock.count(sku)}"
try:
ref = self.money.charge(customer, qty * self.stock.price(sku))
except Declined:
self.stock.release(sku, qty)
return "declined"
try:
slot = self.slots.schedule(order_id, address)
except NoSlot:
self.money.refund(ref) # undo in reverse order: the charge, then the reservation
self.stock.release(sku, qty)
return "no delivery slot, charge refunded"
self.messages.send(customer, f"order {order_id} arrives at {slot}")
return f"placed, {qty} x {sku}, delivery {slot}"
def trace(label: str, order_id: str, customer: str, qty: int, broke=(), full=False) -> None:
log = []
desk = OrderDesk(FakeStock(log), FakeMoney(log, broke), FakeSlots(log, full), FakeMessages(log))
desk.place_order(order_id, customer, "pizza", qty, "12 Park Road")
print(f"{label}: " + " > ".join(log))
trace("placed", "o-1", "asha", 2)
trace("declined", "o-2", "ravi", 2, broke={"ravi"})
trace("no slot", "o-5", "asha", 1, full=True)// Trace.java: the desk with recording fakes in place of the subsystems: the log is the order of the calls
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
class Trace {
static final class Declined extends RuntimeException {
Declined(String who) { super(who); }
}
static final class NoSlot extends RuntimeException {
NoSlot(String orderId) { super(orderId); }
}
interface Stock { // the four things the desk needs, as small interfaces
int price(String sku);
int count(String sku);
boolean reserve(String sku, int qty);
void release(String sku, int qty);
}
interface Money {
String charge(String customer, int amount);
void refund(String ref);
}
interface Slots {
String schedule(String orderId, String address);
}
interface Messages {
void send(String customer, String text);
}
static final class FakeStock implements Stock { // has 10 pizzas at 300 and writes every call to the log
private final List<String> log;
FakeStock(List<String> log) { this.log = log; }
public int price(String sku) { return 300; }
public int count(String sku) { return 10; }
public boolean reserve(String sku, int qty) {
log.add("reserve " + sku + " " + qty);
return true;
}
public void release(String sku, int qty) { log.add("release " + sku + " " + qty); }
}
static final class FakeMoney implements Money {
private final List<String> log;
private final Set<String> broke;
FakeMoney(List<String> log, Set<String> broke) {
this.log = log;
this.broke = broke;
}
public String charge(String customer, int amount) {
if (broke.contains(customer)) {
log.add("charge " + customer + " " + amount + " declined");
throw new Declined(customer);
}
log.add("charge " + customer + " " + amount);
return "pay_1";
}
public void refund(String ref) { log.add("refund " + ref); }
}
static final class FakeSlots implements Slots {
private final List<String> log;
private final boolean full;
FakeSlots(List<String> log, boolean full) {
this.log = log;
this.full = full;
}
public String schedule(String orderId, String address) {
if (full) {
log.add("schedule " + orderId + " no slot");
throw new NoSlot(orderId);
}
log.add("schedule " + orderId);
return "18:30";
}
}
static final class FakeMessages implements Messages {
private final List<String> log;
FakeMessages(List<String> log) { this.log = log; }
public void send(String customer, String text) { log.add("notify " + customer); }
}
static final class OrderDesk { // the same facade as before: it asks for four collaborators, whatever they are
private final Stock stock;
private final Money money;
private final Slots slots;
private final Messages messages;
OrderDesk(Stock stock, Money money, Slots slots, Messages messages) {
this.stock = stock;
this.money = money;
this.slots = slots;
this.messages = messages;
}
String placeOrder(String orderId, String customer, String sku, int qty, String address) {
if (!stock.reserve(sku, qty)) return "out of stock, asked " + qty + ", have " + stock.count(sku);
String ref;
try {
ref = money.charge(customer, qty * stock.price(sku));
} catch (Declined e) {
stock.release(sku, qty);
return "declined";
}
String slot;
try {
slot = slots.schedule(orderId, address);
} catch (NoSlot e) {
money.refund(ref); // undo in reverse order: the charge, then the reservation
stock.release(sku, qty);
return "no delivery slot, charge refunded";
}
messages.send(customer, "order " + orderId + " arrives at " + slot);
return "placed, " + qty + " x " + sku + ", delivery " + slot;
}
}
static void trace(String label, String orderId, String customer, int qty, Set<String> broke, boolean full) {
List<String> log = new ArrayList<>();
OrderDesk desk = new OrderDesk(new FakeStock(log), new FakeMoney(log, broke), new FakeSlots(log, full), new FakeMessages(log));
desk.placeOrder(orderId, customer, "pizza", qty, "12 Park Road");
System.out.println(label + ": " + String.join(" > ", log));
}
public static void main(String[] args) {
trace("placed", "o-1", "asha", 2, Set.of(), false);
trace("declined", "o-2", "ravi", 2, Set.of("ravi"), false);
trace("no slot", "o-5", "asha", 1, Set.of(), true);
}
}// trace.cpp: the desk with recording fakes in place of the subsystems: the log is the order of the calls
#include <iostream>
#include <set>
#include <stdexcept>
#include <string>
#include <vector>
struct Declined : std::runtime_error {
explicit Declined(const std::string& who) : std::runtime_error(who) {}
};
struct NoSlot : std::runtime_error {
explicit NoSlot(const std::string& order_id) : std::runtime_error(order_id) {}
};
class Stock { // the four things the desk needs, as small interfaces
public:
virtual ~Stock() = default;
virtual int price(const std::string& sku) = 0;
virtual int count(const std::string& sku) = 0;
virtual bool reserve(const std::string& sku, int qty) = 0;
virtual void release(const std::string& sku, int qty) = 0;
};
class Money {
public:
virtual ~Money() = default;
virtual std::string charge(const std::string& customer, int amount) = 0;
virtual void refund(const std::string& ref) = 0;
};
class Slots {
public:
virtual ~Slots() = default;
virtual std::string schedule(const std::string& order_id, const std::string& address) = 0;
};
class Messages {
public:
virtual ~Messages() = default;
virtual void send(const std::string& customer, const std::string& text) = 0;
};
using Log = std::vector<std::string>;
class FakeStock : public Stock { // has 10 pizzas at 300 and writes every call to the log
Log& log_;
public:
explicit FakeStock(Log& log) : log_(log) {}
int price(const std::string&) override { return 300; }
int count(const std::string&) override { return 10; }
bool reserve(const std::string& sku, int qty) override {
log_.push_back("reserve " + sku + " " + std::to_string(qty));
return true;
}
void release(const std::string& sku, int qty) override { log_.push_back("release " + sku + " " + std::to_string(qty)); }
};
class FakeMoney : public Money {
Log& log_;
std::set<std::string> broke_;
public:
FakeMoney(Log& log, std::set<std::string> broke) : log_(log), broke_(std::move(broke)) {}
std::string charge(const std::string& customer, int amount) override {
if (broke_.count(customer) > 0) {
log_.push_back("charge " + customer + " " + std::to_string(amount) + " declined");
throw Declined(customer);
}
log_.push_back("charge " + customer + " " + std::to_string(amount));
return "pay_1";
}
void refund(const std::string& ref) override { log_.push_back("refund " + ref); }
};
class FakeSlots : public Slots {
Log& log_;
bool full_;
public:
FakeSlots(Log& log, bool full) : log_(log), full_(full) {}
std::string schedule(const std::string& order_id, const std::string&) override {
if (full_) {
log_.push_back("schedule " + order_id + " no slot");
throw NoSlot(order_id);
}
log_.push_back("schedule " + order_id);
return "18:30";
}
};
class FakeMessages : public Messages {
Log& log_;
public:
explicit FakeMessages(Log& log) : log_(log) {}
void send(const std::string& customer, const std::string&) override { log_.push_back("notify " + customer); }
};
class OrderDesk { // the same facade as before: it asks for four collaborators, whatever they are
Stock& stock_;
Money& money_;
Slots& slots_;
Messages& messages_;
public:
OrderDesk(Stock& stock, Money& money, Slots& slots, Messages& messages) : stock_(stock), money_(money), slots_(slots), messages_(messages) {}
std::string place_order(const std::string& order_id, const std::string& customer, const std::string& sku, int qty, const std::string& address) {
if (!stock_.reserve(sku, qty)) return "out of stock, asked " + std::to_string(qty) + ", have " + std::to_string(stock_.count(sku));
std::string ref;
try {
ref = money_.charge(customer, qty * stock_.price(sku));
} catch (const Declined&) {
stock_.release(sku, qty);
return "declined";
}
std::string slot;
try {
slot = slots_.schedule(order_id, address);
} catch (const NoSlot&) {
money_.refund(ref); // undo in reverse order: the charge, then the reservation
stock_.release(sku, qty);
return "no delivery slot, charge refunded";
}
messages_.send(customer, "order " + order_id + " arrives at " + slot);
return "placed, " + std::to_string(qty) + " x " + sku + ", delivery " + slot;
}
};
void trace(const std::string& label, const std::string& order_id, const std::string& customer, int qty, const std::set<std::string>& broke, bool full) {
Log log;
FakeStock stock(log);
FakeMoney money(log, broke);
FakeSlots slots(log, full);
FakeMessages messages(log);
OrderDesk desk(stock, money, slots, messages);
desk.place_order(order_id, customer, "pizza", qty, "12 Park Road");
std::cout << label << ": ";
for (std::size_t i = 0; i < log.size(); ++i) std::cout << (i ? " > " : "") << log[i];
std::cout << "\n";
}
int main() {
trace("placed", "o-1", "asha", 2, {}, false);
trace("declined", "o-2", "ravi", 2, {"ravi"}, false);
trace("no slot", "o-5", "asha", 1, {}, true);
}// trace.js: the desk with recording fakes in place of the subsystems: the log is the order of the calls
class Declined extends Error {}
class NoSlot extends Error {}
class FakeStock { // has 10 pizzas at 300 and writes every call to the log
constructor(log) { this.log = log; }
price(sku) { return 300; }
count(sku) { return 10; }
reserve(sku, qty) {
this.log.push(`reserve ${sku} ${qty}`);
return true;
}
release(sku, qty) { this.log.push(`release ${sku} ${qty}`); }
}
class FakeMoney {
constructor(log, broke) {
this.log = log;
this.broke = new Set(broke);
}
charge(customer, amount) {
if (this.broke.has(customer)) {
this.log.push(`charge ${customer} ${amount} declined`);
throw new Declined(customer);
}
this.log.push(`charge ${customer} ${amount}`);
return "pay_1";
}
refund(ref) { this.log.push(`refund ${ref}`); }
}
class FakeSlots {
constructor(log, full) {
this.log = log;
this.full = full;
}
schedule(orderId, address) {
if (this.full) {
this.log.push(`schedule ${orderId} no slot`);
throw new NoSlot(orderId);
}
this.log.push(`schedule ${orderId}`);
return "18:30";
}
}
class FakeMessages {
constructor(log) { this.log = log; }
send(customer, text) { this.log.push(`notify ${customer}`); }
}
class OrderDesk { // the same facade as before: it asks for four collaborators, whatever they are
constructor(stock, money, slots, messages) {
this.stock = stock;
this.money = money;
this.slots = slots;
this.messages = messages;
}
placeOrder(orderId, customer, sku, qty, address) {
if (!this.stock.reserve(sku, qty)) return `out of stock, asked ${qty}, have ${this.stock.count(sku)}`;
let ref;
try {
ref = this.money.charge(customer, qty * this.stock.price(sku));
} catch (e) {
if (!(e instanceof Declined)) throw e;
this.stock.release(sku, qty);
return "declined";
}
let slot;
try {
slot = this.slots.schedule(orderId, address);
} catch (e) {
if (!(e instanceof NoSlot)) throw e;
this.money.refund(ref); // undo in reverse order: the charge, then the reservation
this.stock.release(sku, qty);
return "no delivery slot, charge refunded";
}
this.messages.send(customer, `order ${orderId} arrives at ${slot}`);
return `placed, ${qty} x ${sku}, delivery ${slot}`;
}
}
function trace(label, orderId, customer, qty, { broke = [], full = false } = {}) {
const log = [];
const desk = new OrderDesk(new FakeStock(log), new FakeMoney(log, broke), new FakeSlots(log, full), new FakeMessages(log));
desk.placeOrder(orderId, customer, "pizza", qty, "12 Park Road");
console.log(`${label}: ${log.join(" > ")}`);
}
trace("placed", "o-1", "asha", 2);
trace("declined", "o-2", "ravi", 2, { broke: ["ravi"] });
trace("no slot", "o-5", "asha", 1, { full: true });placed: reserve pizza 2 > charge asha 600 > schedule o-1 > notify asha
declined: reserve pizza 2 > charge ravi 600 declined > release pizza 2
no slot: reserve pizza 1 > charge asha 300 > schedule o-5 no slot > refund pay_1 > release pizza 1Read the three lines as three paths through one method. Placed: reserve, charge, schedule, notify. Declined: reserve, the charge fails, and the desk releases the stock. No slot: reserve, charge, the booking fails, then the desk undoes what succeeded in reverse: first the refund, then the release. The notification never happens on the failure paths, because the desk only sends it after the last step.
Which undo, in which order?
Delivery fails after the charge succeeded. Which calls undo the earlier steps, and in which order?
One strong answer
Refund the charge first, then release the stock. Undo runs in the reverse order of the steps that succeeded: the charge was the last one to succeed, so it is undone first. The trace shows refund pay_1 > release pizza 1. Nothing was notified yet, so there is nothing to undo there. In a real system an undo can fail too, which is what the Saga lessons in Week 5 are about.
Facade, Adapter and Mediator#
Three patterns put a class in front of other classes, and interviews ask which is which:
| Facade | Adapter (lesson 10) | Mediator (lesson 23) | |
|---|---|---|---|
| It fronts | Several classes: a subsystem | One class | Several peers that talk to each other |
| The interface | A new, simpler one that the facade designs | An existing one that the client already expects | The one the peers use to reach each other |
| Direction | One way: clients, then facade, then subsystem | One way: client, adapter, adaptee | Both ways: peers call the mediator, and it calls them back |
| Does the fronted code know? | No. Subsystem classes don't know the facade | No. The adaptee doesn't know | Yes. Each peer holds a reference to the mediator |
| Purpose | Make a subsystem easier to use | Make one class fit | Stop peers from referring to each other, and coordinate them |
| Here | OrderDesk |
AlphaAdapter, the payment adapter |
A group-order room, where joining, paying and locking the cart coordinate |
The book draws the line between the last two this way: a facade's protocol is unidirectional, in that it makes requests of the subsystem classes but not the other way round, while a mediator enables cooperative behaviour the colleagues can't provide themselves, and its protocol is multidirectional. The test in one question: does the fronted code call back? If not, it is a facade. And a facade is not an adapter just because it also wraps something: ask whether the interface was designed for the client (facade) or fixed in advance (adapter).
A few more neighbours: the book says a facade is often a Singleton, and after the Singleton lesson the better version is an ordinary object that is passed in, as the desk is here. A service layer in a layered application is a facade over the domain. An API gateway is the same idea across a network: one entry point in front of several services.
An adapter fits one class to an existing interface. A facade gives several classes a new, simpler one. A mediator lets peers talk through it, both ways.
When not to use it#
- When there is one class behind it. A facade over a single class is a pass-through, so it adds a layer and no simplification.
- When it only renames. If
place_orderforwards to one call, it is noise. A facade earns its place by owning a sequence or a translation. - When it grows rules. Prices, tax and promotions inside the desk make it a god class. Keep those in their own classes and keep the desk to orchestration.
- When it becomes the only door. If the back office, a migration or a test must go through it, it has stopped being a convenience and become a bottleneck. Keep the subsystem reachable.
- When it is one giant facade. A single
AppFacadewith sixty methods is the god class the pattern was supposed to prevent. Split by use case. - As a cover for a bad subsystem. A facade over a tangle leaves the tangle. It is a fine first step, but say so.
Where it shows up in the case studies#
All six are service classes that callers use without touching the internals. Most also hold a lock or a rule, which is a reminder of how thin the line is: the pattern is the shape, and a real service can carry more.
| Case study | Facade | What it hides from the caller |
|---|---|---|
| Movie booking | BookingService: hold, confirm, cancel_hold (and the read available) |
The lesson's words: the API layer "never touches locks or the sold set directly" |
| Splitwise | SplitService.add_expense |
Its table says "Facade / transaction script": validate, split, record and apply, as one unit under the group's lock |
| Library | Library |
The desk, the website and the nightly job call a handful of methods and never touch a copy directly |
| Ride sharing | RideService: request, start, complete, cancel |
Four methods, and the API layer "never touches cells, locks or fares" |
| Food delivery | FoodService |
The customer app, the restaurant tablet and the partner app each call a few methods, and none touches the order's fields |
| File system | FileSystem: mkdir, ls, write, read, mv, rm, du |
Callers speak in paths. The tree, the quota and the lock are internal |
In the interview#
Say it with the catch: "Facade gives a subsystem one simpler entry point, so callers say what they want and one class owns the sequence and the undo. It simplifies but doesn't hide: the subsystem stays reachable, and the facade keeps no rules of its own."
Then expect the follow-ups:
- "Facade or Adapter?" An adapter makes one class fit an interface that already exists. A facade invents a new, simpler interface over several classes.
- "Facade or Mediator?" A facade is one-way and the subsystem doesn't know it. A mediator is two-way, and each colleague holds a reference to it.
- "Should the subsystem classes be private?" Not by the pattern: a facade doesn't prevent direct use. A language can enforce it, as a choice that trades generality for control.
- "What goes in the facade and what doesn't?" The sequence, the undo and the error translation. Not the subsystems' own rules, not changing business rules, and not its own data.
- "How do you test it?" Hand it fakes that record their calls, and assert the order, including the undo in reverse.
- "What if the undo itself fails?" Then it needs retries or a saga (the Week 5 lessons): the facade's reverse order is only the first half.
Common mistakes#
- Saying a facade hides the subsystem. It simplifies. The book says it doesn't prevent direct use.
- Putting a subsystem's rule in the facade, so two classes enforce it and one is stale.
- A facade that grows business rules: prices, tax, promotions. It becomes a god class.
- Forgetting the undo, or doing it in the wrong order, so a failure leaves a charge or a reservation behind.
- Calling an adapter a facade (or the reverse) because both wrap something. Ask who designed the interface.
- Returning subsystem types from the facade, so callers are coupled to the subsystem again.
Facade
Q1/4 A teammate says the subsystem classes behind a facade should all be made private, so nobody can bypass it. What does the pattern say?
Answer & explanation
Answer: B
The book says a facade doesn't prevent applications from using subsystem classes if they need to, so you choose between ease of use and generality. A language can enforce hiding, but that is a decision, not part of the pattern.
Q2/4 A charge succeeds, then the delivery booking fails. Which calls undo the earlier steps, in which order?
Answer & explanation
Answer: C
The charge was the last step to succeed, so it is undone first, then the reservation. The notification was never sent, so there is nothing to undo there.
Q3/4 What separates a Facade from a Mediator?
Answer & explanation
Answer: C
A facade makes requests of the subsystem and not the other way round. A mediator's protocol is multidirectional, and the colleagues call it as well as being called by it.
Q4/4 Where should the rule "never sell more than is in stock" live?
Answer & explanation
Answer: C
A subsystem keeps its own rules. A rule copied into the facade exists twice, and the copies drift. The facade orchestrates and translates.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


