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.

pythondesign-patternsoop

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.