Facade (a simple front door over messy steps)

When to use

  • You keep writing the same multi-step dance (serialize → compress → hash → upload).
  • You want one clean method your code calls, while the Facade handles the gritty details.
  • You need consistent metadata, errors, and logging across many call sites.

Avoid when the underlying thing is already simple (one call) or a small helper function is enough.

Diagram (text)

Caller ──> BackupFacade.store_json(...)
             ├─ json.dumps(...)
             ├─ gzip.compress(...)
             ├─ sha256(...)
             └─ storage.put(..., metadata={...})

Python example (≤40 lines, type-hinted)

Concrete case: hide JSON encoding, gzip, checksum, and storage metadata behind one call.

from __future__ import annotations
from typing import Protocol, Mapping, Callable
from dataclasses import dataclass
import json, gzip, hashlib

class Storage(Protocol):
    def put(self, bucket: str, key: str, data: bytes, *, metadata: Mapping[str, str]) -> None: ...

def gzip_bytes(b: bytes) -> bytes: return gzip.compress(b)
def sha256_hex(b: bytes) -> str: return hashlib.sha256(b).hexdigest()

@dataclass
class BackupFacade:
    storage: Storage
    make_key: Callable[[str], str] = lambda name: name
    def store_json(self, bucket: str, name: str, obj: object) -> str:
        raw = json.dumps(obj, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
        gz = gzip_bytes(raw)
        key = self.make_key(f"{name}.json.gz")
        meta = {"Content-Encoding":"gzip","Content-Type":"application/json","ETag":sha256_hex(gz)}
        self.storage.put(bucket, key, gz, metadata=meta)
        return key

Tiny pytest (cements it)

def test_facade_hides_details(tmp_path):
    import gzip, json
    class FakeStorage:
        def __init__(self): self.items = []
        def put(self, b, k, d, *, metadata): self.items.append((b, k, d, metadata))

    fs = FakeStorage()
    facade = BackupFacade(fs, make_key=lambda n: f"2025-11-06/{n}")
    key = facade.store_json("metrics", "events", {"n": 1})

    bucket, saved_key, data, meta = fs.items[0]
    assert key == "2025-11-06/events.json.gz" and bucket == "metrics"
    assert meta["Content-Encoding"] == "gzip" and meta["Content-Type"] == "application/json"
    assert json.loads(gzip.decompress(data).decode()) == {"n": 1}

Trade-offs & pitfalls

  • Pros: One call site; consistent behavior; fewer leaks of vendor details; easier auditing/tests.
  • Cons: Another layer; can hide too much if it becomes a dumping ground.
  • Watch out for:
    • Overgrown Facade that knows everything—keep the surface small and cohesive.
    • Swallowing errors—wrap and re-raise with context; don’t silence.
    • Hard-coding vendor bits—use the Storage interface (or your adapters) underneath.

Pythonic alternatives

  • A plain helper function if you only need one operation.
  • Context managers for setup/teardown (temp files, staging).
  • Adapters + Facade together: Adapter normalizes the vendor API; Facade gives a friendly workflow.
  • Libraries like fsspec/s3fs/gcsfs already simplify storage—then your Facade becomes very thin.

Mini exercise

Add store_text(bucket, name, text) that sets Content-Type: text/plain and optionally gzips when len(text) > 1MB. Return the final key and whether compression was used. Write a tiny test for both small and large strings.

Checks (quick checklist)

  • Facade exposes one simple method per common workflow.
  • Internals handle serialization, compression, metadata, and storage consistently.
  • No vendor code leaks—Facade talks to an interface (e.g., Storage).
  • Errors are wrapped with context, not swallowed.
  • Tests prove callers don’t need to know the messy steps.