Facade
A facade gives callers one simple operation for a common job that really takes many steps across several classes. This page puts a rental desk in front of Pedal's six rental subsystems, including the clean-up when a lock jams, and counts what it saves.
1. The pain
Starting a Pedal rental touches six parts of the system, in a fixed order: check the account, put a 100p hold on the card, reserve a bike at the dock, open the lock, start the ride record, send a push notification. If the lock jams, the bike must go back and the hold must be released, in reverse order.
The phone app, the station kiosk and the call-centre tool each start rentals, so each grew its own copy of those six calls and their clean-up. When fraud asked for a check before the hold, three teams had to change three copies, and the kiosk's copy forgot to release the hold on a jam.
2. The idea in one picture
Facade puts one simple interface in front of a busy subsystem for the jobs most callers need. RentalDesk.start_rental(rider, dock) makes the six calls in the right order and handles the clean-up. The parts behind it stay as they are, and anyone with an unusual need (the monthly report reading ride records, say) can still use them directly.
A facade simplifies; it doesn't hide or translate. That's the difference from an adapter (which changes one object's interface) and from a mediator (which the parts themselves talk through).
3. The roles
| Role | Its job | In Pedal |
|---|---|---|
| Facade | Offers a few high-level operations and calls the subsystem in the right order. | RentalDesk.start_rental() |
| Subsystem classes | Do the real work; don't know the facade exists. | Accounts, Payments, Docks, Locks, Rides, Notifier |
| Client | Uses the facade for the common job. | The phone app, the kiosk, the call-centre tool |
| Power user (optional) | Still uses subsystem classes directly for unusual needs. | The monthly report reading Rides |
4. Before and after
Starting a rental both ways (Python sketch; the full program is in section 5).
Without the pattern
# in the app, the kiosk and the call-centre tool:
if not accounts.can_ride(rider):
return refuse()
payments.hold(rider, 100)
bike = docks.reserve(dock)
if not locks.open(dock):
docks.put_back(dock, bike)
payments.release(rider) # the kiosk forgot this
return refuse()
ride = rides.start(rider, bike)
notifier.push(rider, f"Ride {ride} started")With it
class RentalDesk:
def start_rental(self, rider, dock):
# the same six steps and the clean-up,
# written and tested once
...
# in the app, the kiosk and the call-centre tool:
desk.start_rental(rider, dock)| The next change | Without the pattern | With it |
|---|---|---|
| Add a fraud check before the hold | Three copies, three teams | One line in start_rental |
| Fix the jam clean-up | Find which copies got it wrong | Already right, in one place |
| A fourth channel (a partner's app) | A fourth copy of six calls | One call to the facade |
The order and the clean-up are knowledge about the subsystem. The facade is where that knowledge lives.
5. The code
Six small subsystems and a RentalDesk that calls them in order. Mia's rental succeeds; Leo's lock jams and the desk undoes the hold and the reservation; Zoe's account is suspended.
from __future__ import annotations
# ---- The subsystems: each does one job and knows nothing about rentals as a whole. ----
class Accounts:
def __init__(self) -> None:
self.suspended = {"zoe"}
def can_ride(self, rider: str) -> bool:
return rider not in self.suspended
class Payments:
def __init__(self) -> None:
self.holds: dict[str, int] = {}
def hold(self, rider: str, pence: int) -> None:
self.holds[rider] = pence
def release(self, rider: str) -> None:
self.holds.pop(rider, None)
class Docks:
def __init__(self) -> None:
self.bikes = {"D7": ["E-205"], "D9": ["B-17"]}
def reserve(self, dock: str) -> str:
return self.bikes[dock].pop()
def put_back(self, dock: str, bike: str) -> None:
self.bikes[dock].append(bike)
class Locks:
def open(self, dock: str) -> bool:
return dock != "D9" # D9's lock is jammed today
class Rides:
def __init__(self) -> None:
self.count = 0
def start(self, rider: str, bike: str) -> str:
self.count += 1
return f"RD-{self.count}"
class Notifier:
def push(self, rider: str, text: str) -> None:
pass # would send a phone notification
# ---- The facade: one call for the common job, in the right order, with the clean-up done right. ----
class RentalDesk:
def __init__(self, accounts: Accounts, payments: Payments, docks: Docks, locks: Locks, rides: Rides, notifier: Notifier) -> None:
self.accounts, self.payments, self.docks = accounts, payments, docks
self.locks, self.rides, self.notifier = locks, rides, notifier
def start_rental(self, rider: str, dock: str) -> str:
steps: list[str] = []
if not self.accounts.can_ride(rider):
return "account suspended: rental refused"
steps.append("account ok")
self.payments.hold(rider, 100)
steps.append("hold 100p")
bike = self.docks.reserve(dock)
steps.append(f"bike {bike} reserved")
if not self.locks.open(dock): # undo the earlier steps in reverse
self.docks.put_back(dock, bike)
self.payments.release(rider)
steps.append("lock jammed -> bike put back, hold released: rental refused")
return " -> ".join(steps)
steps.append("lock opened")
ride = self.rides.start(rider, bike)
steps.append(f"ride {ride} started")
self.notifier.push(rider, f"Ride {ride} started")
steps.append("push sent")
return " -> ".join(steps)
if __name__ == "__main__":
payments, docks = Payments(), Docks()
desk = RentalDesk(Accounts(), payments, docks, Locks(), Rides(), Notifier())
for rider, dock in [("mia", "D7"), ("leo", "D9"), ("zoe", "D7")]:
print(f"start_rental({rider}, {dock}): {desk.start_rental(rider, dock)}")
held = ", ".join(f"{r} {p}p" for r, p in payments.holds.items())
print(f"card holds still open: {held} | bikes back at D9: {', '.join(docks.bikes['D9'])}")
// ---- The subsystems: each does one job and knows nothing about rentals as a whole. ----
class Accounts {
private readonly suspended = new Set(["zoe"]);
canRide(rider: string): boolean { return !this.suspended.has(rider); }
}
class Payments {
readonly holds = new Map<string, number>();
hold(rider: string, pence: number): void { this.holds.set(rider, pence); }
release(rider: string): void { this.holds.delete(rider); }
}
class Docks {
readonly bikes: Record<string, string[]> = { D7: ["E-205"], D9: ["B-17"] };
reserve(dock: string): string { return this.bikes[dock].pop()!; }
putBack(dock: string, bike: string): void { this.bikes[dock].push(bike); }
}
class Locks {
open(dock: string): boolean { return dock !== "D9"; } // D9's lock is jammed today
}
class Rides {
private count = 0;
start(rider: string, bike: string): string { void rider; void bike; return `RD-${++this.count}`; }
}
class Notifier {
push(rider: string, text: string): void { void rider; void text; } // would send a phone notification
}
// ---- The facade: one call for the common job, in the right order, with the clean-up done right. ----
class RentalDesk {
constructor(private readonly accounts: Accounts, private readonly payments: Payments, private readonly docks: Docks,
private readonly locks: Locks, private readonly rides: Rides, private readonly notifier: Notifier) {}
startRental(rider: string, dock: string): string {
const steps: string[] = [];
if (!this.accounts.canRide(rider)) return "account suspended: rental refused";
steps.push("account ok");
this.payments.hold(rider, 100);
steps.push("hold 100p");
const bike = this.docks.reserve(dock);
steps.push(`bike ${bike} reserved`);
if (!this.locks.open(dock)) { // undo the earlier steps in reverse
this.docks.putBack(dock, bike);
this.payments.release(rider);
steps.push("lock jammed -> bike put back, hold released: rental refused");
return steps.join(" -> ");
}
steps.push("lock opened");
const ride = this.rides.start(rider, bike);
steps.push(`ride ${ride} started`);
this.notifier.push(rider, `Ride ${ride} started`);
steps.push("push sent");
return steps.join(" -> ");
}
}
const payments = new Payments(), docks = new Docks();
const desk = new RentalDesk(new Accounts(), payments, docks, new Locks(), new Rides(), new Notifier());
for (const [rider, dock] of [["mia", "D7"], ["leo", "D9"], ["zoe", "D7"]]) console.log(`start_rental(${rider}, ${dock}): ${desk.startRental(rider, dock)}`);
const held = [...payments.holds].map(([r, p]) => `${r} ${p}p`).join(", ");
console.log(`card holds still open: ${held} | bikes back at D9: ${docks.bikes.D9.join(", ")}`);import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Main {
// ---- The subsystems: each does one job and knows nothing about rentals as a whole. ----
static class Accounts {
private final Set<String> suspended = new HashSet<>(Set.of("zoe"));
boolean canRide(String rider) { return !suspended.contains(rider); }
}
static class Payments {
final Map<String, Integer> holds = new HashMap<>();
void hold(String rider, int pence) { holds.put(rider, pence); }
void release(String rider) { holds.remove(rider); }
}
static class Docks {
final Map<String, List<String>> bikes = new HashMap<>(Map.of("D7", new ArrayList<>(List.of("E-205")), "D9", new ArrayList<>(List.of("B-17"))));
String reserve(String dock) { List<String> b = bikes.get(dock); return b.remove(b.size() - 1); }
void putBack(String dock, String bike) { bikes.get(dock).add(bike); }
}
static class Locks {
boolean open(String dock) { return !dock.equals("D9"); } // D9's lock is jammed today
}
static class Rides {
private int count;
String start(String rider, String bike) { return "RD-" + (++count); }
}
static class Notifier {
void push(String rider, String text) { } // would send a phone notification
}
// ---- The facade: one call for the common job, in the right order, with the clean-up done right. ----
static class RentalDesk {
private final Accounts accounts; private final Payments payments; private final Docks docks;
private final Locks locks; private final Rides rides; private final Notifier notifier;
RentalDesk(Accounts accounts, Payments payments, Docks docks, Locks locks, Rides rides, Notifier notifier) {
this.accounts = accounts; this.payments = payments; this.docks = docks; this.locks = locks; this.rides = rides; this.notifier = notifier;
}
String startRental(String rider, String dock) {
List<String> steps = new ArrayList<>();
if (!accounts.canRide(rider)) return "account suspended: rental refused";
steps.add("account ok");
payments.hold(rider, 100);
steps.add("hold 100p");
String bike = docks.reserve(dock);
steps.add("bike " + bike + " reserved");
if (!locks.open(dock)) { // undo the earlier steps in reverse
docks.putBack(dock, bike);
payments.release(rider);
steps.add("lock jammed -> bike put back, hold released: rental refused");
return String.join(" -> ", steps);
}
steps.add("lock opened");
String ride = rides.start(rider, bike);
steps.add("ride " + ride + " started");
notifier.push(rider, "Ride " + ride + " started");
steps.add("push sent");
return String.join(" -> ", steps);
}
}
public static void main(String[] args) {
Payments payments = new Payments();
Docks docks = new Docks();
RentalDesk desk = new RentalDesk(new Accounts(), payments, docks, new Locks(), new Rides(), new Notifier());
String[][] asks = {{"mia", "D7"}, {"leo", "D9"}, {"zoe", "D7"}};
for (String[] a : asks) System.out.println("start_rental(" + a[0] + ", " + a[1] + "): " + desk.startRental(a[0], a[1]));
List<String> held = new ArrayList<>();
payments.holds.forEach((r, p) -> held.add(r + " " + p + "p"));
System.out.println("card holds still open: " + String.join(", ", held) + " | bikes back at D9: " + String.join(", ", docks.bikes.get("D9")));
}
}Output (the same in all three languages)
start_rental(mia, D7): account ok -> hold 100p -> bike E-205 reserved -> lock opened -> ride RD-1 started -> push sent start_rental(leo, D9): account ok -> hold 100p -> bike B-17 reserved -> lock jammed -> bike put back, hold released: rental refused start_rental(zoe, D7): account suspended: rental refused card holds still open: mia 100p | bikes back at D9: B-17
6. Try it
Who calls what
Three channels start rentals. Switch between calling the parts directly and calling a facade, then make a change and count the edits.
7. When not to use it
What you get
- Simple calls for common jobs: one method instead of six.
- Order and clean-up in one place: written and tested once.
- Callers decoupled from the parts: the subsystem can change behind the facade.
- Nothing taken away: power users can still reach the parts.
What you pay
- Can become a god object: every new need lands in the facade.
- Another layer to read through: for simple, one-part jobs it's overhead.
- Hides useful options: callers who need a variant must go around it.
- Easy to over-share: one facade for every caller tends to serve none well.
| If… | Use instead |
|---|---|
| One object's interface doesn't fit | 06 Adapter |
| The parts need to coordinate with each other, not just be called in order | 19 Mediator |
| The steps span services and must survive crashes | A saga or workflow engine (see the System design track) |
| Each caller needs different parts | Several small facades, one per job |
8. How it goes wrong
PedalService with eighty methods is a god object. Keep each facade to a few related jobs.start_rental are hard to find and reuse. The facade orchestrates; the parts decide.9. Remember it and say it
How this hook works: what “Ask reception” means
What the slogan means, phrase by phrase
- “Ask”
- A single high-level call.
- “reception”
- The facade.
- “knows the order”
- Sequencing and clean-up live in one place.
Why this picture fits: Hotel guests never phone housekeeping; reception coordinates for them.
The story → the concept
| The hotel | The code |
|---|---|
| Reception | RentalDesk |
| Bookings, deposits, keys, housekeeping | Accounts, Payments, Locks, Notifier |
| A guest | The phone app or kiosk |
| Staff using the back office | The monthly report reading Rides directly |
Watch it on an example
- Mia asks for a bike at D7.
- The desk checks her account, holds 100p, reserves E-205, opens the lock, starts RD-1 and notifies her.
- At D9 the lock jams: the desk puts the bike back and releases the hold.
The trap it saves you from
Copying a six-step sequence into every caller, each with its own clean-up bugs.
It still works when…
Yes: an SDK's "quick start" client, a service layer in a web app, and a library's top-level convenience functions.
In an interview or code review
- Say what it hides: "Starting a rental is six calls plus clean-up, so a
RentalDeskfacade does it once for all three channels." - Say what it doesn't: the subsystems stay public for reports and admin tools.
- Distinguish it: adapter changes one interface; facade simplifies many; mediator is something the parts talk through.
- Scale it up: across services, the same idea becomes an API gateway or a backend-for-frontend; if the steps must survive crashes, a saga.
10. Check yourself
What does a facade provide?
Can the monthly report still use Rides directly?
Fraud asks for a check before the hold. With a facade, how many places change?
Which is a sign the facade has gone wrong?
Facade or mediator: parts that must react to each other?
11. Go deeper
| Resource | Why |
|---|---|
| System design track | API gateways and backends-for-frontends: facades at service scale. |
| 06 Adapter | The pattern most often confused with facade. |
Python shutil | High-level file operations in front of the lower-level os calls. |
| Wikipedia: Facade pattern | Structure and examples. |