Facade Pattern — Deep Dive
Building a production facade
A real facade does more than delegate calls. It handles orchestration, error recovery, and resource cleanup. Here’s a complete example: a deployment facade that coordinates multiple subsystems.
from dataclasses import dataclass
from enum import Enum, auto
class DeployStatus(Enum):
SUCCESS = auto()
ROLLED_BACK = auto()
FAILED = auto()
@dataclass
class DeployResult:
status: DeployStatus
version: str
message: str
class DockerRegistry:
def push_image(self, tag: str) -> str:
return f"sha256:abc123-{tag}"
def verify_image(self, digest: str) -> bool:
return True
class KubernetesCluster:
def apply_manifest(self, manifest: dict) -> None:
pass
def wait_healthy(self, deployment: str, timeout: int = 120) -> bool:
return True
def rollback(self, deployment: str) -> None:
pass
class SlackNotifier:
def post(self, channel: str, message: str) -> None:
pass
class DeployFacade:
"""Simplified interface for the deployment subsystem."""
def __init__(
self,
registry: DockerRegistry,
cluster: KubernetesCluster,
notifier: SlackNotifier,
):
self._registry = registry
self._cluster = cluster
self._notifier = notifier
def deploy(self, service: str, version: str) -> DeployResult:
tag = f"{service}:{version}"
# Step 1: Push and verify image
try:
digest = self._registry.push_image(tag)
if not self._registry.verify_image(digest):
return DeployResult(DeployStatus.FAILED, version, "Image verification failed")
except Exception as e:
return DeployResult(DeployStatus.FAILED, version, f"Registry error: {e}")
# Step 2: Apply to cluster
manifest = {"image": f"registry.example.com/{tag}", "replicas": 3}
try:
self._cluster.apply_manifest(manifest)
except Exception as e:
return DeployResult(DeployStatus.FAILED, version, f"Apply failed: {e}")
# Step 3: Health check with automatic rollback
if not self._cluster.wait_healthy(service):
self._cluster.rollback(service)
self._notifier.post("#deploys", f"⚠️ {service} {version} rolled back — unhealthy")
return DeployResult(DeployStatus.ROLLED_BACK, version, "Health check failed")
# Step 4: Notify
self._notifier.post("#deploys", f"✅ {service} {version} deployed successfully")
return DeployResult(DeployStatus.SUCCESS, version, "Deployed")
Callers need one line: facade.deploy("api-server", "v2.3.1"). The facade handles the four-step orchestration, rollback logic, and notifications internally.
Async facades
Modern Python applications often use async I/O. Facades adapt naturally:
import asyncio
class AsyncEmailService:
async def send(self, to: str, subject: str, body: str) -> bool:
await asyncio.sleep(0.1) # simulate network call
return True
class AsyncAuditLog:
async def record(self, event: str, details: dict) -> None:
await asyncio.sleep(0.05)
class UserFacade:
def __init__(self, email: AsyncEmailService, audit: AsyncAuditLog):
self._email = email
self._audit = audit
async def onboard_user(self, user_id: str, email_addr: str) -> bool:
# Run independent operations concurrently
email_task = self._email.send(
email_addr, "Welcome!", f"Your account {user_id} is ready."
)
audit_task = self._audit.record(
"user_onboarded", {"user_id": user_id}
)
results = await asyncio.gather(email_task, audit_task, return_exceptions=True)
email_ok = results[0] is True
audit_ok = not isinstance(results[1], Exception)
if not email_ok:
# Email is critical; audit failure is acceptable
return False
return True
The async facade can run subsystem calls concurrently with asyncio.gather, improving throughput while keeping the caller’s interface simple.
Facade with dependency injection
Inject subsystem components rather than hardcoding them. This makes the facade testable and configurable:
from typing import Protocol
class StorageBackend(Protocol):
def save(self, key: str, data: bytes) -> str: ...
def load(self, key: str) -> bytes: ...
class CompressionEngine(Protocol):
def compress(self, data: bytes) -> bytes: ...
def decompress(self, data: bytes) -> bytes: ...
class ArchiveFacade:
def __init__(self, storage: StorageBackend, compression: CompressionEngine):
self._storage = storage
self._compression = compression
def archive(self, name: str, content: bytes) -> str:
compressed = self._compression.compress(content)
return self._storage.save(f"archive/{name}", compressed)
def retrieve(self, name: str) -> bytes:
compressed = self._storage.load(f"archive/{name}")
return self._compression.decompress(compressed)
In production, inject real S3 storage and zstd compression. In tests, inject in-memory fakes. The facade code is identical.
Module-level facades in real Python projects
logging.basicConfig()
Python’s logging module has handlers, formatters, filters, and loggers. Most users just want to log messages. basicConfig() is the facade — one call configures the entire subsystem for the common case.
requests.get()
Under the hood, requests.get() creates a Session, builds a Request, prepares it as a PreparedRequest, sends it through an HTTPAdapter, and returns a Response. The top-level function hides all of that.
Django’s django.core.mail.send_mail()
Wraps email backend selection, connection management, message construction, and error handling. Callers pass subject, body, and recipients.
Facade layering strategy
In large applications, facades can form layers:
┌──────────────────────┐
│ API Layer (views) │ ← calls domain facades
├──────────────────────┤
│ Domain Facades │ ← orchestrates subsystem facades
├──────────────────────┤
│ Subsystem Facades │ ← simplifies individual subsystems
├──────────────────────┤
│ Raw Subsystems │ ← libraries, databases, services
└──────────────────────┘
Each layer adds value by hiding complexity from the layer above. The danger is over-layering — if a layer just passes calls through without simplifying anything, it adds indirection without value. Every facade layer should reduce the number of concepts the caller needs to understand.
Error handling philosophy
A facade should present domain-level errors, not subsystem-level exceptions. Don’t leak implementation details:
class PaymentError(Exception):
pass
class InsufficientFunds(PaymentError):
pass
class PaymentFacade:
def charge(self, user_id: str, amount: int) -> str:
try:
return self._stripe.create_charge(amount, self._get_token(user_id))
except stripe.CardError as e:
if e.code == "insufficient_funds":
raise InsufficientFunds(f"Card declined for user {user_id}") from e
raise PaymentError(f"Payment failed: {e.user_message}") from e
except stripe.APIConnectionError:
raise PaymentError("Payment service unavailable") from None
Callers catch PaymentError and InsufficientFunds — they never need to know about Stripe’s exception hierarchy. If you switch to a different provider, the facade’s public exceptions stay the same.
Anti-patterns
God facade
A facade that wraps the entire application into one class. If your facade has 30+ methods, it’s not simplifying — it’s just relocating complexity. Split into multiple focused facades.
Facade that blocks subsystem access
A facade should simplify, not restrict. If advanced users can’t reach the underlying subsystem when needed, the facade becomes a bottleneck. Keep the subsystem importable alongside the facade.
Stateful facade
Facades work best as stateless orchestrators. If your facade accumulates state across method calls, it’s probably a service object or a builder, not a facade.
The one thing to remember: A well-designed facade turns a multi-step, multi-component operation into a single, clear function call — handling orchestration, errors, and cleanup internally while keeping the underlying subsystem accessible for power users.
See Also
- Python Adapter Pattern How Python's Adapter Pattern works like a travel power plug — making incompatible things work together.
- Python Bridge Pattern Why separating what something does from how it does it keeps your Python code from becoming a tangled mess.
- Python Builder Pattern Why building complex Python objects step by step beats cramming everything into one giant constructor.
- Python Composite Pattern How the Composite Pattern lets you treat a group of things the same way you'd treat a single thing in Python.
- Python Flyweight Pattern How the Flyweight Pattern saves memory by sharing common data instead of copying it thousands of times.