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.

Watch it evolve1 / 8
The problem

Two ways to start a trip

MobileApp and SupportDesk each find the booking, hold a payment, unlock the car and start the meter.

80
Start
Stage 1
Stage 2
Final

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().

The first versionJava
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");
    }
}
*«record»Booking− id: String− member: String− car: StringBookings− byId: Map<String, Booking>~ add(Booking)~ find(String): BookingPayments~ hold(String, int)Telematics~ unlock(String)~ startMeter(String)MobileApp− bookings: Bookings− payments: Payments− telematics: Telematics~ startTrip(String)SupportDesk− bookings: Bookings− payments: Payments− telematics: Telematics~ startTrip(String, String)

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:

The rule, added to the appJava
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

MobileApp

Four classes, which methods to call, in what order, and the license rule

SupportDesk

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.

Java
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());
    }
}
*«record»Booking− id: String− member: String− car: StringBookings− byId: Map<String, Booking>~ add(Booking)~ find(String): BookingLicenses− expired: Set<String>~ valid(String): booleanPayments~ hold(String, int)Telematics~ unlock(String)~ startMeter(String)TripStarter− bookings: Bookings− licenses: Licenses− payments: Payments− telematics: Telematics~ start(String)MobileApp− trips: TripStarter~ startTrip(String)SupportDesk− trips: TripStarter~ startTrip(String, String)

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.

Sequence in every client
2
Sequence in TripStarter
1

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.

*«record»Booking− id: String− member: String− car: StringBookings− byId: Map<String, Booking>~ add(Booking)~ find(String): BookingLicenses− expired: Set<String>~ valid(String): booleanPayments~ hold(String, int)Telematics~ unlock(String)~ startMeter(String)TripStarter− bookings: Bookings− licenses: Licenses− payments: Payments− telematics: Telematics~ start(String)MobileApp− trips: TripStarter~ startTrip(String)SupportDesk− trips: TripStarter~ startTrip(String, String)

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

TripStarter

Offers the task as one call, and makes the subsystem calls in the right order

Subsystem classes

Bookings, Licenses, Payments, Telematics

Do the actual work, each its own part, unaware of the facade

Clients

MobileApp, SupportDesk

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 TripStarter as 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 as TripStarter and TripEnder.
  • 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; CarShareHelper or Manager invites 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. TripStarter is 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 public, so only that package can use it

C++

Keep the subsystem classes in a detail namespace or in the facade's source file, out of the public header

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

1. The car-share adds a rule: new members pay a $100 hold instead of $50. Where does the change go?
2. A developer adds unlockCar(), refund(), rateTrip() and exportInvoices() to TripStarter. What is the risk?
3. How does a Facade differ from a Mediator?
4. After the refactor, the kiosk team calls Telematics.unlock() directly to save a step. What happens?

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.