Facade
Provides a unified, simplified interface to a set of interfaces in a subsystem, making the subsystem easier to use — without hiding or sealing off direct access to the subsystem when needed.
Intent
Simplify access to a complex subsystem by exposing a high-level interface that orchestrates the interactions between the internal components. The client uses the Facade for common use cases and accesses the subsystem directly only when it needs fine-grained control.
The Facade belongs to the structural patterns category in the GoF catalog (1994). It's one of the most intuitive patterns — you have probably already applied it without knowing: whenever you created a service class that orchestrates repositories, validators, and notifiers, you created a Facade.
Problem
Consider the process of completing a purchase (checkout) in an e-commerce store. This flow involves multiple subsystems: stock verification, payment processing, shipping calculation, order generation, confirmation e-mail, and report updates. Each subsystem has its own classes, dependencies, and rules.
Without a Facade, the controller or use-case code would need to know and orchestrate every subsystem directly — with all its nuances of call order and error handling. This creates high coupling between the client code and the implementation details of the subsystems.
As the system grows, any internal change to a subsystem (e.g.: migrating the payment gateway) requires changing every client that knows it directly.
The Facade is not an impenetrable barrier
A common misconception: the Facade completely hides the subsystem, preventing direct access. That's not the case. The Facade simplifies the common case — for advanced needs, the subsystem remains directly accessible. The Facade is a convenient entry point, not a wall.
Solution
The Facade organizes the code into two main groups:
-
Facade: a high-level class that knows which
subsystem classes are responsible for each part of the work. It
delegates client requests to the appropriate subsystem objects,
managing the call order and hiding the orchestration complexity.
E.g.:
CheckoutFacadewith the methodcheckout(). -
Subsystem classes: the classes that implement the
actual functionality. They don't know the Facade — it's the Facade
that knows them. E.g.:
StockService,PaymentService,ShippingService,EmailService.
The Facade can have one or several entry interfaces — high-level methods that represent the most common use cases. Each method orchestrates the correct sequence of calls to the subsystems.
Facade and Singleton
Facades are frequently implemented as Singletons — it makes sense to have just one instance of the orchestration layer. In applications with dependency injection containers (NestJS, Laravel, Spring), the container already handles the lifecycle; in simpler applications, it's common to see the Facade as an explicit Singleton or as a module (in Node.js/TypeScript, modules are naturally singletons through import caching).
Structure
Client code
│
│ facade.checkout(order)
▼
CheckoutFacade
┌───────────────────────────────────────────────────┐
│ - stock: StockService │
│ - payment: PaymentService │
│ - shipping: ShippingService │
│ - email: EmailService │
│ │
│ + checkout(order): CheckoutResult │
│ 1. stock.check(order.items) │
│ 2. payment.process(order.payment) │
│ 3. shipping.calculateAndSchedule(order.address) │
│ 4. email.sendConfirmation(order.customer) │
└───────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
Subsystems (independent, unaware of each other)
StockService PaymentService ShippingService EmailService
Note: the subsystems are still directly accessible
when needed — the Facade doesn't hide them.
Code examples
Example 1 — Order checkout Facade
The Facade orchestrates the stock, payment, shipping, and e-mail subsystems in a single high-level call. The client code doesn't need to know the order or the details of each subsystem.
// ── Subsystems — each with its own responsibility ─────────────
class StockService {
check(items: string[]): boolean {
console.log(`[Stock] Checking ${items.length} item(s)...`);
return true; // simulation: always available
}
reserve(items: string[]): void {
console.log(`[Stock] ${items.length} item(s) reserved.`);
}
}
class PaymentService {
process(amountCents: number, paymentMethod: string): string {
console.log(`[Payment] Processing $${(amountCents / 100).toFixed(2)} via ${paymentMethod}...`);
return `TXN-${Date.now()}`; // transaction ID
}
}
class ShippingService {
calculateAndSchedule(zipCode: string): { days: number; amountCents: number } {
console.log(`[Shipping] Calculating for ZIP ${zipCode}...`);
return { days: 5, amountCents: 1490 }; // 5 days, $14.90
}
}
class EmailService {
sendConfirmation(email: string, transactionId: string): void {
console.log(`[Email] Confirmation sent to ${email}. Transaction: ${transactionId}`);
}
}
// ── Domain types ─────────────────────────────────────────────
interface OrderData {
items: string[];
amountCents: number;
paymentMethod: string;
zipCode: string;
customerEmail: string;
}
interface CheckoutResult {
transactionId: string;
deliveryDays: number;
shippingAmountCents: number;
}
// ── Facade ───────────────────────────────────────────────────
class CheckoutFacade {
constructor(
private readonly stock: StockService,
private readonly payment: PaymentService,
private readonly shipping: ShippingService,
private readonly email: EmailService
) {}
// High-level interface — orchestrates the entire sequence:
checkout(order: OrderData): CheckoutResult {
// 1. Checks availability
if (!this.stock.check(order.items)) {
throw new Error("One or more items are unavailable.");
}
// 2. Processes payment
const transactionId = this.payment.process(
order.amountCents,
order.paymentMethod
);
// 3. Reserves stock and schedules delivery
this.stock.reserve(order.items);
const delivery = this.shipping.calculateAndSchedule(order.zipCode);
// 4. Notifies the customer
this.email.sendConfirmation(order.customerEmail, transactionId);
return {
transactionId,
deliveryDays: delivery.days,
shippingAmountCents: delivery.amountCents,
};
}
}
// ── Usage — the client only uses the Facade ────────────────────
const facade = new CheckoutFacade(
new StockService(),
new PaymentService(),
new ShippingService(),
new EmailService()
);
const result = facade.checkout({
items: ["GoF Book", "Notebook"],
amountCents: 9800,
paymentMethod: "credit_card",
zipCode: "01310-100",
customerEmail: "dev@example.com",
});
console.log(`Order completed! Transaction: ${result.transactionId}`);
console.log(`Delivery in ${result.deliveryDays} days.`);
console.log(`Shipping: $${(result.shippingAmountCents / 100).toFixed(2)}`);
// [Stock] Checking 2 item(s)...
// [Payment] Processing $98.00 via credit_card...
// [Stock] 2 item(s) reserved.
// [Shipping] Calculating for ZIP 01310-100...
// [Email] Confirmation sent to dev@example.com. Transaction: TXN-...
// Order completed! Transaction: TXN-...
// Delivery in 5 days.
// Shipping: $14.90
<?php
// ── Subsystems ───────────────────────────────────────────────
class StockService
{
/** @param string[] $items */
public function check(array $items): bool
{
echo '[Stock] Checking ' . count($items) . ' item(s)...' . PHP_EOL;
return true; // simulation: always available
}
/** @param string[] $items */
public function reserve(array $items): void
{
echo '[Stock] ' . count($items) . ' item(s) reserved.' . PHP_EOL;
}
}
class PaymentService
{
public function process(int $amountCents, string $paymentMethod): string
{
$amount = number_format($amountCents / 100, 2);
echo "[Payment] Processing \${$amount} via {$paymentMethod}..." . PHP_EOL;
return 'TXN-' . time();
}
}
class ShippingService
{
/** @return array{days: int, amountCents: int} */
public function calculateAndSchedule(string $zipCode): array
{
echo "[Shipping] Calculating for ZIP {$zipCode}..." . PHP_EOL;
return ['days' => 5, 'amountCents' => 1490]; // 5 days, $14.90
}
}
class EmailService
{
public function sendConfirmation(string $email, string $transactionId): void
{
echo "[Email] Confirmation sent to {$email}. Transaction: {$transactionId}" . PHP_EOL;
}
}
// ── Facade ───────────────────────────────────────────────────
class CheckoutFacade
{
public function __construct(
private readonly StockService $stock,
private readonly PaymentService $payment,
private readonly ShippingService $shipping,
private readonly EmailService $email
) {}
/**
* @param array{
* items: string[],
* amountCents: int,
* paymentMethod: string,
* zipCode: string,
* customerEmail: string
* } $order
* @return array{transactionId: string, deliveryDays: int, shippingAmountCents: int}
*/
public function checkout(array $order): array
{
// 1. Checks availability
if (!$this->stock->check($order['items'])) {
throw new \RuntimeException('One or more items are unavailable.');
}
// 2. Processes payment
$transactionId = $this->payment->process(
$order['amountCents'],
$order['paymentMethod']
);
// 3. Reserves stock and schedules delivery
$this->stock->reserve($order['items']);
$delivery = $this->shipping->calculateAndSchedule($order['zipCode']);
// 4. Notifies the customer
$this->email->sendConfirmation($order['customerEmail'], $transactionId);
return [
'transactionId' => $transactionId,
'deliveryDays' => $delivery['days'],
'shippingAmountCents' => $delivery['amountCents'],
];
}
}
// ── Usage — the client only uses the Facade ────────────────────
$facade = new CheckoutFacade(
new StockService(),
new PaymentService(),
new ShippingService(),
new EmailService()
);
$result = $facade->checkout([
'items' => ['GoF Book', 'Notebook'],
'amountCents' => 9800,
'paymentMethod' => 'credit_card',
'zipCode' => '01310-100',
'customerEmail' => 'dev@example.com',
]);
echo "Order completed! Transaction: {$result['transactionId']}" . PHP_EOL;
echo "Delivery in {$result['deliveryDays']} days." . PHP_EOL;
printf("Shipping: $%.2f\n", $result['shippingAmountCents'] / 100);
// [Stock] Checking 2 item(s)...
// [Payment] Processing $98.00 via credit_card...
// [Stock] 2 item(s) reserved.
// [Shipping] Calculating for ZIP 01310-100...
// [Email] Confirmation sent to dev@example.com. Transaction: TXN-...
// Order completed! Transaction: TXN-...
// Delivery in 5 days.
// Shipping: $14.90
When to use
- To simplify access to a complex subsystem: when client code needs to interact with many classes of a subsystem in a specific sequence, the Facade encapsulates that orchestration.
- To create abstraction layers between subsystems: in layered architectures (e.g.: Clean Architecture, Hexagonal), the Facade is the boundary between the application layer and the infrastructure subsystems.
- To reduce transitive dependencies: without a Facade, every client of the subsystem would have to import and know every internal class. With a Facade, clients only import it.
- To isolate the complexity of third-party libraries: a Facade over an external SDK protects the application code from changes in the library's API (similar to the Adapter, but focused on simplifying multiple calls, not interface compatibility).
When to avoid
- When the subsystem is already simple: a Facade over a single service with one or two methods is bureaucracy without benefit.
- When it becomes a "God Object": if the Facade accumulates business logic instead of just orchestrating, it violates the Single Responsibility Principle. The Facade should delegate — not decide.
- When it stifles flexibility: if clients always need fine-grained control over the subsystems, the Facade's abstraction layer gets in the way more than it helps.
Pros and cons
Pros
- Simplifies using the subsystem for the most common use cases.
- Reduces coupling between clients and the subsystem's internal details.
- Makes it easier to replace or evolve the subsystem internally — the Facade's clients don't need to change.
- Improves readability: the
checkout()method communicates the business flow without exposing the internal mechanics. - A single point to add cross-cutting concerns (logging, metrics, transactions) to the orchestrated flow.
Cons
- Can become a "God Object" if it absorbs business logic beyond orchestration.
- Can hide complexity the client sometimes needs to see and control.
- A Facade that covers only part of the subsystem forces the client to know both the Facade and the subsystem directly — an abstraction inconsistency.
Common pitfalls
1. Facade turns into a God Object with business logic
The most frequent pitfall: over time, business rules migrate into the Facade ("if the order is over $200, free shipping", "if it's a premium customer, apply a discount"). The Facade starts accumulating responsibilities that belong to the domain. Keep the Facade as a pure orchestrator — business rules belong to domain entities and services, not the facade layer.
2. Confusing Facade with Adapter
The Adapter converts the interface of a single class into another interface expected by the client. The Facade provides a simplified interface to a set of classes. The Facade doesn't need the internal interfaces to be incompatible — it simplifies, it doesn't adapt.
3. A Facade that hides nothing
Warning: a Facade that is only a pass-through (just
forwards calls without any orchestration or simplification) is an
unnecessary indirection layer. If the Facade's method is just
return this.service.method(params), without adding any
coordination, remove the Facade and access the service directly.
4. Testing the Facade instead of the subsystems
Unit tests should test the subsystems individually — not the entire Facade. The Facade should have an integration (or orchestration) test that verifies the sequence of calls with mocks of the subsystems. Don't write unit tests that run the real subsystems through the Facade — that mixes testing levels and makes the tests fragile.
Related patterns
The Facade interacts with other structural and creational patterns:
The Adapter changes the interface of one class into another; the Facade provides a simplified interface to a subsystem — the intentions are distinct, but both create a single entry point. The Decorator adds responsibilities to an object while keeping the same interface; the Facade can internally compose Decorators over the subsystems, but the Facade itself doesn't need to keep the subsystems' interface. The Singleton is frequently combined with the Facade: it makes sense that only one instance of the orchestration layer exists in the application. The Mediator also centralizes communication between objects, but with a different focus: the Mediator manages the bidirectional interaction between components that know each other mutually (reducing cross-dependencies); the Facade manages the unidirectional access of external clients to a subsystem that doesn't know the client.