The Facade pattern — one simple interface over a complicated subsystem
Not every wrapper is solving an incompatibility problem — sometimes the actual issue is that doing one common thing correctly requires coordinating five different objects in the right order, and every caller shouldn't have to know that.
4 min read
The problem: a simple task that requires coordinating several objects
class InventoryChecker:
def check_stock(self, product_id): ...
class PaymentProcessor:
def charge(self, amount): ...
class ShippingCalculator:
def calculate_cost(self, address, weight): ...
class OrderNotifier:
def send_confirmation(self, email): ...
# Placing an order means every caller has to get this exact sequence right:
def place_order(product_id, amount, address, weight, email):
if not InventoryChecker().check_stock(product_id):
raise ValueError("out of stock")
PaymentProcessor().charge(amount)
shipping_cost = ShippingCalculator().calculate_cost(address, weight)
OrderNotifier().send_confirmation(email)
return shipping_costEach of these four classes does one focused job well — this isn't a design mistake, it's the Single Responsibility principle from the SOLID lesson, working as intended. The problem is what happens at the call site: "place an order" is conceptually one operation, but actually doing it correctly means knowing about four separate classes, instantiating each one, and calling them in the right order with the right arguments. Every place in the codebase that needs to place an order has to get all of that right, independently.
The fix: one class that knows the correct sequence, so callers don't have to
class OrderFacade:
def __init__(self):
self._inventory = InventoryChecker()
self._payment = PaymentProcessor()
self._shipping = ShippingCalculator()
self._notifier = OrderNotifier()
def place_order(self, product_id, amount, address, weight, email):
if not self._inventory.check_stock(product_id):
raise ValueError("out of stock")
self._payment.charge(amount)
shipping_cost = self._shipping.calculate_cost(address, weight)
self._notifier.send_confirmation(email)
return shipping_cost
# every call site is now this simple:
OrderFacade().place_order(product_id=1, amount=50, address="...", weight=2, email="ada@example.com")OrderFacade doesn't add any new capability — every one of these calls could already be made directly. What it provides is a single, simple entry point that encapsulates the correct coordination sequence in exactly one place. Every call site now just calls place_order(...) without needing to know that four separate subsystems exist underneath, or in what order they need to be invoked.
Why this is different from Adapter, since both simplify what's underneath
The Adapter pattern (from its own lesson) solves an incompatibility problem — two interfaces that don't match, translated so one can stand in for the other, with nothing conceptually new added or removed. Facade solves a complexity problem — the underlying interfaces are all perfectly usable and compatible on their own, there are just many of them, and coordinating them correctly is the actual difficulty. An adapter typically wraps one object to translate its interface; a facade typically coordinates several objects into one simplified operation. Both reduce what a caller needs to know, but for genuinely different reasons.
The facade doesn't hide the subsystem — it just makes it optional to touch directly
# Most callers use the facade:
OrderFacade().place_order(...)
# But a caller with an unusual need can still reach the subsystem directly:
checker = InventoryChecker()
if checker.check_stock(product_id):
print("in stock, but not ordering yet")A well-designed facade doesn't make the underlying classes private or inaccessible — InventoryChecker, PaymentProcessor, and the rest are all still directly usable for a caller with a genuinely unusual need (checking stock without placing an order, say). The facade is a convenience for the common case, not a wall preventing access to what's underneath — this is exactly what distinguishes a good facade from over-restrictive encapsulation that forces every caller through one narrow door even when they need something the facade doesn't offer.
Where this shows up in code you already use
Django's own render(request, template, context) function (from the templates lesson) is a facade: underneath, it coordinates loading the template file, resolving {% extends %}/{% include %} inheritance, rendering the context into it, and wrapping the result in an HttpResponse — several real steps, most view code never needs to think about individually. A well-designed client library for a cloud service (one client.upload_file(path) call, instead of manually handling authentication, chunked upload requests, and retry logic yourself) is the same pattern applied to a much larger, more genuinely complicated subsystem.
The concrete signal a Facade belongs somewhere
Multiple call sites independently coordinating the same several objects in the same order to accomplish one conceptual task is the tell — especially once that coordination logic has to change (a new required step, a reordered sequence) and every call site would need updating individually. A single class with one clearly-scoped job doesn't need a facade in front of it; the pattern earns its keep specifically once "doing this one common thing correctly" requires knowing about, and correctly sequencing, more than one class.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why does OrderFacade.place_order() add value even though every underlying class it calls already works correctly on its own?
2. What's the structural difference between what Adapter and Facade each solve?
3. Does a well-designed Facade prevent direct access to the classes it coordinates?
4. How is Django's render(request, template, context) an example of the Facade pattern?