Skip to contentJMRG
All posts

Facade Pattern: Simplifying Complex Systems

design-patternsstructuralfacade

What is the Facade Pattern?

The Facade pattern provides a simplified interface to a complex set of classes, library, or framework. It hides the complexity of subsystems behind a single class that exposes only the operations the client needs.

classDiagram
    class Facade {
        +operation()
    }

    class SubsystemA {
        +operationA()
    }

    class SubsystemB {
        +operationB()
    }

    class SubsystemC {
        +operationC()
    }

    class Client

    Client --> Facade
    Facade --> SubsystemA
    Facade --> SubsystemB
    Facade --> SubsystemC

When to Use Facade

  • Simplify access to a complex system
  • Decouple client code from subsystems
  • Provide a unified entry point
  • Organize subsystems into layers

The Problem

In an e-commerce system, we have multiple independent services that the client must coordinate:

// Without Facade - Client must coordinate everything manually
const inventory = new InventoryService();
const payment = new PaymentService();
const shipping = new ShippingService();
const notification = new NotificationService();

// Check stock
for (const item of items) {
  const inStock = await inventory.checkStock(item.productId, item.quantity);
  if (!inStock) throw new Error('Out of stock');
  reservations.push(await inventory.reserveStock(item.productId, item.quantity));
}

// Calculate shipping
const shippingOptions = await shipping.calculateShipping(address, items);
const selectedShipping = shippingOptions[0];

// Process payment
const authId = await payment.authorize(total, 'EUR', paymentMethod);
await payment.capture(authId);

// ... more complex steps ...

The Solution: Facade

1. Identify the Subsystems

class InventoryService {
  async checkStock(productId: string, quantity: number): Promise<boolean> { }
  async reserveStock(productId: string, quantity: number): Promise<string> { }
  async confirmReservation(reservationId: string): Promise<void> { }
  async releaseReservation(reservationId: string): Promise<void> { }
}

class PaymentService {
  async authorize(amount: number, currency: string, method: string): Promise<string> { }
  async capture(authorizationId: string): Promise<string> { }
  async voidAuthorization(authorizationId: string): Promise<void> { }
}

class ShippingService {
  async calculateShipping(address: Address, items: OrderItem[]): Promise<ShippingOption[]> { }
  async createShipment(orderId: string, address: Address, carrier: string): Promise<string> { }
}

class NotificationService {
  async sendOrderConfirmation(email: string, orderId: string): Promise<void> { }
  async sendPaymentReceipt(email: string, amount: number): Promise<void> { }
}

2. Define the Simplified Interface

The client only needs one operation:

interface OrderRequest {
  userId: string;
  email: string;
  items: Array<{ productId: string; quantity: number; price: number }>;
  shippingAddress: Address;
  paymentMethod: string;
}

interface OrderResult {
  orderId: string;
  total: number;
  shippingCost: number;
  estimatedDelivery: Date;
  trackingId?: string;
}

3. Implement the Facade

The Facade coordinates all subsystems:

class OrderFacade {
  private inventory = new InventoryService();
  private payment = new PaymentService();
  private shipping = new ShippingService();
  private notification = new NotificationService();

  async placeOrder(request: OrderRequest): Promise<OrderResult> {
    const reservations: string[] = [];
    let authorizationId: string | null = null;

    try {
      // 1. Check stock and reserve
      for (const item of request.items) {
        const inStock = await this.inventory.checkStock(item.productId, item.quantity);
        if (!inStock) {
          throw new Error(`Product ${item.productId} out of stock`);
        }
        const reservationId = await this.inventory.reserveStock(item.productId, item.quantity);
        reservations.push(reservationId);
      }

      // 2. Calculate totals
      const subtotal = request.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
      const shippingOptions = await this.shipping.calculateShipping(
        request.shippingAddress,
        request.items
      );
      const shippingCost = shippingOptions[0].cost;
      const total = subtotal + shippingCost;

      // 3. Process payment
      authorizationId = await this.payment.authorize(total, 'EUR', request.paymentMethod);
      await this.payment.capture(authorizationId);

      // 4. Confirm reservations
      for (const reservationId of reservations) {
        await this.inventory.confirmReservation(reservationId);
      }

      // 5. Create shipment
      const orderId = `ORD-${Date.now()}`;
      const trackingId = await this.shipping.createShipment(
        orderId,
        request.shippingAddress,
        shippingOptions[0].carrier
      );

      // 6. Send notifications
      await this.notification.sendOrderConfirmation(request.email, orderId);
      await this.notification.sendPaymentReceipt(request.email, total);

      return {
        orderId,
        total,
        shippingCost,
        estimatedDelivery: shippingOptions[0].estimatedDelivery,
        trackingId,
      };
    } catch (error) {
      // Rollback on error
      for (const reservationId of reservations) {
        await this.inventory.releaseReservation(reservationId);
      }
      if (authorizationId) {
        await this.payment.voidAuthorization(authorizationId);
      }
      throw error;
    }
  }
}

4. Use the Facade

// With Facade - Single call
const facade = new OrderFacade();

const result = await facade.placeOrder({
  userId: 'user_123',
  email: '[email protected]',
  items: [
    { productId: 'PROD_001', quantity: 2, price: 29.99 },
  ],
  shippingAddress: {
    street: '123 Main Street',
    city: 'Madrid',
    country: 'Spain',
    postalCode: '28001',
  },
  paymentMethod: 'card_visa_****1234',
});

console.log(`Order ${result.orderId} completed!`);

Benefits of the Facade Pattern

Benefit Description
Simplicity Client only knows one interface
Decoupling Changes in subsystems don't affect client
Maintainability Coordination logic centralized
Testability Easy to mock for tests

Summary

Aspect Description
Purpose Simplify access to complex systems
Components Facade, Subsystems, Client
Advantage Decoupling, single entry point
Disadvantage Can become "god object" if it grows too much

Conclusion

The Facade pattern is essential for simplifying complex systems and creating clean APIs. It's widely used in libraries and frameworks to hide internal complexity.

In the next article we'll explore the State Pattern, which allows changing an object's behavior based on its internal state.


Based on "Design Patterns: Elements of Reusable Object-Oriented Software" (Gang of Four).