Episode 9 — System Design / 9.4 — Structural Design Patterns

9.4.b -- Facade Pattern

In one sentence: The Facade pattern provides a simplified, unified interface to a complex subsystem of classes -- like a hotel concierge who handles restaurant reservations, taxi bookings, and theater tickets through a single conversation instead of you calling each service yourself.

Navigation: <- 9.4.a -- Adapter | 9.4.c -- Proxy ->


Table of Contents


1. What is the Facade Pattern?

A Facade is a structural pattern that provides a simple interface to a library, framework, or any complex set of classes. Instead of forcing clients to interact with dozens of objects, the facade exposes only what the client actually needs.

  WITHOUT FACADE:
  ┌────────┐     ┌──────────┐
  │        │────>│ SMTP     │
  │        │     └──────────┘
  │        │     ┌──────────┐
  │ Client │────>│ Template │     Client must know all subsystems
  │        │     └──────────┘     and coordinate them correctly
  │        │     ┌──────────┐
  │        │────>│ Queue    │
  │        │     └──────────┘
  │        │     ┌──────────┐
  │        │────>│ Logger   │
  └────────┘     └──────────┘

  WITH FACADE:
  ┌────────┐     ┌──────────────┐     ┌──────────┐
  │        │     │              │────>│ SMTP     │
  │        │     │              │     └──────────┘
  │ Client │────>│  EmailFacade │     ┌──────────┐
  │        │     │              │────>│ Template │
  │        │     │              │     └──────────┘
  └────────┘     │              │     ┌──────────┐
                 │              │────>│ Queue    │
                 │              │     └──────────┘
                 │              │     ┌──────────┐
                 │              │────>│ Logger   │
                 └──────────────┘     └──────────┘
RoleWhat it does
FacadeKnows which subsystem classes handle which requests; delegates client calls to appropriate subsystem objects
Subsystem classesImplement actual functionality; have no knowledge of the facade
ClientUses the facade instead of calling subsystem objects directly

Key point: The facade does NOT prevent direct access to subsystems. Clients who need fine-grained control can still reach the subsystem directly.


2. The Problem It Solves

Imagine sending a transactional email. Without a facade, every caller must:

  1. Load the correct email template
  2. Compile the template with dynamic data
  3. Validate the recipient email address
  4. Configure SMTP connection settings
  5. Handle attachments
  6. Queue the email for delivery
  7. Log the send attempt
  8. Handle failures and retries

That is 8 steps -- and every part of your codebase that sends email must do all of them correctly:

  ┌─────────────────────────────────────────────────────────────┐
  │  WITHOUT FACADE: Every caller repeats the same 8 steps     │
  │                                                             │
  │  OrderService:      loads template, compiles, validates,    │
  │                     configures SMTP, queues, logs...        │
  │                                                             │
  │  AuthService:       loads template, compiles, validates,    │
  │                     configures SMTP, queues, logs...        │
  │                                                             │
  │  NotificationSvc:   loads template, compiles, validates,    │
  │                     configures SMTP, queues, logs...        │
  │                                                             │
  │  Problem: duplicated logic, easy to get wrong, hard to     │
  │           change (must update every caller)                 │
  └─────────────────────────────────────────────────────────────┘

3. Structure and Participants

  ┌─────────────────────────────────────────┐
  │              EmailFacade                │
  │─────────────────────────────────────────│
  │ - templateEngine: TemplateEngine        │
  │ - smtpClient: SmtpClient               │
  │ - emailQueue: EmailQueue                │
  │ - validator: EmailValidator             │
  │ - logger: Logger                        │
  │─────────────────────────────────────────│
  │ + sendWelcomeEmail(user)                │
  │ + sendOrderConfirmation(order)          │
  │ + sendPasswordReset(user, token)        │
  │ + sendBulkNewsletter(users, content)    │
  └─────────┬───────────────────────────────┘
            │ delegates to
            │
  ┌─────────┼───────────┬──────────────┬─────────────┐
  │         │           │              │             │
  ▼         ▼           ▼              ▼             ▼
┌────────┐ ┌────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐
│Template│ │  SMTP  │ │  Email   │ │  Email   │ │ Logger │
│ Engine │ │ Client │ │  Queue   │ │Validator │ │        │
└────────┘ └────────┘ └──────────┘ └──────────┘ └────────┘

4. Email Sending Facade -- Full Example

// ============================================================
// EMAIL SENDING FACADE -- FULL IMPLEMENTATION
// ============================================================

// ---- SUBSYSTEM 1: Template Engine ----

class TemplateEngine {
  constructor() {
    this.templates = new Map([
      ['welcome', {
        subject: 'Welcome to {{appName}}, {{name}}!',
        body: `Hi {{name}},\n\nWelcome to {{appName}}! Your account is ready.\n\nBest,\nThe Team`,
      }],
      ['order-confirmation', {
        subject: 'Order #{{orderId}} Confirmed',
        body: `Hi {{name}},\n\nYour order #{{orderId}} for ${{total}} has been confirmed.\nEstimated delivery: {{deliveryDate}}.\n\nThank you!`,
      }],
      ['password-reset', {
        subject: 'Password Reset Request',
        body: `Hi {{name}},\n\nClick below to reset your password:\n{{resetLink}}\n\nThis link expires in {{expiry}} minutes.`,
      }],
    ]);
  }

  getTemplate(templateName) {
    const template = this.templates.get(templateName);
    if (!template) {
      throw new Error(`Template "${templateName}" not found`);
    }
    return { ...template };
  }

  compile(template, data) {
    let subject = template.subject;
    let body = template.body;

    for (const [key, value] of Object.entries(data)) {
      const placeholder = `{{${key}}}`;
      subject = subject.split(placeholder).join(value);
      body = body.split(placeholder).join(value);
    }

    return { subject, body };
  }
}


// ---- SUBSYSTEM 2: Email Validator ----

class EmailValidator {
  validate(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(email)) {
      throw new Error(`Invalid email address: ${email}`);
    }
    return true;
  }

  validateBulk(emails) {
    const valid = [];
    const invalid = [];
    for (const email of emails) {
      try {
        this.validate(email);
        valid.push(email);
      } catch {
        invalid.push(email);
      }
    }
    return { valid, invalid };
  }
}


// ---- SUBSYSTEM 3: SMTP Client ----

class SmtpClient {
  constructor(config) {
    this.host = config.host;
    this.port = config.port;
    this.secure = config.secure;
    this.auth = config.auth;
    this.connected = false;
  }

  connect() {
    console.log(`  [SMTP] Connecting to ${this.host}:${this.port}...`);
    this.connected = true;
  }

  disconnect() {
    console.log('  [SMTP] Disconnecting...');
    this.connected = false;
  }

  send(mailOptions) {
    if (!this.connected) this.connect();
    console.log(`  [SMTP] Sending to ${mailOptions.to}: "${mailOptions.subject}"`);
    return {
      messageId: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
      accepted: [mailOptions.to],
      rejected: [],
    };
  }
}


// ---- SUBSYSTEM 4: Email Queue ----

class EmailQueue {
  constructor() {
    this.queue = [];
    this.processing = false;
  }

  enqueue(emailJob) {
    this.queue.push({
      ...emailJob,
      id: `job_${Date.now()}`,
      status: 'queued',
      attempts: 0,
      maxRetries: 3,
    });
    console.log(`  [Queue] Job enqueued. Queue size: ${this.queue.length}`);
    return this.queue[this.queue.length - 1];
  }

  async processNext(sendFn) {
    if (this.queue.length === 0) return null;
    const job = this.queue.shift();
    job.status = 'processing';
    job.attempts++;

    try {
      const result = await sendFn(job);
      job.status = 'sent';
      console.log(`  [Queue] Job ${job.id} sent successfully`);
      return result;
    } catch (error) {
      if (job.attempts < job.maxRetries) {
        job.status = 'queued';
        this.queue.push(job); // re-queue for retry
        console.log(`  [Queue] Job ${job.id} failed, retrying (${job.attempts}/${job.maxRetries})`);
      } else {
        job.status = 'failed';
        console.log(`  [Queue] Job ${job.id} permanently failed`);
      }
      return null;
    }
  }
}


// ---- SUBSYSTEM 5: Logger ----

class EmailLogger {
  log(level, message, meta = {}) {
    const timestamp = new Date().toISOString();
    console.log(`  [EmailLog ${level.toUpperCase()}] ${timestamp} - ${message}`,
      Object.keys(meta).length > 0 ? meta : ''
    );
  }

  info(message, meta) { this.log('info', message, meta); }
  error(message, meta) { this.log('error', message, meta); }
}


// ============================================================
// THE FACADE -- simple interface over all subsystems
// ============================================================

class EmailFacade {
  constructor(smtpConfig) {
    // Initialize all subsystems
    this.templateEngine = new TemplateEngine();
    this.validator = new EmailValidator();
    this.smtp = new SmtpClient(smtpConfig);
    this.queue = new EmailQueue();
    this.logger = new EmailLogger();
  }

  // --- Simple public methods that hide all complexity ---

  sendWelcomeEmail(user) {
    this.logger.info('Sending welcome email', { to: user.email });

    this.validator.validate(user.email);

    const template = this.templateEngine.getTemplate('welcome');
    const { subject, body } = this.templateEngine.compile(template, {
      name: user.name,
      appName: 'Cohart',
    });

    const result = this.smtp.send({
      to: user.email,
      from: 'welcome@cohart.com',
      subject,
      body,
    });

    this.logger.info('Welcome email sent', { messageId: result.messageId });
    return result;
  }

  sendOrderConfirmation(order) {
    this.logger.info('Sending order confirmation', { orderId: order.id });

    this.validator.validate(order.customerEmail);

    const template = this.templateEngine.getTemplate('order-confirmation');
    const { subject, body } = this.templateEngine.compile(template, {
      name: order.customerName,
      orderId: order.id,
      total: order.total.toFixed(2),
      deliveryDate: order.estimatedDelivery,
    });

    const result = this.smtp.send({
      to: order.customerEmail,
      from: 'orders@cohart.com',
      subject,
      body,
    });

    this.logger.info('Order confirmation sent', { messageId: result.messageId });
    return result;
  }

  sendPasswordReset(user, resetToken) {
    this.logger.info('Sending password reset', { to: user.email });

    this.validator.validate(user.email);

    const template = this.templateEngine.getTemplate('password-reset');
    const { subject, body } = this.templateEngine.compile(template, {
      name: user.name,
      resetLink: `https://cohart.com/reset?token=${resetToken}`,
      expiry: '30',
    });

    const result = this.smtp.send({
      to: user.email,
      from: 'security@cohart.com',
      subject,
      body,
    });

    this.logger.info('Password reset email sent', { messageId: result.messageId });
    return result;
  }

  async sendBulkNewsletter(users, content) {
    this.logger.info('Starting bulk send', { totalRecipients: users.length });

    const emails = users.map((u) => u.email);
    const { valid, invalid } = this.validator.validateBulk(emails);

    if (invalid.length > 0) {
      this.logger.error('Invalid emails found', { count: invalid.length, invalid });
    }

    // Queue all valid emails
    const validUsers = users.filter((u) => valid.includes(u.email));
    for (const user of validUsers) {
      this.queue.enqueue({
        to: user.email,
        from: 'newsletter@cohart.com',
        subject: content.subject,
        body: content.body.replace('{{name}}', user.name),
      });
    }

    // Process queue
    const results = [];
    const sendFn = (job) => this.smtp.send(job);
    let result;
    do {
      result = await this.queue.processNext(sendFn);
      if (result) results.push(result);
    } while (result);

    this.logger.info('Bulk send complete', {
      sent: results.length,
      failed: validUsers.length - results.length,
    });

    return { sent: results.length, failed: validUsers.length - results.length };
  }
}


// ============================================================
// CLIENT CODE -- beautifully simple
// ============================================================

const emailService = new EmailFacade({
  host: 'smtp.example.com',
  port: 587,
  secure: true,
  auth: { user: 'api', pass: 'key123' },
});

// One-liner to send a welcome email
console.log('=== Welcome Email ===');
emailService.sendWelcomeEmail({
  name: 'Alice',
  email: 'alice@example.com',
});

// One-liner to send order confirmation
console.log('\n=== Order Confirmation ===');
emailService.sendOrderConfirmation({
  id: 'ORD-789',
  customerName: 'Bob',
  customerEmail: 'bob@example.com',
  total: 149.99,
  estimatedDelivery: '2026-04-18',
});

// One-liner to send password reset
console.log('\n=== Password Reset ===');
emailService.sendPasswordReset(
  { name: 'Carol', email: 'carol@example.com' },
  'abc123resettoken'
);

Output:

=== Welcome Email ===
  [EmailLog INFO] 2026-04-11T... - Sending welcome email { to: 'alice@example.com' }
  [SMTP] Connecting to smtp.example.com:587...
  [SMTP] Sending to alice@example.com: "Welcome to Cohart, Alice!"
  [EmailLog INFO] 2026-04-11T... - Welcome email sent { messageId: 'msg_...' }

=== Order Confirmation ===
  [EmailLog INFO] 2026-04-11T... - Sending order confirmation { orderId: 'ORD-789' }
  [SMTP] Sending to bob@example.com: "Order #ORD-789 Confirmed"
  [EmailLog INFO] 2026-04-11T... - Order confirmation sent { messageId: 'msg_...' }

=== Password Reset ===
  [EmailLog INFO] 2026-04-11T... - Sending password reset { to: 'carol@example.com' }
  [SMTP] Sending to carol@example.com: "Password Reset Request"
  [EmailLog INFO] 2026-04-11T... - Password reset email sent { messageId: 'msg_...' }

5. API Facade for Microservices

In a microservices architecture, the API Gateway is a facade. Instead of the client calling 5 different services, it calls one endpoint.

// ============================================================
// API GATEWAY FACADE FOR MICROSERVICES
// ============================================================

// ---- Microservices (subsystems) ----

class UserService {
  async getUser(userId) {
    console.log(`  [UserService] Fetching user ${userId}`);
    return { id: userId, name: 'Alice', email: 'alice@example.com' };
  }
}

class OrderService {
  async getOrders(userId) {
    console.log(`  [OrderService] Fetching orders for ${userId}`);
    return [
      { id: 'ORD-1', total: 59.99, status: 'delivered' },
      { id: 'ORD-2', total: 129.99, status: 'shipped' },
    ];
  }
}

class RecommendationService {
  async getRecommendations(userId) {
    console.log(`  [RecommendationService] Getting recs for ${userId}`);
    return [
      { productId: 'P-100', name: 'Widget Pro', score: 0.95 },
      { productId: 'P-200', name: 'Gadget Plus', score: 0.87 },
    ];
  }
}

class NotificationService {
  async getUnreadCount(userId) {
    console.log(`  [NotificationService] Counting unread for ${userId}`);
    return { unread: 3 };
  }
}

class LoyaltyService {
  async getPoints(userId) {
    console.log(`  [LoyaltyService] Fetching points for ${userId}`);
    return { points: 2450, tier: 'Gold' };
  }
}


// ---- API GATEWAY FACADE ----

class DashboardFacade {
  constructor() {
    this.userService = new UserService();
    this.orderService = new OrderService();
    this.recService = new RecommendationService();
    this.notifService = new NotificationService();
    this.loyaltyService = new LoyaltyService();
  }

  /**
   * Single call returns everything the dashboard page needs.
   * Without facade, frontend would make 5 separate API calls.
   */
  async getDashboardData(userId) {
    console.log(`[Facade] Fetching dashboard data for user ${userId}\n`);

    // Fetch all data in parallel -- facade knows the coordination
    const [user, orders, recommendations, notifications, loyalty] =
      await Promise.all([
        this.userService.getUser(userId),
        this.orderService.getOrders(userId),
        this.recService.getRecommendations(userId),
        this.notifService.getUnreadCount(userId),
        this.loyaltyService.getPoints(userId),
      ]);

    // Facade composes a single, coherent response
    return {
      user: {
        name: user.name,
        email: user.email,
        loyaltyTier: loyalty.tier,
        loyaltyPoints: loyalty.points,
      },
      recentOrders: orders.slice(0, 5),
      recommendations: recommendations.slice(0, 3),
      unreadNotifications: notifications.unread,
    };
  }
}


// ---- CLIENT: one call instead of five ----

async function renderDashboard() {
  const facade = new DashboardFacade();
  const data = await facade.getDashboardData('user_42');
  console.log('\nDashboard data:', JSON.stringify(data, null, 2));
}

renderDashboard();
  WITHOUT FACADE (frontend):               WITH FACADE (frontend):

  fetch('/api/users/42')                    fetch('/api/dashboard/42')
  fetch('/api/orders?userId=42')               |
  fetch('/api/recommendations/42')             +-- ONE request
  fetch('/api/notifications/count/42')         |
  fetch('/api/loyalty/42')                     v
       |                                    Complete dashboard data
       +-- FIVE round trips
       |
       v
  Client assembles data

6. Before and After Comparison

Before (no facade)

// BAD: Every service that sends email must do ALL of this
async function handleUserRegistration(userData) {
  // 1. Create user (business logic)
  const user = await createUser(userData);

  // 2. Now send welcome email -- lots of coordination
  const templateEngine = new TemplateEngine();
  const template = templateEngine.getTemplate('welcome');
  const compiled = templateEngine.compile(template, {
    name: user.name,
    appName: 'Cohart',
  });

  const validator = new EmailValidator();
  validator.validate(user.email);

  const smtp = new SmtpClient({
    host: process.env.SMTP_HOST,
    port: parseInt(process.env.SMTP_PORT),
    secure: true,
    auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
  });

  const logger = new EmailLogger();
  logger.info('Sending welcome email', { to: user.email });

  const result = smtp.send({
    to: user.email,
    from: 'welcome@cohart.com',
    subject: compiled.subject,
    body: compiled.body,
  });

  logger.info('Email sent', { messageId: result.messageId });

  return user;
}

// Problem: This same 20 lines appears in handlePasswordReset,
// handleOrderConfirmation, handleNewsletterSignup, etc.

After (with facade)

// GOOD: Email complexity is hidden behind the facade
async function handleUserRegistration(userData) {
  const user = await createUser(userData);
  emailService.sendWelcomeEmail(user);  // ONE LINE
  return user;
}

async function handlePasswordReset(user) {
  const token = generateResetToken();
  emailService.sendPasswordReset(user, token);  // ONE LINE
  return token;
}

async function handleOrderPlaced(order) {
  emailService.sendOrderConfirmation(order);  // ONE LINE
}

7. When Facade Becomes a God Object

A facade can grow into an anti-pattern if you are not careful:

  HEALTHY FACADE:                      GOD OBJECT (anti-pattern):

  EmailFacade                          AppFacade
  - sendWelcomeEmail()                 - sendEmail()
  - sendOrderConfirmation()            - processPayment()
  - sendPasswordReset()                - createUser()
  - sendBulkNewsletter()               - generateReport()
                                       - uploadFile()
  Focused on ONE subsystem             - sendNotification()
  (email sending)                      - runAnalytics()
                                       - syncInventory()
                                       - ... 50 more methods

                                       Covers EVERYTHING
                                       (knows too much, does too much)

Warning signs your facade is becoming a god object:

SignWhat it means
50+ methodsFacade is covering too many unrelated subsystems
Methods from different domainsEmail, payments, auth, analytics all in one class
Subsystem changes break the facadeFacade is too tightly coupled
Multiple teams edit the same facadeToo many responsibilities in one place
Facade has business logicIt should only delegate, not decide

The fix: split into multiple focused facades

// Instead of one giant facade:
class AppFacade { /* 50 methods */ }

// Split by domain:
class EmailFacade { /* 4-5 email methods */ }
class PaymentFacade { /* 3-4 payment methods */ }
class NotificationFacade { /* 3-4 notification methods */ }
class ReportFacade { /* 2-3 reporting methods */ }

8. Facade vs Adapter

These two are often confused. Here is the clear distinction:

AspectFacadeAdapter
IntentSimplify a complex subsystemMake incompatible interfaces work together
Number of objects wrappedMany (entire subsystem)One (single object)
New interface?Yes (a simpler one)Yes (the expected one)
Existing interface usable?Yes, subsystem is still directly accessibleAdaptee's interface is incompatible, so no
AnalogyHotel conciergePower plug adapter

9. When to Use and When to Avoid

Use Facade when:

ScenarioWhy Facade helps
Complex subsystem with many classesOne entry point reduces cognitive load
Many callers repeat the same coordinationCentralize the orchestration logic
Layered architectureFacade defines entry points for each layer
API gateway for microservicesSingle endpoint aggregates multiple services
Legacy system has a messy APIFacade provides a clean modern interface

Avoid Facade when:

ScenarioWhy Facade is wrong
Subsystem is already simpleUnnecessary abstraction adds overhead
Clients need fine-grained controlFacade may be too restrictive
You are hiding design problemsFix the subsystem instead of hiding it
Facade grows beyond one responsibilitySplit into multiple facades

10. Key Takeaways

  1. A Facade provides a simplified interface to a complex subsystem -- it reduces coupling between client code and subsystem internals.
  2. The facade delegates work to subsystem objects -- it does not implement the functionality itself.
  3. Clients can still use subsystem classes directly if they need fine-grained control -- the facade is a convenience, not a wall.
  4. API Gateways in microservices are real-world facades -- one call, multiple backend services.
  5. Watch for the god object anti-pattern: a facade should be focused on one domain (email, payments, dashboard), not everything.
  6. Facade + Adapter often work together: a facade simplifies, and adapters inside it translate incompatible interfaces.

11. Explain-It Challenge

Without looking back, explain in your own words:

  1. What is the difference between a Facade and just "writing a helper function"?
  2. Why does the API Gateway pattern qualify as a Facade?
  3. How would you know when your Facade has grown into a God Object?
  4. Can clients bypass the Facade and use subsystems directly? Should they?
  5. Draw an ASCII diagram of the Email Facade and its 5 subsystems.

Navigation: <- 9.4.a -- Adapter | 9.4.c -- Proxy ->