A new engineer joins a team and is asked to add "place an order" to a second client, a mobile backend. She opens the existing web code and finds a function that imports an inventory client, a payments client and a shipping client, calls them in a specific order, and has a try block with four except branches. Nothing in the file says why the order matters. She copies it. Her copy handles three of the four failures. Two months later, a customer is charged for an order that was never shipped, and nobody can say which of the two copies is the correct one.
That story is the whole case for the Facade pattern, and it is also the reason most facades fail. The pattern is not "a class that wraps other classes." Wrapping is the cheap part. A facade earns its place only when it deletes decisions from the caller: which subsystem to call first, which vocabulary to use, what to undo when step three fails. If your facade forwards calls one-to-one, you have added a layer and removed nothing.
The pattern in one paragraph
A facade is an object that offers a simplified interface to a group of collaborating classes. Callers talk to the facade, the facade talks to the subsystem, and the subsystem does not know the facade exists. The Facade entry on Refactoring Guru puts the intent plainly: provide a simple interface to a complex subsystem, and note that the facade does not stop callers from using the subsystem directly when they need to. Keep that last clause in mind. It is the part people drop, and it is the third failure mode below.
Facade differs from the neighboring patterns in what it changes. An Adapter changes an interface so that two things that disagree can talk. A Decorator keeps the interface and adds behavior. A Facade defines a new, smaller interface over many objects. Adapter is about compatibility, Decorator is about behavior, and Facade is about the number of things the caller has to know.
A checkout facade you can run
Take the subsystem from the opening: inventory, payments, shipping. Each has its own vocabulary, its own exceptions, and its own idea of what "undo" means. This is the shape most teams already have, just spread across call sites.
class Inventory:
def reserve(self, sku, qty): ... # raises OutOfStock, returns (sku, qty)
def release(self, reservation): ...
class Payments:
def charge(self, token, cents): ... # raises CardDeclined, returns charge id
def refund(self, charge_id): ...
class Shipping:
def create_label(self, zip_code, items): ... # raises NoCarrier, returns label id
The facade has one verb, one result type and one failure contract. The interesting part is not the happy path. It is the unwinding, because that is the decision the caller used to get wrong.
@dataclass
class OrderResult:
ok: bool
reason: str = ""
receipt: dict = field(default_factory=dict)
class Checkout:
def __init__(self, inventory, payments, shipping):
self._inv, self._pay, self._ship = inventory, payments, shipping
def place_order(self, sku, qty, token, cents, zip_code) -> OrderResult:
undo = []
try:
res = self._inv.reserve(sku, qty)
undo.append(lambda: self._inv.release(res))
charge = self._pay.charge(token, cents)
undo.append(lambda: self._pay.refund(charge))
label = self._ship.create_label(zip_code, [sku])
except (OutOfStock, CardDeclined, NoCarrier) as e:
for step in reversed(undo):
step()
return OrderResult(False, type(e).__name__)
return OrderResult(True, receipt={"charge": charge, "label": label})
I ran this against in-memory fakes with five units of stock and four orders: one that succeeds, one with a declined card, one to a zip code with no carrier, and one asking for nine units. The first order succeeds and takes two units. The declined card fails after the reservation, so the reservation is released. The unreachable zip code is the interesting one: the reservation is made, the card is charged, then the label fails, and the log shows the refund for ch_ok_1999 followed by the release. The oversized order fails at the first step and has nothing to undo. Final stock is three, which is five minus the single successful order of two. The payment log contains exactly two charges and exactly one refund. Every other path leaves the world as it found it.
None of this is clever, and that is the point. Before the facade, those twelve lines lived in every call site, each copy a little different. After it, a caller writes checkout.place_order(...), checks result.ok, and never imports CardDeclined. The mobile backend and the web app can no longer disagree about what a failed order means, because the meaning lives in one place.
Failure one: the facade that forwards
The most common bad facade is a class with the same method names as the subsystem, each one delegating to the original. It looks like structure and does nothing. A quick test: take your facade, and for each public method ask whether the caller still needs to know the order of operations, the error types, or the subsystem's data structures. If the answer is yes for most methods, the facade is a proxy with extra steps.
The telltale sign is the return type. If place_order returned the inventory's Reservation object or the payment provider's Charge record, the caller would have to import those types to use the result, and the coupling you meant to hide would be back in every file. In the code above, the result is an OrderResult that the facade owns, with a reason string that is the exception class name. In a real system you would use an enum, but the principle is the same: the facade has its own vocabulary, translated at the boundary.
Failure two: the facade that grows into the subsystem
The opposite failure is slower and more tempting. A facade for checkout gets a coupon parameter, then gift_wrap, then split_shipment, then retry_payment_on_soft_decline. Each addition is reasonable. Each one is a caller who needed something the simple interface did not offer. Eighteen months later, place_order has fourteen parameters, three of which interact, and the facade is exactly as complicated as the subsystem it was supposed to hide, with the added disadvantage that it is yours to maintain.
I would not fight this with discipline alone. Fight it with a rule: a facade serves the common case, and anything that is not the common case does not go in. If the common case covers eighty percent of call sites, the other twenty percent should use the subsystem directly, and that is not a failure of the facade. It is the facade working. The pattern gets in trouble when the team treats every call site that bypasses it as a bug.
Failure three: the facade with no escape hatch
The third failure is the one Refactoring Guru's clause warns about. If the only way to reach inventory is through Checkout, then the day someone needs a bulk reservation, or a streaming progress report, or a feature the facade does not model, they will either fork the facade or write a second one beside it. Both are worse than the original problem.
The standard library shows the right shape. Python's subprocess documentation recommends run() for the cases it can handle and describes Popen as the underlying interface for more advanced use. They coexist. run() is a facade over Popen, it deletes decisions (wait for exit, collect output, raise on failure if you ask), and nobody is blocked from Popen when they need to read a line before the process exits.
I checked that distinction rather than trust the docs:
r = subprocess.run([sys.executable, "-c", "print('hi')"],
capture_output=True, text=True, check=True)
print(r.stdout.strip(), r.returncode) # hi 0
p = subprocess.Popen([sys.executable, "-c",
"import time;print('a',flush=True);time.sleep(.2);print('b')"],
stdout=subprocess.PIPE, text=True)
print(p.stdout.readline().strip()) # a, before the process exits
p.wait()
The first call is the common case in two lines. The second is the case run() cannot serve: reading output while the child is still running. Both work because the facade sits beside the subsystem, not in front of it.
Git has the same design at a larger scale. The Pro Git book splits commands into plumbing, the low-level operations that manipulate the object database, and porcelain, the user-facing commands such as commit and checkout. Porcelain is a facade over plumbing. Scripts that need exactness use plumbing, people use porcelain, and the existence of both is why Git has survived two decades of people building tools on top of it. The Requests library is another case: requests.get() is a one-line facade over connection pooling, redirects, encoding and cookies, and when you need control over any of those, you drop to a Session and its adapters.
For your own facade, the escape hatch can be as small as exposing the collaborators read-only, or accepting them as constructor arguments so a caller can build the subsystem directly with the same instances. What matters is that the door exists and is documented.
Yes, but: isn't this just a service layer?
In many codebases the Facade is the application service. Checkout is what Domain-Driven Design would call an application service, and the unwinding logic in place_order is a hand-rolled saga. That is a fair objection, and it deserves a straight answer: yes, they overlap, and the name you use matters less than the properties. The facade should be thin on policy. If it starts deciding who is allowed to order, what discount applies, or which warehouse ships, it has become a domain service and should be named and tested as one.
The second objection is the compensation code. My undo list is not a real saga. The refund can fail, the process can crash between the charge and the label, and the in-memory list is gone with it. A production version needs durable state for the half-finished order, which is the territory of the Saga pattern from earlier in this series. A facade does not make distributed failure go away. It makes the failure contract explicit in one place, which is the precondition for fixing it properly.
What to do on Monday
Pick one workflow in your codebase that three or more call sites perform the same way: onboarding a user, issuing a refund, publishing a document. Diff the call sites. Wherever they differ, someone made a decision, and probably nobody remembers why. Write the facade for that workflow with one verb, a result type you own, and the unwinding in one place. Then write a single test per failure path, asserting that the world is back where it started.
Finally, leave the subsystem importable and say so in the docstring. A facade that has to be right for every caller will be wrong for some of them. One that stays out of the way when it is not needed will still be there in five years.

