Skip to content
aipatterns.dev

Design patternStructural Patterns

Facade

Give a complex subsystem one small entry point for a common task, so callers stop orchestrating its classes and calling them in the right order.

Complexity
Low
Seen in the wild
Medium
Also known as
Service Layer, Gateway

Intent

Provide a simple interface to a set of classes that must be called together, in order, with matching arguments. The facade owns the orchestration for one use case. Callers ask for the outcome ("hire this person") instead of driving five subsystems themselves. The subsystems stay public for callers that need fine control.

Forces

  • A one-line request vs a multi-step procedure: onboarding means an account, group access, payroll and a laptop, in that order, with cleanup if a step fails.
  • Duplicated orchestration vs one source of truth: the HR portal, the CLI and the bulk-import job each copy the sequence and drift apart.
  • Hiding complexity vs keeping control: most callers want the default path; a few need to call one subsystem directly.
  • Coupling vs convenience: callers that import every subsystem break when any of them changes.

Therefore

Create a class (or module) with one method per use case that calls the subsystem classes in the right order, handles their failures, and returns a result in the caller's terms.

The facade adds no new capability. It only sequences what already exists. Subsystems don't know the facade exists, and nothing stops a caller from bypassing it. Keep each facade focused on one area; several small facades beat one that fronts the whole system.

Structure

Diagram source (Mermaid)
classDiagram
  class HrPortal {
    +submit(form) void
  }
  class Onboarding {
    +hire(newHire) OnboardingResult
  }
  class Directory {
    +createAccount(name) String
    +disable(email) void
  }
  class AccessControl {
    +grant(email, groups) void
  }
  class Payroll {
    +enroll(email, salary) String
  }
  class Equipment {
    +orderLaptop(email, model) String
  }
  HrPortal --> Onboarding
  Onboarding --> Directory
  Onboarding --> AccessControl
  Onboarding --> Payroll
  Onboarding --> Equipment

Participants

  • Facade (Onboarding): knows which subsystem classes handle each step, calls them in order, and handles partial failure.
  • Subsystem classes (Directory, AccessControl, Payroll, Equipment): do the real work and have no reference to the facade.
  • Client (HrPortal): calls the facade for the common path and may still use a subsystem directly for special cases.

Use it when

  • Several callers repeat the same sequence of calls across the same group of classes.
  • A subsystem is hard to use correctly: required call order, shared IDs passed between steps, cleanup on failure.
  • You want to layer a system, so the upper layer talks to one entry point per lower-level area.
  • You are wrapping a sprawling library (a media toolkit, a cloud SDK) and your code uses a small slice of it.
  • You are preparing to replace a subsystem and want to shrink the surface that depends on it first.

Do not use it when

  • The "facade" would front the whole application. One class with forty unrelated methods is a god object (Large Class). Write one facade per use-case area instead.
  • Each facade method forwards to exactly one subsystem call. That is a Middle Man; delete it (Remove Middle Man) and let callers use the class.
  • There is one caller. Put the sequence in a plain function next to that caller; promote it when a second caller appears.
  • Callers need fine-grained control over most steps. A facade that grows a flag per step (skipPayroll, laptopModel, noGroups) is hiding nothing; expose the subsystems.
  • You need to make one class fit an existing interface. That is Adapter.
  • Subsystem objects need to talk to each other in both directions. That is Mediator; a facade only calls down.

Application grill

Inspect first: the orchestration callers repeat today (setup, calls in order, teardown) and how many call sites repeat it; whether the subsystem is one package (a facade is one module) or scattered (a facade may hide that).

Missing pieces: none beyond the language: one module with one exported function per common task.

Ask before you build:

  • Scale: how many call sites repeat the orchestration - three or more makes the facade earn itself?
  • Performance: does the one-call convenience hide N+1 work inside that callers used to see and batch?
  • Reliability: which subsystem failures does the facade translate, and which bubble up untouched?
  • Budget: a facade that accumulates business logic stops being a facade - who keeps it thin?
  • Implementation: do tests exercise the facade end to end, or each subsystem separately?

Implementation steps

  1. Find the duplicated orchestration: the same calls, in the same order, in several callers.
  2. Write the facade method signature in the caller's terms: what goes in and what outcome comes back.
  3. Move one copy of the sequence into the facade, with the subsystems injected through its constructor.
  4. Add the failure handling the callers were each doing (or forgetting): compensation, retries, consistent errors.
  5. Replace each caller's copy with a call to the facade, running tests after each.
  6. Leave the subsystems accessible. Remove direct subsystem imports only from callers that no longer need them.

Example

Three tools (the HR portal, a CLI and a CSV import job) each onboarded new employees by calling four internal services. Two of them forgot to disable the account when payroll enrollment failed.

type Department = "engineering" | "sales";
interface NewHire { fullName: string; department: Department; annualSalary: number; laptop: string }
interface OnboardingResult { email: string; employeeId: string; laptopTicket: string }

// --- Subsystems: each one works alone and knows nothing of the facade ---
class Directory {
  createAccount(fullName: string): string {
    const email = `${fullName.toLowerCase().replace(/\s+/g, ".")}@acme.io`;
    console.log(`[directory] created ${email}`);
    return email;
  }
  disable(email: string): void { console.log(`[directory] disabled ${email}`); }
}

class AccessControl {
  grant(email: string, groups: string[]): void { console.log(`[access] ${email}: ${groups.join(", ")}`); }
}

class Payroll {
  private nextId = 1001;
  enroll(email: string, annualSalary: number): string {
    if (annualSalary <= 0) throw new Error(`payroll rejected ${email}: invalid salary`);
    console.log(`[payroll] enrolled ${email}`);
    return `EMP-${this.nextId++}`;
  }
}

class Equipment {
  private nextTicket = 500;
  orderLaptop(email: string, model: string): string {
    console.log(`[it] ${model} for ${email}`);
    return `IT-${this.nextTicket++}`;
  }
}

// --- Facade ---
const GROUPS: Record<Department, string[]> = {
  engineering: ["github", "aws-dev", "oncall"],
  sales: ["crm", "slack-sales"],
};

class Onboarding {
  constructor(
    private readonly directory: Directory,
    private readonly access: AccessControl,
    private readonly payroll: Payroll,
    private readonly equipment: Equipment,
  ) {}

  hire(h: NewHire): OnboardingResult {
    const email = this.directory.createAccount(h.fullName);
    let employeeId: string;
    try {
      employeeId = this.payroll.enroll(email, h.annualSalary);
    } catch (e) {
      this.directory.disable(email); // no orphan accounts
      throw e;
    }
    this.access.grant(email, GROUPS[h.department]);
    const laptopTicket = this.equipment.orderLaptop(email, h.laptop);
    return { email, employeeId, laptopTicket };
  }
}

const onboarding = new Onboarding(new Directory(), new AccessControl(), new Payroll(), new Equipment());
console.log(onboarding.hire({ fullName: "Dana Reyes", department: "engineering", annualSalary: 132000, laptop: "MBP 14" }));
try {
  onboarding.hire({ fullName: "Sam Ito", department: "sales", annualSalary: 0, laptop: "XPS 13" });
} catch (e) {
  console.log((e as Error).message);
}

The facade fixes the bug once: every caller now gets the compensation step. The IT team's asset tool still calls Equipment directly to order replacement laptops, and that is fine.

Trade-offs

Gains

  • One correct implementation of a multi-step procedure instead of several drifting copies.
  • Callers depend on one entry point, not on every subsystem class.
  • A natural seam for tests (fake the facade) and for swapping subsystems later.
  • Failure handling and ordering rules live in one place.

Costs

  • A facade attracts every new "just add it here" method and can grow into a god object.
  • It can hide useful subsystem features, pushing callers to bypass it inconsistently.
  • An extra layer to read when the sequence is short.
  • The facade becomes a coupling point to every subsystem it fronts; changes in any of them land here.

Relations

  • Adapter makes one existing class match an interface someone else defined. A facade defines a new, simpler interface over several classes.
  • Mediator also centralizes coordination, but its colleagues know it and talk through it. Subsystems never know about a facade.
  • Abstract Factory can sit behind a facade to hide how subsystem objects are created.
  • Singleton is often misused for facades; one instance is usually enough, but inject it rather than reaching for a global.
  • Proxy keeps its subject's interface exactly. A facade offers a different, smaller interface.

Agent checklist

  • At least two callers duplicated the orchestration, or the sequence has a correctness rule (order, cleanup) worth enforcing once.
  • Each facade method represents a use case and does more than forward one call.
  • Subsystems are injected into the facade and have no reference back to it.
  • Partial failures are handled (compensation or a clear error), and tested.
  • The facade covers one area; you split it before it reaches unrelated responsibilities.
  • Callers with special needs can still reach the subsystems directly.

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.