InterviewKata
Design patterns · 08 of 26 · Fitting objects together

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

At a hotel, you ask reception for a room. Behind the desk, someone checks the booking, takes a card deposit, assigns a room, codes a key and tells housekeeping. You never phone housekeeping yourself. Reception is one counter in front of many departments, and it knows the order things happen in.

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

RoleIts jobIn Pedal
FacadeOffers a few high-level operations and calls the subsystem in the right order.RentalDesk.start_rental()
Subsystem classesDo the real work; don't know the facade exists.Accounts, Payments, Docks, Locks, Rides, Notifier
ClientUses 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 changeWithout the patternWith it
Add a fraud check before the holdThree copies, three teamsOne line in start_rental
Fix the jam clean-upFind which copies got it wrongAlready right, in one place
A fourth channel (a partner's app)A fourth copy of six callsOne 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 fit06 Adapter
The parts need to coordinate with each other, not just be called in order19 Mediator
The steps span services and must survive crashesA saga or workflow engine (see the System design track)
Each caller needs different partsSeveral small facades, one per job

8. How it goes wrong

A facade for everythingOne PedalService with eighty methods is a god object. Keep each facade to a few related jobs.
Business rules hidden in the facadePricing decisions in start_rental are hard to find and reuse. The facade orchestrates; the parts decide.
Forbidding direct accessA facade is a convenience, not a wall. If you need a boundary, that's a module or service interface.
Facade leaking subsystem typesReturning the dock's internal record forces callers to learn the subsystem anyway. Return what callers need.

9. Remember it and say it

Memory hook: "Ask reception"One counter in front of many departments, and it knows the order.
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 hotelThe code
ReceptionRentalDesk
Bookings, deposits, keys, housekeepingAccounts, Payments, Locks, Notifier
A guestThe phone app or kiosk
Staff using the back officeThe monthly report reading Rides directly

Watch it on an example

  1. Mia asks for a bike at D7.
  2. The desk checks her account, holds 100p, reserves E-205, opens the lock, starts RD-1 and notifies her.
  3. 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

10. Check yourself

What does a facade provide?

It orchestrates the subsystem in the right order.

Can the monthly report still use Rides directly?

Power users can reach the parts.

Fraud asks for a check before the hold. With a facade, how many places change?

The sequence lives in start_rental().

Which is a sign the facade has gone wrong?

That's a god object hiding business logic.

Facade or mediator: parts that must react to each other?

A facade is called by outsiders; a mediator is used by the parts themselves.

11. Go deeper

ResourceWhy
System design trackAPI gateways and backends-for-frontends: facades at service scale.
06 AdapterThe pattern most often confused with facade.
Python shutilHigh-level file operations in front of the lower-level os calls.
Wikipedia: Facade patternStructure and examples.