Course contents

Facade — simple interface, complex subsystem

Sign in to run this lab and save your progress

Learn

Before you start

Facade is the most common "coordinator" pattern in real codebases. Almost every service class in a NestJS or Django app is basically a Facade — it holds a few injected collaborators and offers one method per user-facing action.

Recognize it, name it, don't over-engineer around it.

The one-line version

Facade gives you ONE method that runs multiple services in the right order with the right rules.

When to use Facade

Signals:

  • Multiple places do the same coordination. Every copy of a 5-service sequence is a Facade waiting to happen.
  • The steps have to happen in a specific ORDER. "Audit before returning failure." "Session before analytics." Those rules belong in ONE place.
  • Callers need only ONE thing to call — not five service classes.
  • You want the underlying services to stay testable on their own. Facade coordinates; it doesn't swallow.

If ANY of these fire, consider Facade. Especially if two or three fire together.

Where Facade shows up in real code

  • NestJS / Django service classesAuthService.login() coordinates repositories, hashers, session manager, event emitters. Textbook Facade.
  • Payment checkout flows — coordinate cart validation, inventory hold, payment, receipt, email, analytics.
  • Deploy scripts — coordinate build, upload, migrate, restart, notify.
  • Order-placement flows — coordinate stock check, payment, fulfillment scheduling, confirmation email.
  • API gateway endpoints — coordinate auth, rate limit, request forwarding, response transformation, logging.

Every one has the same shape: N services underneath, one method the caller invokes.

Facade vs the other "wrap-something" patterns

Pattern What it does
Adapter Translates shape A to shape B. One-to-one.
Facade Simplifies a whole subsystem behind one entry point. Many-to-one.
Decorator Adds behavior AROUND an object with the same shape. One-to-one.
Proxy Stands in for an object; controls access. One-to-one.

Facade is the "many-to-one" one. If you're wrapping ONE thing, it's not Facade — it's one of the other three.

Common mistakes

  1. God-class Facade. Facade with 40 methods, each doing 5 different things. You've re-created the god class Facade was supposed to prevent. Rule: one Facade per USE CASE, not one Facade per SUBSYSTEM.

  2. Facade absorbs the services. Instead of holding self._auth = auth and delegating, the Facade re-implements authenticate() inside. Now you can't test auth on its own. Breaks SRP.

  3. Facade builds its own services inside __init__. Now tests can't pass in fakes. DIP (module 2 lesson

    1. says: pass them in.
  4. Facade with too many methods. If your LoginFacade has .login(), .logout(), .reset_password(), .enroll_2fa() — that's four use cases. Consider four Facades or one AuthFacade grouped on purpose.

Where the rules go

Facade is where the coordination rules live. Examples from our lesson:

  • "Audit BEFORE returning failure" — one place.
  • "New device → send email + remember it" — both steps together.
  • "Analytics AFTER session creation" — event only fires for real logins.

Every rule is a business decision. Facade is where they get enforced without spreading to callers.

The interview payoff

When an interviewer asks "walk me through the login flow" and you say: "the LoginFacade coordinates auth, session, audit, mailer, and analytics — here's the sequence and here's why the order matters", you're showing:

  • You know Facade by name.
  • You know WHY the sequence has a specific order.
  • You know the services stay usable on their own.

That's a 30-second answer that gets you real design credit.

The decision cheat-sheet

Situation Pick
Multiple call sites, same sequence of N services Facade
Multiple call sites, different sequences Not Facade — probably a service layer with per-use-case methods (which is a family of Facades)
One call site, one-off coordination Just inline it
Shape mismatch to a single service Adapter (last lesson)
Services need to call EACH OTHER Mediator (not this course)

Your task

Facade is the pattern for when your callers have to coordinate five services to do one thing. You give them ONE method that does the coordination. Underneath, the complexity is still there — Facade just hides it behind a doorway.

The problem

A user logs in. Behind the scenes, five things happen:

  1. AuthService checks the password
  2. SessionService creates a session token
  3. AuditLog records the login attempt (success or failure)
  4. WelcomeMailer sends a "recently logged in from a new device" email (if the device is new)
  5. Analytics fires a user_logged_in event

Every caller that needs to "log a user in" has to coordinate all five:

# login handler
ok = auth.authenticate(email, password)
audit.record(email, "login_attempt", ok=ok)
if not ok:
    return None
token = session.create(email)
if device_id not in known_devices(email):
    welcome_mailer.send_new_device_email(email, device_id)
analytics.track("user_logged_in", email=email)
return token

Now:

  • The mobile app has the same block.
  • The admin panel's "impersonate user" flow has a variation.
  • The test suite copies this into 20 fixtures.

Five services touched at every call site. Add a sixth (say, SecurityAlerts for suspicious logins) → touch every place.

Questions to ask about the requirements

  1. How many places do the same coordination? One → just leave it inline. Many → a Facade pays off.
  2. Are the callers always doing the SAME sequence? If each caller does a slightly different mix, you need variations, not just a Facade.
  3. Do the underlying services need to stay usable on their own? (Yes — a test that only exercises AuditLog should still work. Facade coordinates; it doesn't replace.)
  4. Are the underlying services owned by different teams that might change their APIs? If yes, Facade protects you.
  5. Does the coordination have RULES? (Like "audit BEFORE returning failure, otherwise unauthorized attempts don't get logged.") Those rules live in the Facade.

For our problem: many call sites (login handler, mobile, admin, tests), same sequence, services stay usable on their own, rules exist (audit order matters). Textbook Facade.

Different designs, compared

Design A — leave the coordination inline everywhere

What we have now.

  • Good: Zero abstractions.
  • Bad: Every call site copies the sequence. New steps → edit every site. Rule violations (like forgetting to audit) become common. Tests can't run without faking 5 services.
  • When it wins: Only one call site.

Design B — one god class that OWNS all five services

Merge auth/session/audit/mailer/analytics into one LoginManager class that does everything.

  • Good: One class handles login.
  • Bad: Breaks SRP badly. LoginManager becomes a dumping ground. You can't test auth on its own anymore — it's tangled with session/audit/analytics. Every future login feature bloats this one class.
  • When it wins: Never for our shape.

Design C — Facade

class LoginFacade:
    def __init__(self, auth, session, audit, mailer, analytics):
        self._auth = auth
        self._session = session
        self._audit = audit
        self._mailer = mailer
        self._analytics = analytics

    def login(self, email, password, device_id):
        ok = self._auth.authenticate(email, password)
        self._audit.record(email, "login_attempt", ok=ok)
        if not ok:
            return None
        token = self._session.create(email)
        if not self._session.is_known_device(email, device_id):
            self._mailer.send_new_device_email(email, device_id)
        self._analytics.track("user_logged_in", email=email)
        return token

Callers just call login_facade.login(email, password, device). The five services still exist and can still be used on their own — the Facade just coordinates them.

  • Good: Callers depend on one thing. New steps go inside .login() — every caller gets them for free. Rules (like "audit before returning failure") live in one place. Underlying services stay testable on their own.
  • Bad: One extra class. Small extra work for small coordinations.
  • When it wins: Multiple call sites, same sequence, services stay useful on their own.

Design D — mediator between services

A mediator has services CALL BACK into it to notify each other. Different problem: coordinating messages going every direction. Facade is one-way (caller → facade → services). Mediator is many-way (services ↔ mediator ↔ services). Different pattern for a different problem — worth naming so you don't confuse them.

  • When Mediator wins: Chat systems where every user's action notifies every other user. Auction bidding. Not our login problem.

The locked-in choice: Design C (Facade)

Reviewing the requirements:

  • Many call sites. → Rules out A.
  • Services stay usable on their own. → Rules out B (which merges them into a god class).
  • Coordination is one-way (caller drives; services don't call back). → Rules out D (Mediator is overkill; we're not routing between services).

Design C wins by process of elimination — every alternative loses on a specific requirement.

Mechanics

class AuthService:
    _valid = {"alice@x.com": "pw123", "bob@x.com": "pw456"}
    def authenticate(self, email, password):
        return self._valid.get(email) == password


class SessionService:
    def __init__(self):
        self._tokens = {}
        self._known_devices = {}
    def create(self, email):
        token = f"session:{email}:token"
        self._tokens[email] = token
        return token
    def is_known_device(self, email, device_id):
        return device_id in self._known_devices.get(email, set())
    def remember_device(self, email, device_id):
        self._known_devices.setdefault(email, set()).add(device_id)


class AuditLog:
    def __init__(self):
        self.entries = []
    def record(self, email, event, ok):
        self.entries.append({"email": email, "event": event, "ok": ok})


class WelcomeMailer:
    def __init__(self):
        self.sent = []
    def send_new_device_email(self, email, device_id):
        self.sent.append((email, device_id))


class Analytics:
    def __init__(self):
        self.events = []
    def track(self, event, **payload):
        self.events.append({"event": event, "payload": payload})


class LoginFacade:
    def __init__(self, auth, session, audit, mailer, analytics):
        self._auth = auth
        self._session = session
        self._audit = audit
        self._mailer = mailer
        self._analytics = analytics

    def login(self, email, password, device_id):
        ok = self._auth.authenticate(email, password)
        self._audit.record(email, "login_attempt", ok=ok)
        if not ok:
            return None
        token = self._session.create(email)
        if not self._session.is_known_device(email, device_id):
            self._mailer.send_new_device_email(email, device_id)
            self._session.remember_device(email, device_id)
        self._analytics.track("user_logged_in", email=email)
        return token

Your task

In main.py build the five services and the LoginFacade exactly as sketched above.

Structural tests enforce:

  • LoginFacade accepts all five services via constructor (DIP — module 2 lesson 5 applies).
  • Successful login: token returned, audit logged with ok=True, analytics fired, mailer sent IF new device.
  • Failed login: None returned, audit logged with ok=False, NO session token created, NO analytics, NO mailer.
  • Second login from the SAME device: no duplicate welcome email.
  • Underlying services stay independently usable (test can call them directly without touching the facade).

Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
07-facade-starter