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?
- 2. The Problem It Solves
- 3. Structure and Participants
- 4. Email Sending Facade -- Full Example
- 5. API Facade for Microservices
- 6. Before and After Comparison
- 7. When Facade Becomes a God Object
- 8. Facade vs Adapter
- 9. When to Use and When to Avoid
- 10. Key Takeaways
- 11. Explain-It Challenge
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 │
└──────────────┘ └──────────┘
| Role | What it does |
|---|---|
| Facade | Knows which subsystem classes handle which requests; delegates client calls to appropriate subsystem objects |
| Subsystem classes | Implement actual functionality; have no knowledge of the facade |
| Client | Uses 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:
- Load the correct email template
- Compile the template with dynamic data
- Validate the recipient email address
- Configure SMTP connection settings
- Handle attachments
- Queue the email for delivery
- Log the send attempt
- 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:
| Sign | What it means |
|---|---|
| 50+ methods | Facade is covering too many unrelated subsystems |
| Methods from different domains | Email, payments, auth, analytics all in one class |
| Subsystem changes break the facade | Facade is too tightly coupled |
| Multiple teams edit the same facade | Too many responsibilities in one place |
| Facade has business logic | It 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:
| Aspect | Facade | Adapter |
|---|---|---|
| Intent | Simplify a complex subsystem | Make incompatible interfaces work together |
| Number of objects wrapped | Many (entire subsystem) | One (single object) |
| New interface? | Yes (a simpler one) | Yes (the expected one) |
| Existing interface usable? | Yes, subsystem is still directly accessible | Adaptee's interface is incompatible, so no |
| Analogy | Hotel concierge | Power plug adapter |
9. When to Use and When to Avoid
Use Facade when:
| Scenario | Why Facade helps |
|---|---|
| Complex subsystem with many classes | One entry point reduces cognitive load |
| Many callers repeat the same coordination | Centralize the orchestration logic |
| Layered architecture | Facade defines entry points for each layer |
| API gateway for microservices | Single endpoint aggregates multiple services |
| Legacy system has a messy API | Facade provides a clean modern interface |
Avoid Facade when:
| Scenario | Why Facade is wrong |
|---|---|
| Subsystem is already simple | Unnecessary abstraction adds overhead |
| Clients need fine-grained control | Facade may be too restrictive |
| You are hiding design problems | Fix the subsystem instead of hiding it |
| Facade grows beyond one responsibility | Split into multiple facades |
10. Key Takeaways
- A Facade provides a simplified interface to a complex subsystem -- it reduces coupling between client code and subsystem internals.
- The facade delegates work to subsystem objects -- it does not implement the functionality itself.
- Clients can still use subsystem classes directly if they need fine-grained control -- the facade is a convenience, not a wall.
- API Gateways in microservices are real-world facades -- one call, multiple backend services.
- Watch for the god object anti-pattern: a facade should be focused on one domain (email, payments, dashboard), not everything.
- 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:
- What is the difference between a Facade and just "writing a helper function"?
- Why does the API Gateway pattern qualify as a Facade?
- How would you know when your Facade has grown into a God Object?
- Can clients bypass the Facade and use subsystems directly? Should they?
- Draw an ASCII diagram of the Email Facade and its 5 subsystems.
Navigation: <- 9.4.a -- Adapter | 9.4.c -- Proxy ->