Adapter and Facade Patterns Explained

Introduction
You've mastered creational patterns — Singleton, Factory, Builder, Prototype. Now it's time for structural patterns: patterns about how objects and classes are composed into larger structures.
Two structural patterns show up everywhere in real codebases:
| Pattern | One-Line Summary |
|---|---|
| Adapter | Makes an incompatible interface work where a different interface is expected |
| Facade | Provides a simple interface to a complex subsystem |
Both are about interfaces — but they solve opposite problems. Adapter converts an interface. Facade simplifies one.
What You'll Learn
✅ Understand the problem each pattern solves
✅ Implement Object Adapter and Class Adapter variants
✅ Build a Facade that hides subsystem complexity
✅ Apply both patterns in TypeScript, Python, and Java
✅ Choose between Adapter, Facade, and other structural patterns
✅ Avoid common mistakes when wrapping interfaces
Prerequisites
- Completed Prototype Pattern Explained
- Familiar with Polymorphism and Interfaces
- Comfortable with SOLID Principles, especially Interface Segregation and Dependency Inversion
Part 1: The Adapter Pattern
The Problem: Incompatible Interfaces
You're building a payment system. Your entire codebase expects a PaymentProcessor interface:
interface PaymentProcessor {
charge(amount: number, currency: string): PaymentResult;
}Everything works — checkout, subscriptions, refunds — all coded against PaymentProcessor.
Now your boss says: "We're adding Stripe." But Stripe's SDK doesn't implement your PaymentProcessor interface. It has its own API:
// Stripe's SDK — you can't modify this
class StripeAPI {
createCharge(amountInCents: number, cur: string, idempotencyKey: string): StripeResponse {
// Stripe-specific logic
}
}Without the Adapter pattern, you'd litter your codebase with conditionals:
// ❌ Every checkout function becomes coupled to every payment provider
function checkout(cart: Cart, provider: string) {
if (provider === "internal") {
internalProcessor.charge(cart.total, "USD");
} else if (provider === "stripe") {
stripeAPI.createCharge(cart.total * 100, "USD", generateKey());
} else if (provider === "paypal") {
paypalSDK.makePayment({ value: cart.total, currency_code: "USD" });
}
// Every new provider = every checkout function modified ❌
}The Adapter pattern wraps the incompatible class to make it fit the expected interface — without modifying either side.
Pattern Structure
Participants:
- Target — the interface your code expects (
PaymentProcessor) - Adaptee — the class with the incompatible interface (
StripeAPI) - Adapter — wraps the adaptee and implements the target interface (
StripeAdapter) - Client — works with the target interface, unaware of the adapter
Object Adapter vs Class Adapter
There are two ways to implement an adapter:
| Variant | Mechanism | Language Support |
|---|---|---|
| Object Adapter | Holds a reference to the adaptee (composition) | All languages |
| Class Adapter | Inherits from both Target and Adaptee (multiple inheritance) | C++, Python |
Use Object Adapter in most cases — it's more flexible and works in all languages. Class Adapter is only possible with multiple inheritance (Python supports it, Java and TypeScript don't).
TypeScript Implementation
Object Adapter (Recommended)
// Target interface — what our code expects
interface PaymentProcessor {
charge(amount: number, currency: string): PaymentResult;
}
interface PaymentResult {
success: boolean;
transactionId: string;
}
// Adaptee — third-party SDK we can't modify
class StripeAPI {
createCharge(
amountInCents: number,
cur: string,
idempotencyKey: string
): { id: string; status: string } {
console.log(`Stripe: charging ${amountInCents} cents (${cur})`);
return { id: `stripe_${Date.now()}`, status: "succeeded" };
}
}
// Adapter — bridges the gap
class StripeAdapter implements PaymentProcessor {
constructor(private stripe: StripeAPI) {}
charge(amount: number, currency: string): PaymentResult {
const amountInCents = Math.round(amount * 100);
const key = `idempotent_${Date.now()}_${Math.random()}`;
const response = this.stripe.createCharge(amountInCents, currency, key);
return {
success: response.status === "succeeded",
transactionId: response.id,
};
}
}
// Client code — works with any PaymentProcessor
function processCheckout(processor: PaymentProcessor, amount: number) {
const result = processor.charge(amount, "USD");
if (result.success) {
console.log(`Payment successful: ${result.transactionId}`);
}
}
// Usage — client never knows about Stripe
const stripeAdapter = new StripeAdapter(new StripeAPI());
processCheckout(stripeAdapter, 49.99);Multiple Adapters for Multiple Providers
// Another adaptee — PayPal SDK
class PayPalSDK {
makePayment(options: {
value: number;
currency_code: string;
}): { orderId: string; outcome: string } {
console.log(`PayPal: ${options.value} ${options.currency_code}`);
return { orderId: `pp_${Date.now()}`, outcome: "COMPLETED" };
}
}
// PayPal adapter
class PayPalAdapter implements PaymentProcessor {
constructor(private paypal: PayPalSDK) {}
charge(amount: number, currency: string): PaymentResult {
const response = this.paypal.makePayment({
value: amount,
currency_code: currency,
});
return {
success: response.outcome === "COMPLETED",
transactionId: response.orderId,
};
}
}
// Now both providers are interchangeable
const processors: Record<string, PaymentProcessor> = {
stripe: new StripeAdapter(new StripeAPI()),
paypal: new PayPalAdapter(new PayPalSDK()),
};
function checkout(cart: Cart, providerName: string) {
const processor = processors[providerName];
return processor.charge(cart.total, "USD"); // ✅ clean, no conditionals
}Python Implementation
Python supports both Object Adapter and Class Adapter (via multiple inheritance):
Object Adapter
from abc import ABC, abstractmethod
from dataclasses import dataclass
import time, random
# Target
class PaymentProcessor(ABC):
@abstractmethod
def charge(self, amount: float, currency: str) -> dict:
pass
# Adaptee — third-party SDK
class StripeAPI:
def create_charge(self, amount_in_cents: int, cur: str, idempotency_key: str) -> dict:
print(f"Stripe: charging {amount_in_cents} cents ({cur})")
return {"id": f"stripe_{int(time.time())}", "status": "succeeded"}
# Object Adapter
class StripeAdapter(PaymentProcessor):
def __init__(self, stripe: StripeAPI):
self._stripe = stripe
def charge(self, amount: float, currency: str) -> dict:
amount_in_cents = round(amount * 100)
key = f"idempotent_{time.time()}_{random.random()}"
response = self._stripe.create_charge(amount_in_cents, currency, key)
return {
"success": response["status"] == "succeeded",
"transaction_id": response["id"],
}
# Usage
adapter = StripeAdapter(StripeAPI())
result = adapter.charge(49.99, "USD")
print(result) # {'success': True, 'transaction_id': 'stripe_...'}Class Adapter (Python-only — multiple inheritance)
# Class Adapter — inherits from BOTH Target and Adaptee
class StripeClassAdapter(PaymentProcessor, StripeAPI):
"""
Inherits PaymentProcessor (Target) and StripeAPI (Adaptee).
Overrides charge() to delegate to inherited create_charge().
"""
def charge(self, amount: float, currency: str) -> dict:
amount_in_cents = round(amount * 100)
key = f"idempotent_{time.time()}_{random.random()}"
# Call inherited method directly — no composition needed
response = self.create_charge(amount_in_cents, currency, key)
return {
"success": response["status"] == "succeeded",
"transaction_id": response["id"],
}
# Usage — same interface, different wiring
adapter = StripeClassAdapter()
result = adapter.charge(29.99, "EUR")Note: Class Adapter is tighter coupled — it inherits all of
StripeAPI's methods and is harder to test. Prefer Object Adapter unless you have a specific reason to inherit.
Java Implementation
// Target interface
public interface PaymentProcessor {
PaymentResult charge(double amount, String currency);
}
public record PaymentResult(boolean success, String transactionId) {}
// Adaptee — third-party SDK
public class StripeAPI {
public StripeResponse createCharge(int amountInCents, String cur, String idempotencyKey) {
System.out.printf("Stripe: charging %d cents (%s)%n", amountInCents, cur);
return new StripeResponse("stripe_" + System.currentTimeMillis(), "succeeded");
}
}
public record StripeResponse(String id, String status) {}
// Adapter
public class StripeAdapter implements PaymentProcessor {
private final StripeAPI stripe;
public StripeAdapter(StripeAPI stripe) {
this.stripe = stripe;
}
@Override
public PaymentResult charge(double amount, String currency) {
int amountInCents = (int) Math.round(amount * 100);
String key = "idempotent_" + System.currentTimeMillis();
StripeResponse response = stripe.createCharge(amountInCents, currency, key);
return new PaymentResult(
"succeeded".equals(response.status()),
response.id()
);
}
}
// Client code
public class CheckoutService {
private final PaymentProcessor processor;
public CheckoutService(PaymentProcessor processor) {
this.processor = processor;
}
public void checkout(double amount) {
PaymentResult result = processor.charge(amount, "USD");
if (result.success()) {
System.out.println("Payment successful: " + result.transactionId());
}
}
}
// Usage
CheckoutService service = new CheckoutService(new StripeAdapter(new StripeAPI()));
service.checkout(49.99);Real-World Adapter Examples
1. Logger Adapter
Your app uses a custom Logger interface, but you want to swap in Winston, Pino, or any other logging library:
interface Logger {
info(message: string, context?: Record<string, unknown>): void;
error(message: string, error?: Error): void;
warn(message: string): void;
}
// Adapt Winston to your Logger interface
class WinstonAdapter implements Logger {
constructor(private winston: WinstonLogger) {}
info(message: string, context?: Record<string, unknown>): void {
this.winston.log("info", message, { metadata: context });
}
error(message: string, error?: Error): void {
this.winston.log("error", message, {
stack: error?.stack,
name: error?.name,
});
}
warn(message: string): void {
this.winston.log("warn", message);
}
}
// Adapt Pino to the same interface
class PinoAdapter implements Logger {
constructor(private pino: PinoLogger) {}
info(message: string, context?: Record<string, unknown>): void {
this.pino.info(context ?? {}, message);
}
error(message: string, error?: Error): void {
this.pino.error({ err: error }, message);
}
warn(message: string): void {
this.pino.warn(message);
}
}2. Data Format Adapter
Convert between XML and JSON when integrating legacy and modern APIs:
interface DataProvider {
getData(): Record<string, unknown>;
}
// Legacy system returns XML
class LegacyXMLService {
fetchData(): string {
return "<user><name>Alice</name><age>30</age></user>";
}
}
class XMLToJSONAdapter implements DataProvider {
constructor(private xmlService: LegacyXMLService) {}
getData(): Record<string, unknown> {
const xml = this.xmlService.fetchData();
return this.parseXML(xml);
}
private parseXML(xml: string): Record<string, unknown> {
// simplified — in production use a library like fast-xml-parser
const nameMatch = xml.match(/<name>(.*?)<\/name>/);
const ageMatch = xml.match(/<age>(.*?)<\/age>/);
return {
name: nameMatch?.[1] ?? "",
age: Number(ageMatch?.[1] ?? 0),
};
}
}Part 2: The Facade Pattern
The Problem: Complexity Overload
You're building an e-commerce system. Placing an order involves multiple subsystems:
// ❌ Client code must orchestrate 6 different subsystems
function placeOrder(userId: string, items: CartItem[]) {
// 1. Validate inventory
const inventory = new InventoryService();
for (const item of items) {
if (!inventory.checkStock(item.productId, item.quantity)) {
throw new Error(`Out of stock: ${item.productId}`);
}
}
// 2. Calculate pricing
const pricing = new PricingEngine();
const subtotal = pricing.calculateSubtotal(items);
const tax = pricing.calculateTax(subtotal, "US-CA");
const discount = pricing.applyPromoCode("SAVE10", subtotal);
const total = subtotal + tax - discount;
// 3. Process payment
const payment = new PaymentGateway();
const paymentResult = payment.authorize(userId, total);
if (!paymentResult.success) throw new Error("Payment failed");
payment.capture(paymentResult.authorizationId);
// 4. Reserve inventory
for (const item of items) {
inventory.reserve(item.productId, item.quantity);
}
// 5. Create shipping
const shipping = new ShippingService();
const address = new AddressBook().getDefault(userId);
const shipment = shipping.createShipment(items, address);
shipping.schedulePickup(shipment.id);
// 6. Send notifications
const notifier = new NotificationService();
notifier.sendOrderConfirmation(userId, paymentResult.orderId);
notifier.sendShippingNotification(userId, shipment.trackingNumber);
}Problems:
- Every client that places an order must know the correct sequence of 6 subsystems
- Tight coupling — clients depend on every subsystem's API
- Fragile — changing any subsystem's API breaks every client
- Duplicated orchestration — web checkout, mobile app, API endpoint all repeat this logic
Pattern Structure
Participants:
- Facade — provides a simple, unified interface to the subsystem
- Subsystems — the complex classes that do the real work (unchanged)
- Client — uses the facade instead of interacting with subsystems directly
Key distinction from Adapter: The Facade doesn't translate interfaces — it simplifies them. The subsystems don't have incompatible interfaces; they're just complex to orchestrate.
TypeScript Implementation
// Subsystem classes (unchanged — they're already correct individually)
class InventoryService {
checkStock(productId: string, quantity: number): boolean {
console.log(`Checking stock for ${productId}`);
return true;
}
reserve(productId: string, quantity: number): void {
console.log(`Reserved ${quantity}x ${productId}`);
}
release(productId: string, quantity: number): void {
console.log(`Released ${quantity}x ${productId}`);
}
}
class PricingEngine {
calculateSubtotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
calculateTax(subtotal: number, region: string): number {
const rates: Record<string, number> = { "US-CA": 0.0725, "US-TX": 0.0625 };
return subtotal * (rates[region] ?? 0);
}
applyPromoCode(code: string, subtotal: number): number {
const discounts: Record<string, number> = { SAVE10: 0.1, SAVE20: 0.2 };
return subtotal * (discounts[code] ?? 0);
}
}
class PaymentGateway {
authorize(userId: string, amount: number): { success: boolean; authorizationId: string } {
console.log(`Authorizing $${amount} for user ${userId}`);
return { success: true, authorizationId: `auth_${Date.now()}` };
}
capture(authorizationId: string): void {
console.log(`Captured payment: ${authorizationId}`);
}
refund(authorizationId: string): void {
console.log(`Refunded: ${authorizationId}`);
}
}
class ShippingService {
createShipment(items: CartItem[], address: Address): { id: string; trackingNumber: string } {
console.log(`Shipping ${items.length} items to ${address.city}`);
return { id: `ship_${Date.now()}`, trackingNumber: `TRK${Date.now()}` };
}
schedulePickup(shipmentId: string): void {
console.log(`Pickup scheduled for ${shipmentId}`);
}
}
class NotificationService {
sendOrderConfirmation(userId: string, orderId: string): void {
console.log(`Order confirmation sent to ${userId}`);
}
sendShippingNotification(userId: string, trackingNumber: string): void {
console.log(`Shipping notification sent to ${userId}`);
}
}
// ✅ Facade — one simple method hides all the complexity
class OrderFacade {
private inventory = new InventoryService();
private pricing = new PricingEngine();
private payment = new PaymentGateway();
private shipping = new ShippingService();
private notifications = new NotificationService();
placeOrder(userId: string, items: CartItem[], promoCode?: string): OrderResult {
// 1. Validate stock
for (const item of items) {
if (!this.inventory.checkStock(item.productId, item.quantity)) {
return { success: false, error: `Out of stock: ${item.productId}` };
}
}
// 2. Calculate total
const subtotal = this.pricing.calculateSubtotal(items);
const tax = this.pricing.calculateTax(subtotal, "US-CA");
const discount = promoCode
? this.pricing.applyPromoCode(promoCode, subtotal)
: 0;
const total = subtotal + tax - discount;
// 3. Process payment
const paymentResult = this.payment.authorize(userId, total);
if (!paymentResult.success) {
return { success: false, error: "Payment failed" };
}
this.payment.capture(paymentResult.authorizationId);
// 4. Reserve inventory
for (const item of items) {
this.inventory.reserve(item.productId, item.quantity);
}
// 5. Ship
const address = { city: "San Francisco", zip: "94102" } as Address;
const shipment = this.shipping.createShipment(items, address);
this.shipping.schedulePickup(shipment.id);
// 6. Notify
this.notifications.sendOrderConfirmation(userId, paymentResult.authorizationId);
this.notifications.sendShippingNotification(userId, shipment.trackingNumber);
return {
success: true,
orderId: paymentResult.authorizationId,
trackingNumber: shipment.trackingNumber,
total,
};
}
}
// Client code — beautifully simple
const orderService = new OrderFacade();
const result = orderService.placeOrder("user_123", cartItems, "SAVE10");Python Implementation
from dataclasses import dataclass
import time
# Subsystem classes
class InventoryService:
def check_stock(self, product_id: str, quantity: int) -> bool:
print(f"Checking stock for {product_id}")
return True
def reserve(self, product_id: str, quantity: int) -> None:
print(f"Reserved {quantity}x {product_id}")
class PricingEngine:
def calculate_subtotal(self, items: list[dict]) -> float:
return sum(item["price"] * item["quantity"] for item in items)
def calculate_tax(self, subtotal: float, region: str) -> float:
rates = {"US-CA": 0.0725, "US-TX": 0.0625}
return subtotal * rates.get(region, 0)
def apply_promo_code(self, code: str, subtotal: float) -> float:
discounts = {"SAVE10": 0.1, "SAVE20": 0.2}
return subtotal * discounts.get(code, 0)
class PaymentGateway:
def authorize(self, user_id: str, amount: float) -> dict:
print(f"Authorizing ${amount} for {user_id}")
return {"success": True, "authorization_id": f"auth_{int(time.time())}"}
def capture(self, authorization_id: str) -> None:
print(f"Captured payment: {authorization_id}")
class ShippingService:
def create_shipment(self, items: list[dict], address: dict) -> dict:
print(f"Shipping {len(items)} items to {address['city']}")
return {"id": f"ship_{int(time.time())}", "tracking": f"TRK{int(time.time())}"}
def schedule_pickup(self, shipment_id: str) -> None:
print(f"Pickup scheduled for {shipment_id}")
class NotificationService:
def send_order_confirmation(self, user_id: str, order_id: str) -> None:
print(f"Order confirmation sent to {user_id}")
def send_shipping_notification(self, user_id: str, tracking: str) -> None:
print(f"Shipping notification sent to {user_id}")
# ✅ Facade
@dataclass
class OrderResult:
success: bool
order_id: str = ""
tracking_number: str = ""
total: float = 0.0
error: str = ""
class OrderFacade:
def __init__(self):
self._inventory = InventoryService()
self._pricing = PricingEngine()
self._payment = PaymentGateway()
self._shipping = ShippingService()
self._notifications = NotificationService()
def place_order(
self, user_id: str, items: list[dict], promo_code: str | None = None
) -> OrderResult:
# 1. Validate stock
for item in items:
if not self._inventory.check_stock(item["product_id"], item["quantity"]):
return OrderResult(success=False, error=f"Out of stock: {item['product_id']}")
# 2. Calculate total
subtotal = self._pricing.calculate_subtotal(items)
tax = self._pricing.calculate_tax(subtotal, "US-CA")
discount = self._pricing.apply_promo_code(promo_code, subtotal) if promo_code else 0
total = subtotal + tax - discount
# 3. Process payment
payment_result = self._payment.authorize(user_id, total)
if not payment_result["success"]:
return OrderResult(success=False, error="Payment failed")
self._payment.capture(payment_result["authorization_id"])
# 4. Reserve inventory
for item in items:
self._inventory.reserve(item["product_id"], item["quantity"])
# 5. Ship
shipment = self._shipping.create_shipment(items, {"city": "San Francisco"})
self._shipping.schedule_pickup(shipment["id"])
# 6. Notify
self._notifications.send_order_confirmation(user_id, payment_result["authorization_id"])
self._notifications.send_shipping_notification(user_id, shipment["tracking"])
return OrderResult(
success=True,
order_id=payment_result["authorization_id"],
tracking_number=shipment["tracking"],
total=total,
)
# Usage
facade = OrderFacade()
result = facade.place_order("user_123", [
{"product_id": "SKU001", "quantity": 2, "price": 29.99},
{"product_id": "SKU002", "quantity": 1, "price": 49.99},
], promo_code="SAVE10")
print(f"Order {'placed' if result.success else 'failed'}: {result}")Java Implementation
// Subsystem classes (simplified)
public class InventoryService {
public boolean checkStock(String productId, int quantity) {
System.out.println("Checking stock for " + productId);
return true;
}
public void reserve(String productId, int quantity) {
System.out.printf("Reserved %dx %s%n", quantity, productId);
}
}
public class PricingEngine {
public double calculateSubtotal(List<CartItem> items) {
return items.stream()
.mapToDouble(item -> item.price() * item.quantity())
.sum();
}
public double calculateTax(double subtotal, String region) {
return switch (region) {
case "US-CA" -> subtotal * 0.0725;
case "US-TX" -> subtotal * 0.0625;
default -> 0;
};
}
}
public class PaymentGateway {
public PaymentAuth authorize(String userId, double amount) {
System.out.printf("Authorizing $%.2f for %s%n", amount, userId);
return new PaymentAuth(true, "auth_" + System.currentTimeMillis());
}
public void capture(String authorizationId) {
System.out.println("Captured: " + authorizationId);
}
}
public record PaymentAuth(boolean success, String authorizationId) {}
public record CartItem(String productId, int quantity, double price) {}
// ✅ Facade
public class OrderFacade {
private final InventoryService inventory = new InventoryService();
private final PricingEngine pricing = new PricingEngine();
private final PaymentGateway payment = new PaymentGateway();
public OrderResult placeOrder(String userId, List<CartItem> items) {
// 1. Validate stock
for (CartItem item : items) {
if (!inventory.checkStock(item.productId(), item.quantity())) {
return OrderResult.failure("Out of stock: " + item.productId());
}
}
// 2. Calculate total
double subtotal = pricing.calculateSubtotal(items);
double tax = pricing.calculateTax(subtotal, "US-CA");
double total = subtotal + tax;
// 3. Process payment
PaymentAuth auth = payment.authorize(userId, total);
if (!auth.success()) {
return OrderResult.failure("Payment failed");
}
payment.capture(auth.authorizationId());
// 4. Reserve inventory
for (CartItem item : items) {
inventory.reserve(item.productId(), item.quantity());
}
return OrderResult.success(auth.authorizationId(), total);
}
}
public record OrderResult(boolean success, String orderId, double total, String error) {
public static OrderResult success(String orderId, double total) {
return new OrderResult(true, orderId, total, "");
}
public static OrderResult failure(String error) {
return new OrderResult(false, "", 0, error);
}
}
// Usage
OrderFacade facade = new OrderFacade();
OrderResult result = facade.placeOrder("user_123", List.of(
new CartItem("SKU001", 2, 29.99),
new CartItem("SKU002", 1, 49.99)
));
System.out.println(result.success() ? "Order placed!" : "Failed: " + result.error());Real-World Facade Examples
1. Video Conversion Facade
// Complex subsystem
class VideoFile { constructor(public path: string) {} }
class CodecFactory {
detect(file: VideoFile): string { return "h264"; }
}
class MPEG4Codec { encode(data: Buffer): Buffer { return data; } }
class OGGCodec { encode(data: Buffer): Buffer { return data; } }
class AudioExtractor { extract(file: VideoFile): Buffer { return Buffer.from([]); } }
class AudioMixer { mix(video: Buffer, audio: Buffer): Buffer { return video; } }
class BitrateReader {
static read(file: VideoFile): Buffer { return Buffer.from([]); }
static convert(buffer: Buffer, codec: MPEG4Codec | OGGCodec): Buffer {
return codec.encode(buffer);
}
}
// ✅ Facade — hides codec detection, audio extraction, mixing, encoding
class VideoConverterFacade {
convert(inputPath: string, outputFormat: string): string {
const file = new VideoFile(inputPath);
const sourceCodec = new CodecFactory().detect(file);
const codec = outputFormat === "mp4" ? new MPEG4Codec() : new OGGCodec();
const videoData = BitrateReader.read(file);
const audioData = new AudioExtractor().extract(file);
const convertedVideo = BitrateReader.convert(videoData, codec);
const finalFile = new AudioMixer().mix(convertedVideo, audioData);
const outputPath = inputPath.replace(/\.\w+$/, `.${outputFormat}`);
console.log(`Converted ${inputPath} → ${outputPath}`);
return outputPath;
}
}
// Client sees one method
const converter = new VideoConverterFacade();
converter.convert("video.avi", "mp4");2. Database Facade
// Complex subsystem: connection pooling, query building, transactions
class ConnectionPool {
acquire(): Connection { /* ... */ return {} as Connection; }
release(conn: Connection): void { /* ... */ }
}
class QueryBuilder {
select(table: string, where?: Record<string, unknown>): string {
const conditions = where
? " WHERE " + Object.entries(where).map(([k, v]) => `${k} = '${v}'`).join(" AND ")
: "";
return `SELECT * FROM ${table}${conditions}`;
}
insert(table: string, data: Record<string, unknown>): string {
const cols = Object.keys(data).join(", ");
const vals = Object.values(data).map(v => `'${v}'`).join(", ");
return `INSERT INTO ${table} (${cols}) VALUES (${vals})`;
}
}
class TransactionManager {
begin(conn: Connection): void { /* ... */ }
commit(conn: Connection): void { /* ... */ }
rollback(conn: Connection): void { /* ... */ }
}
// ✅ Facade
class DatabaseFacade {
private pool = new ConnectionPool();
private queryBuilder = new QueryBuilder();
private txManager = new TransactionManager();
async findAll(table: string, where?: Record<string, unknown>): Promise<Row[]> {
const conn = this.pool.acquire();
try {
const sql = this.queryBuilder.select(table, where);
return await conn.execute(sql);
} finally {
this.pool.release(conn);
}
}
async insert(table: string, data: Record<string, unknown>): Promise<void> {
const conn = this.pool.acquire();
this.txManager.begin(conn);
try {
const sql = this.queryBuilder.insert(table, data);
await conn.execute(sql);
this.txManager.commit(conn);
} catch (error) {
this.txManager.rollback(conn);
throw error;
} finally {
this.pool.release(conn);
}
}
}
// Client — no knowledge of pools, builders, transactions
const db = new DatabaseFacade();
const users = await db.findAll("users", { role: "admin" });
await db.insert("users", { name: "Alice", role: "user" });Adapter vs Facade: When to Use Which
| Aspect | Adapter | Facade |
|---|---|---|
| Goal | Make an incompatible interface compatible | Simplify a complex subsystem |
| Number of classes wrapped | Usually one | Multiple |
| Interface change | Converts interface A to interface B | Creates a new simplified interface |
| Client awareness | Client doesn't know about the adaptee | Client doesn't know about subsystem internals |
| When to use | Integrating third-party or legacy code | Reducing coupling to complex internals |
| SOLID principle | Dependency Inversion | Interface Segregation |
Combining Both Patterns
In practice, you often use both together. The Facade orchestrates the workflow, and Adapters handle incompatible third-party interfaces within that workflow:
// Adapters for incompatible third-party services
const stripeAdapter = new StripeAdapter(new StripeAPI());
const sendgridAdapter = new SendGridAdapter(new SendGridAPI());
// Facade uses adapters as its subsystems
class OrderFacade {
constructor(
private payment: PaymentProcessor, // adapter
private emailer: EmailService, // adapter
private inventory: InventoryService // our own code
) {}
placeOrder(userId: string, items: CartItem[]): OrderResult {
// Facade orchestrates; adapters handle interface translation
this.inventory.reserve(items);
const payment = this.payment.charge(calculateTotal(items), "USD");
this.emailer.sendConfirmation(userId, payment.transactionId);
return { success: true };
}
}Common Pitfalls
1. Creating a "God Adapter" That Does Too Much
// ❌ Adapter doing business logic — it should only translate
class StripeAdapter implements PaymentProcessor {
charge(amount: number, currency: string): PaymentResult {
if (amount > 10000) throw new Error("Amount too high"); // ❌ business rule
if (currency !== "USD") amount *= getExchangeRate(currency); // ❌ business logic
const response = this.stripe.createCharge(/* ... */);
return { success: response.status === "succeeded", /* ... */ };
}
}
// ✅ Adapter only translates — business logic belongs elsewhere
class StripeAdapter implements PaymentProcessor {
charge(amount: number, currency: string): PaymentResult {
const amountInCents = Math.round(amount * 100);
const response = this.stripe.createCharge(amountInCents, currency, generateKey());
return {
success: response.status === "succeeded",
transactionId: response.id,
};
}
}2. Facade That Exposes Subsystem Details
// ❌ Leaking subsystem types through the facade
class OrderFacade {
placeOrder(userId: string, items: CartItem[]): {
payment: StripeResponse; // ❌ leaks Stripe type
shipment: FedExShipment; // ❌ leaks FedEx type
} {
// ...
}
}
// ✅ Facade defines its own return types
class OrderFacade {
placeOrder(userId: string, items: CartItem[]): OrderResult {
// facade-defined type — no subsystem leakage
}
}3. Making Everything a Facade
// ❌ Unnecessary facade — just wrapping one thing with no simplification
class UserFacade {
getUser(id: string) {
return this.userRepository.findById(id); // just delegating — adds nothing
}
}
// ✅ Only use facade when there's genuine complexity to hide
// A single repository call doesn't need a facade4. Adapter With Wrong Direction
// ❌ Adapting OUR code to match the third-party interface — backwards!
class OurPaymentAdapter implements StripeInterface {
// Don't adapt your code to third-party APIs
}
// ✅ Adapt the THIRD-PARTY code to match OUR interface
class StripeAdapter implements OurPaymentProcessor {
// Third-party adapts to us — we own the interface
}Summary
The Adapter and Facade are the two most common structural patterns. They both wrap existing code, but for different reasons:
- Adapter converts an incompatible interface into one you expect — it's a translator
- Facade simplifies a complex subsystem into one easy-to-use interface — it's a simplifier
Key takeaways:
✅ Use Adapter when integrating third-party libraries, legacy systems, or any code whose interface doesn't match yours
✅ Use Facade when a subsystem has too many classes and the client needs a simpler API
✅ Prefer Object Adapter (composition) over Class Adapter (inheritance) — it's more flexible and works in all languages
✅ An Adapter should only translate — keep business logic out of it
✅ A Facade should hide subsystem details — don't leak internal types through the facade's API
✅ You can combine both: Facade orchestrates, Adapters translate within the workflow
What's Next
Up next in the OOP & Design Patterns series:
- OOP-13: Decorator and Proxy Patterns — wrapping objects to extend or control behavior
- OOP-14: Strategy and Template Method Patterns — behavioral patterns for interchangeable algorithms
- OOP-15: Observer, Command, and State Patterns — behavioral patterns for event handling and state machines
Practice Exercises
-
Multi-Cloud Storage Adapter: Create a
StorageServiceinterface withupload(key, data),download(key), anddelete(key)methods. Write adapters for AWS S3 (which usesputObject/getObject/deleteObject) and Google Cloud Storage (which usesbucket.file.save()/bucket.file.download()/bucket.file.delete()). Write a test that swaps providers without changing any client code. -
Home Automation Facade: Build a
SmartHomeFacadewith methods likegoodMorning(),leaveHome(),movieMode(), andgoodNight(). Each method orchestrates multiple subsystems: lights (brightness, color temperature), thermostat (temperature, mode), security (arm/disarm, cameras), and entertainment (TV, speakers). Implement in TypeScript with at least 4 subsystem classes. -
Legacy API Migration: You have a legacy REST client that returns XML strings. Build an adapter that makes it conform to a modern
ApiClientinterface returning typed objects. Then build a facade that coordinates multiple adapted API calls into a singlegetDashboardData()method. -
Adapter + Facade Combined: Build an email notification system with a
NotificationFacade.notify(userId, event)that orchestrates user preference lookup, template rendering, and email sending. The email sending should use an adapter pattern so you can swap between SendGrid, Mailgun, and a console logger for testing.
Part 12 of the OOP & Design Patterns series. First structural pattern post, building on Prototype Pattern.
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.