Low-level designCore9 min
Facade
Give a subsystem one simple entry point for a common task. The facade knows which classes to call and in what order, so every client does the task the same way, and a new rule lands in one place.
Two ways to start a trip
MobileApp and SupportDesk each find the booking, hold a payment, unlock the car and start the meter.
1 · The situation
Starting a trip takes four steps
A car-share service lets members book a car and start the trip from their phone. Starting a trip takes four steps across three classes: Bookings finds the booking, Payments places a $50 hold on the member's card, and Telematics unlocks the car and starts the meter. Each class does one job and knows nothing of the others.
Two clients start trips. MobileApp does it when a member taps Start, and SupportDesk does it when a member calls for help and an agent starts the trip for them. Each makes the same calls in the same order. Python writes start_trip() and start_meter().
import java.util.HashMap;
import java.util.Map;
record Booking(String id, String member, String car) {}
class Bookings {
private final Map<String, Booking> byId = new HashMap<>();
void add(Booking booking) {
byId.put(booking.id(), booking);
}
Booking find(String id) {
return byId.get(id);
}
}
class Payments {
void hold(String member, int dollars) {
System.out.println(" $" + dollars + " held on " + member + "'s card");
}
}
class Telematics {
void unlock(String car) {
System.out.println(" " + car + " unlocked");
}
void startMeter(String car) {
System.out.println(" meter running on " + car);
}
}
class MobileApp {
private final Bookings bookings;
private final Payments payments;
private final Telematics telematics;
MobileApp(Bookings bookings, Payments payments, Telematics telematics) {
this.bookings = bookings;
this.payments = payments;
this.telematics = telematics;
}
void startTrip(String bookingId) {
System.out.println("App: start " + bookingId);
Booking b = bookings.find(bookingId);
payments.hold(b.member(), 50);
telematics.unlock(b.car());
telematics.startMeter(b.car());
}
}
class SupportDesk {
private final Bookings bookings;
private final Payments payments;
private final Telematics telematics;
SupportDesk(Bookings bookings, Payments payments, Telematics telematics) {
this.bookings = bookings;
this.payments = payments;
this.telematics = telematics;
}
void startTrip(String bookingId, String agent) {
System.out.println("Desk (" + agent + "): start " + bookingId);
Booking b = bookings.find(bookingId);
payments.hold(b.member(), 50);
telematics.unlock(b.car());
telematics.startMeter(b.car());
}
}
public class Main {
public static void main(String[] args) {
Bookings bookings = new Bookings();
bookings.add(new Booking("B-1", "Ana", "K-12"));
bookings.add(new Booking("B-2", "Ben", "K-40"));
Payments payments = new Payments();
Telematics telematics = new Telematics();
MobileApp app = new MobileApp(bookings, payments, telematics);
SupportDesk desk = new SupportDesk(bookings, payments, telematics);
app.startTrip("B-1");
desk.startTrip("B-2", "Kim");
}
}The class diagram of this version, drawn from its code: both clients hold every subsystem class.
Ana starts her trip in the app, and an agent starts Ben's from the desk:
App: start B-1 $50 held on Ana's card K-12 unlocked meter running on K-12 Desk (Kim): start B-2 $50 held on Ben's card K-40 unlocked meter running on K-40
2 · The change
A new rule reaches one client of two
The insurer adds a rule: no trip starts for a member whose driver's license has expired. A new class, Licenses, knows whose license is valid. The app team adds the check to MobileApp:
class MobileApp {
private final Bookings bookings;
private final Licenses licenses;
private final Payments payments;
private final Telematics telematics;
MobileApp(Bookings bookings, Licenses licenses, Payments payments, Telematics telematics) {
this.bookings = bookings;
this.licenses = licenses;
this.payments = payments;
this.telematics = telematics;
}
void startTrip(String bookingId) {
System.out.println("App: start " + bookingId);
Booking b = bookings.find(bookingId);
if (!licenses.valid(b.member())) {
System.out.println(" refused: license expired");
return;
}
payments.hold(b.member(), 50);
telematics.unlock(b.car());
telematics.startMeter(b.car());
}
}Ben's license has expired. The app refuses him, so he calls support:
App: start B-1 $50 held on Ana's card K-12 unlocked meter running on K-12 App: start B-2 refused: license expired Desk (Kim): start B-2 $50 held on Ben's card K-40 unlocked meter running on K-40
The app refuses Ben, and the desk unlocks the car for him anyway.
Nobody told the support team. SupportDesk has its own copy of the sequence, and the copy has no license check. Each client knows the whole subsystem:
Client | What it has to know |
|---|---|
| Four classes, which methods to call, in what order, and the license rule |
| Three classes and the order, and nothing about licenses |
A self-service kiosk, planned next | All of it again |
The sequence is one piece of knowledge, how to start a trip, written out in every client. Every new rule, such as a deposit for new members or a check that the car is charged, has to be added to each copy, and a missed copy is a rule that doesn't hold.
3 · The refactor
Give the task one home
Start from the version with the license rule. Move the sequence, with the rule, into one class, and have the clients ask it:
TripStarter holds the four subsystem classes. Its start(bookingId) finds the booking, checks the license, places the hold, unlocks the car and starts the meter, in that order.
class TripStarter {
private final Bookings bookings;
private final Licenses licenses;
private final Payments payments;
private final Telematics telematics;
TripStarter(Bookings bookings, Licenses licenses, Payments payments, Telematics telematics) {
this.bookings = bookings;
this.licenses = licenses;
this.payments = payments;
this.telematics = telematics;
}
void start(String bookingId) {
Booking b = bookings.find(bookingId);
if (!licenses.valid(b.member())) {
System.out.println(" refused: license expired");
return;
}
payments.hold(b.member(), 50);
telematics.unlock(b.car());
telematics.startMeter(b.car());
}
}Now the rule holds at both doors:
App: start B-1 $50 held on Ana's card K-12 unlocked meter running on K-12 App: start B-2 refused: license expired Desk (Kim): start B-2 refused: license expired
Ben is refused in the app and at the desk.
Classes to edit for the next start rule. Before: every client, with a kiosk on the way. After: TripStarter alone.
4 · The pattern
Facade: one simple entry point to a subsystem
Facade puts a simple interface in front of a group of classes for the tasks clients actually need. The facade knows which classes take part and in what order to call them; the clients know only the facade. The classes behind it don't change, and don't know the facade exists.
The roles in the car-share code: TripStarter is the facade, the classes behind it the subsystem, and the app and the desk its clients.
Role | In the car-share code | Its job |
|---|---|---|
Facade |
| Offers the task as one call, and makes the subsystem calls in the right order |
Subsystem classes |
| Do the actual work, each its own part, unaware of the facade |
Clients |
| Ask the facade for the task |
One call from the client becomes the whole sequence; the booking lookup that starts it is left out to keep the figure small.
A facade doesn't add behavior to the subsystem classes, and it usually doesn't need an interface of its own: TripStarter is a plain class. Its value is in what it holds, the order of the steps and the rules that span more than one class.
5 · What it costs
One more class, and a pull toward doing everything
- One more layer. Reading a trip start now means opening
TripStarteras well as the classes it calls. - A facade can grow into a god class. If every task in the service goes through one
CarShareService, it collects every rule and changes for every reason. Give each task area its own facade, such asTripStarterandTripEnder. - Clients can go around it. If a client calls
Telematics.unlock()directly, the license rule is skipped again. Decide whether the subsystem stays open, and make it closed when a rule must hold.
Situation | Is a facade worth it? |
|---|---|
Several clients do the same task across several classes | Yes: one home for the steps and their rules |
One client does the task once | Not yet: a method in that client is enough |
Each client needs a different mix of the subsystem | Only for the common task; let the rest call the classes |
A third-party library with a sprawling interface | Yes: a small facade of your own keeps the rest of the code off it |
6 · Getting it right
Name it for the task, and keep it thin
- Name it for the task.
TripStarter.start()says what it's for;CarShareHelperorManagerinvites everything in. - Keep subsystem rules in the subsystem. Who counts as having a valid license belongs in
Licenses. The facade decides only that the check happens, and when. - Pass the subsystem in.
TripStarteris given its four classes, so a test can hand it fakes and check the order of the calls. - Close the door when a rule must hold. Let only the facade reach the subsystem, using what each language offers:
Language | Keeping the subsystem behind the facade |
|---|---|
Java | Put the subsystem in the facade's package, without |
C++ | Keep the subsystem classes in a |
JavaScript | Export only the facade from the module; the subsystem classes stay private to it |
Python | Name the subsystem modules or classes with a leading underscore, the convention for internal names |
7 · Look-alikes
A simpler front, and other fronts
Several patterns put one object in front of others. What tells them apart is who talks through it and what interface it offers:
Pattern | What it puts in front of | Its interface |
|---|---|---|
Facade | A whole subsystem, for clients | A new, simpler one, shaped by the tasks |
Adapter | One class whose interface doesn't fit | The one the client already expects |
Proxy | One object, to control access to it | The same as that object |
Mediator | A group of objects that talk to each other | One the group itself calls |
Facade and Mediator both sit in the middle of several classes. The difference is direction: the classes behind a facade never call it and don't know it exists, while the objects around a mediator call it to reach each other.
8 · In practice
Facades you have already used
Libraries often ship a facade over a larger set of classes. Python's shutil module offers high-level operations on files and folders, such as copying a whole tree, on top of the lower-level calls in os. Java's SLF4J is a simple facade for logging: code writes to its small API, and the actual logging framework behind it is chosen when the program is deployed.
The practice checks don't look for a facade. Every version on this page reads as clean, since the same sequence repeated in two clients isn't a finding: the duplication shows only when a rule arrives, which is why a facade earns its place when a task has rules of its own.
Check yourself
4 questions
Takeaways
Remember this
- A Facade gives a subsystem one simple entry point for a common task.
- The facade knows the steps and their order; the clients only know the task.
- A rule that spans several classes belongs in the facade, so it holds for every client.
- The classes behind a facade are unchanged and unaware of it.
- Split facades by task, or one facade grows into a god class.