Module 3 · 3. Structural Patterns · Lesson 6 of 12
Adapter and Facade
Learning outcome
By the end of this lesson you will be able to: design and implement an Adapter that maps an incompatible legacy or third‑party interface to your application's expected interface, and design a Facade that provides a focused, cohesive entry point to a set of cooperating subsystems while avoiding globalizing responsibilities or squeezing business logic into the facade.
Intuition
Think of Adapter like a language interpreter for two libraries that disagree on call shape (types, units, or semantics). The Adapter accepts the interface your code expects and internally translates calls to the legacy API.
Think of Facade like a concierge desk in a hotel: clients talk to one person and the concierge coordinates housekeeping, billing, and reception. The Facade reduces surface area and sequencing complexity but should not perform the real domain work itself.
Small pseudocode structure (roles):
Deep dive
When to choose Adapter:
- You must integrate a legacy, vendor, or 3rd-party API that doesn't match your domain interfaces.
- You want to keep the rest of your codebase insulated from API evolution (wrap one place only).
- You must adapt a protocol, units, or call shape (synchronous vs asynchronous mapping is a common case).
When to choose Facade:
- The subsystem exposes many interfaces and clients rarely need low-level calls.
- You want a stable, concise API for common use cases and easier testing via seam points.
- You want to encapsulate sequencing and retries that are purely orchestration.
Trade-offs and composition guidance:
- Adapter is about interface compatibility; it should be thin and not contain business policy.
- Facade is about reducing cognitive load; it should orchestrate, not centralize domain logic. If the facade accumulates policy, split it into a separate service and keep the facade a thin coordinator.
- Prefer composition and dependency injection. Provide the Facade with specific collaborators (interfaces) rather than access to the whole subsystem.
Example responsibilities split:
- Adapter: translate shapes, convert data types, handle trivial adaptation errors.
- Facade: call Inventory -> Payment -> Notification in correct order; handle rollback coordination if orchestration fails.
Failure modes
- God facade: a single Facade that becomes the application's catch-all for business rules and collects every dependency — signals: growing method count; many unit tests that exercise business rules through the facade only.
- Leaky adapter: adapter exposes legacy semantics instead of mapping to domain concepts; clients must still know about the underlying API.
- Tight coupling: facade parameter types directly surface subsystem DTOs; prefer domain-level DTOs.
- Hidden side effects: facade that performs I/O or state changes without clear contract; make side-effects explicit and documented.
When NOT to use:
- If there is only a single call site and integration cost is low, an adapter may be unnecessary indirection.
- If you are tempted to implement domain rules in the facade because testing or layering is hard — refactor instead.
Interview drill
Coding prompt (15–25 min):
- Given a legacy logging library with Log(string) and a new interface ILogger.Log(LogEntry), implement an Adapter. Then design a Facade that coordinates Logger + Metrics + Alerting to publish an event. Explain where to put retries and how to test the orchestration.
Micro-tests / discussion points:
- How do you test an adapter? (Answer: unit-test mapping logic and behavior with a fake/spy of the adaptee.)
- How do you test a facade? (Answer: test orchestration logic with mocks for collaborators and an integration test for end-to-end flow.)
- When would you prefer a decorator over an adapter? (Decorator wraps same interface to add behavior; Adapter changes interface.)
Exercise with explanation:
- Refactor a facade that now contains business rules: move rules to a domain service; keep facade to coordinate. Explain the reasoning and list what tests change.
Revision checklist
- Adapter is thin: maps interface and minimal translation.
- Adapter does not embed business policy.
- Facade orchestrates, does not centralize policy.
- Collaborators are injected via interfaces.
- Side effects are explicit and controlled; transactions/compensations are considered.
- Unit tests cover mapping and orchestration; integration tests cover the end-to-end flow.
Production code
Guidelines for production readiness:
- Use Dependency Injection to install Adapters and Facades as scoped/singleton appropriately.
- Keep adapters stable and small; they mitigate future API churn.
- Use explicit DTOs for the facade boundary—avoid passing subsystem native DTOs through several layers.
- Document failure semantics (idempotency, retries, compensations). Implement retries in the appropriate layer (communication-level retries inside adapters or resilience policies; business-level retries in domain services).
- Avoid making Facade a global/static; prefer well-scoped instances to make lifetime and testing predictable.
Misuse warning: Do not store transient orchestration result state inside a facade instance; keep it stateless or explicitly stateful with clear lifecycle management.
Code walkthrough
The included Console example demonstrates both patterns working together:
- OldPaymentService: a legacy API that only accepts a double and has a different protocol.
- OldPaymentAdapter: Adapter implementing the modern INewPaymentProcessor and translating calls to OldPaymentService.
- InventoryManager and Notifier: cooperating subsystems.
- PaymentFacade: a thin coordinator that reserves inventory, charges payment via the adapter, and then sends a receipt.
Why this pattern is preferable here:
- Adapter isolates legacy API changes to one location.
- Facade reduces the number of moving parts the caller must coordinate and makes the orchestration explicit and testable.
Misuse to avoid (also shown in comments inside the example): do not put domain validations and rules inside the facade — delegate them to domain services.
Minimal illustrative pseudocode is provided here; a full runnable C# example accompanies this lesson in codeExamples.
Executable code examples
Adapter + Facade demo (Console, deterministic)
Program.cs
using System;
// Demonstrates Adapter + Facade together. Deterministic console output follows a fixed sequence.
// Why Adapter: isolates a legacy service (OldPaymentService) behind INewPaymentProcessor.
// Why Facade: provides a single, simple API (PaymentFacade) to coordinate Inventory->Payment->Notification.
// Misuse warning: Adapter should not become a business-rule container. Facade should remain an orchestrator only.
interface INewPaymentProcessor
{
bool Charge(decimal amount, string currency);
}
// Legacy API we must integrate with.
class OldPaymentService
{
// Legacy API takes a double and returns success.
public bool Pay(double amount)
{
Console.WriteLine($"OldPaymentService: processed payment of {amount:0.00}");
return true;
}
}
// Adapter: maps current INewPaymentProcessor to the legacy OldPaymentService.
class OldPaymentAdapter : INewPaymentProcessor
{
private readonly OldPaymentService _old;
public OldPaymentAdapter(OldPaymentService old) => _old = old;
public bool Charge(decimal amount, string currency)
{
// Adapter performs translation only (units/format). No business rules here.
var formatted = amount.ToCurrencyString(currency);
Console.WriteLine($"Adapter: adapting call to OldPaymentService to charge {formatted}");
return _old.Pay((double)amount);
}
}
class InventoryManager
{
public void Reserve(int orderId)
{
Console.WriteLine($"Inventory: reserving items for Order {orderId}");
}
}
class Notifier
{
public void SendReceipt(int orderId, decimal amount, string currency)
{
Console.WriteLine($"Notifier: sending receipt for Order {orderId}");
}
}
// Facade: orchestrates multiple subsystems. Keeps coordination logic centralized and testable.
class PaymentFacade
{
private readonly InventoryManager _inventory;
private readonly INewPaymentProcessor _payment;
private readonly Notifier _notifier;
public PaymentFacade(InventoryManager inventory, INewPaymentProcessor payment, Notifier notifier)
{
_inventory = inventory;
_payment = payment;
_notifier = notifier;
}
public bool ProcessOrder(int orderId, decimal amount, string currency)
{
Console.WriteLine($"Starting order processing for OrderId={orderId}");
_inventory.Reserve(orderId);
var success = _payment.Charge(amount, currency);
if (success)
{
_notifier.SendReceipt(orderId, amount, currency);
}
Console.WriteLine($"Order processed: OrderId={orderId}, Charge={currency} {amount:0.00}, Success={success}");
return success;
}
}
// Classic extension method (valid on current compilers) used by the adapter for deterministic formatting.
static class CurrencyExtensions
{
public static string ToCurrencyString(this decimal amount, string currency)
=> $"{currency} {amount:0.00}";
}
/*
C# 14 extension(...) block example (pseudocode) — commented out because not all compilers support this syntax yet.
This demonstrates the required C# 14 extension block conceptually; adjust when your compiler supports it.
extension (System.Decimal d)
{
public string ToCurrencyString(string currency) => $"{currency} {d:0.00}";
}
*/
class Program
{
static void Main()
{
// Arrange: create legacy service, wrap it with an adapter, install into facade.
var old = new OldPaymentService();
INewPaymentProcessor adapter = new OldPaymentAdapter(old);
var inventory = new InventoryManager();
var notifier = new Notifier();
var facade = new PaymentFacade(inventory, adapter, notifier);
// Act: process a deterministic order.
facade.ProcessOrder(orderId: 42, amount: 100m, currency: "USD");
}
}