Facade — simplifying a complex subsystem (the pattern, vs Laravel Facade)
Concept
Facade pattern (GoF) provides a simple, unified interface to a complex subsystem. It doesn't add functionality — it simplifies access by hiding complexity behind a clean surface.
The confusion with Laravel Facades: Laravel uses the term "Facade" for something different — a static proxy to a service container binding. Cache::get('key') appears to be a static call but actually resolves a service from the container and calls get() on it. This is better described as a static proxy or service locator, not the GoF Facade pattern (though it serves a similar simplification purpose).
GoF Facade pattern: A class that wraps a subsystem. Users interact only with the Facade. The Facade coordinates the subsystem's components. The subsystem components aren't hidden — you CAN access them directly — but most users don't need to.
Example: An OrderFacade wraps: inventory check, payment processing, order persistence, notification dispatch, and invoice generation. The caller does $facade->placeOrder($cart) instead of orchestrating all 5 subsystems manually.
Facade vs Service class: In modern application architecture, a "Service" class plays the same role as a GoF Facade. The distinction is academic — both simplify access to complex subsystems.
Facade vs Adapter: Facade simplifies a complex interface; Adapter makes an incompatible interface compatible. Facade creates a NEW simple interface. Adapter wraps to match an EXISTING interface.
When NOT to use Facade: If the subsystem already has a simple interface, adding a Facade is just extra layers. Don't create Facades for Facades.
Code Example
<?php
// Subsystem classes — complex individually
class InventorySystem
{
public function checkStock(int $productId, int $qty): bool { return true; }
public function reserveStock(int $productId, int $qty): void { }
public function releaseReservation(int $productId, int $qty): void { }
}
class PaymentSystem
{
public function charge(string $paymentMethodId, int $amountCents): array
{
return ['id' => 'pay_123', 'status' => 'succeeded'];
}
public function refund(string $paymentId): void { }
}
class OrderRepository
{
public function create(array $data): int { return 1; }
public function update(int $id, array $data): void { }
}
class NotificationSystem
{
public function sendOrderConfirmation(int $userId, int $orderId): void { }
}
// Facade — simplifies the complex ordering process
class OrderFacade
{
public function __construct(
private readonly InventorySystem $inventory,
private readonly PaymentSystem $payment,
private readonly OrderRepository $orders,
private readonly NotificationSystem $notifications,
) {}
public function placeOrder(int $userId, string $paymentMethodId, array $items): int
{
// Coordinate the subsystems
foreach ($items as $item) {
if (!$this->inventory->checkStock($item['product_id'], $item['quantity'])) {
throw new \DomainException("Product {$item['product_id']} is out of stock.");
}
}
foreach ($items as $item) {
$this->inventory->reserveStock($item['product_id'], $item['quantity']);
}
$total = $this->calculateTotal($items);
$charge = $this->payment->charge($paymentMethodId, $total);
$orderId = $this->orders->create([
'user_id' => $userId,
'payment_id' => $charge['id'],
'total' => $total,
'items' => $items,
]);
$this->notifications->sendOrderConfirmation($userId, $orderId);
return $orderId;
}
private function calculateTotal(array $items): int
{
return array_sum(array_column($items, 'price')) * 100; // cents
}
}
// Controller — simple call through Facade
class OrderController
{
public function __construct(private readonly OrderFacade $orderFacade) {}
public function store(Request $request): JsonResponse
{
$orderId = $this->orderFacade->placeOrder(
auth()->id(),
$request->input('payment_method'),
$request->input('items')
);
return response()->json(['order_id' => $orderId], 201);
}
}
// Laravel's "Facade" is a DIFFERENT concept — a static proxy:
// Cache::get('key') → app('cache')->get('key') → resolves from service container