Software Design Reference Core Designer · Structuring for Stability

Lesson 0008 · Core Designer · Module 2

Strategy, Adapter, and Facade, Intentionally

Lesson 0004 taught you to notice Strategy emerging. Here you learn to reach for it — and for its two structural cousins — on purpose, with the problem first.

Reading ≈ 11 minPractice ≈ 25 min

Mission tie-in: "use patterns intentionally" means naming the problem you have before naming the pattern that fixes it. That order is the whole difference between designing and decorating.

Knowledge: three problems, three shapes

Strategy — problem: one behavior varies; the context should not. Shape: interface + implementations + delegation. You saw it emerging in lesson 0004; here you apply it before the branch chain forms, because the variation is already proven.

Adapter — problem: your code needs interface A; a service speaks interface B, and B is not yours to change. Shape: a wrapper that implements A and translates into B.

class PaymentGateway(Protocol):              # your interface: named for the need
    def charge(self, amount: Money, token: str) -> str: ...

class StripeSdkAdapter:                      # adapts a third-party SDK to it
    def __init__(self, client: StripeSDK) -> None:
        self._client = client

    def charge(self, amount: Money, token: str) -> str:
        result = self._client.payment_intents.create(
            amount=int(amount.cents), currency=amount.currency, token=token
        )
        return result.id

Note the naming: the interface is PaymentGateway, not StripeGateway. An adapter that names itself after the thing it adapts has already let the vendor define your vocabulary — the arrow you will straighten in lesson 0009.

Facade — problem: clients must orchestrate several subsystems to do one recurring thing. Shape: one class with a few coarse methods that drive the subsystems.

class CheckoutFacade:
    def __init__(self, inventory, payments, shipping, email) -> None: ...

    def place_order(self, cart: Cart, payment_token: str) -> Order:
        self._inventory.reserve(cart)
        charge_id = self._payments.charge(cart.total, payment_token)
        order = self._shipping.create_shipment(cart)
        self._email.send_confirmation(cart.customer, order)
        return order

Three patterns, three problems: vary a behavior, translate an interface, simplify a ritual. If you cannot name which problem you have, applying any of them is decoration.

† The test of intentional use: the pattern disappears from the problem. A Strategy means callers never mention variants; an Adapter means callers never see the SDK; a Facade means callers never see the subsystems.
The principle behind the principle

A pattern is not a shape you like; it is a decision about where variation lives and who absorbs a change. Each of these three answers a different question about one boundary, which is why "which pattern?" is answerable at all:

Naming the shape is not decoration either. Its practical value is prediction: a reviewer who recognises a Strategy knows, without reading the code, where the next variant goes and what will not be touched. That predictability is most of what "readable architecture" means in a team of more than three people.

Every one of the three costs the same thing: one level of indirection, paid on every future read. So the test is not "does this apply?" — all three usually could — but does it remove a repeated decision? A Facade over a single call removes nothing and adds a hop. An Adapter around an SDK you call in one place removes nothing today; what it buys is optionality, and optionality has a price you should state rather than assume.

Where each stops applying. A Facade that accumulates every workflow in the system becomes the god object it was meant to prevent — one facade owns one ritual. An Adapter whose interface is the SDK's interface with different capitalisation has adapted nothing; the test is whether your method names would still make sense if the vendor disappeared tomorrow. And a Strategy degenerates when variants need to combine, or when selection requires more than one key: a two-dimensional variation produces a matrix of classes, and the honest answer there is a rules table or a pipeline, not more classes.

How a senior designer chooses in a real review

A payments integration. Three engineers, a six-week window, one vendor live and a second in a commercial conversation that may or may not conclude.

They start by counting call sites, not by naming patterns. The SDK is called in six places across three modules. That number is the input to everything else: it is the size of the edit if the vendor changes, and the size of the indirection tax if it does not.

Then they price both errors. Wrong to adapt: one extra file, about forty lines, and one hop of indirection on six call sites — recoverable in an afternoon. Wrong not to adapt: a vendor migration touching six call sites in three modules, with no test that can run without a network, under whatever deadline the commercial decision arrives with. The costs are wildly asymmetric, so the adapter goes in — not because adapters are good, but because the mistake it prevents is the expensive one.

Then they reject the version of this decision that has no brakes. The same argument, applied uniformly, produces a port for the logger, a port for datetime.now(), and a port for the templating library. The rule that stops it is worth memorising: adapt what you might have to replace; stub what you must control in a test; leave everything else alone. A clock is the second kind — you do not need a ClockPort hierarchy, you need to pass a function. A logger is usually the third kind: you are not going to replace it, and if you do, the migration is a configuration change.

Then the facade gets the same scrutiny. The proposed CheckoutFacade also handles refunds and subscription renewals. Three rituals, one class — and that class becomes the file every team edits, which is the merge-conflict hot-spot from lesson 0006 arriving by a different road. One ritual per facade. If two rituals share steps, share the steps, not the facade.

The consequence, eighteen months on. The payments port did its job: the vendor change was eleven days of work, contained to one adapter and its tests, with the domain untouched. The logging port that nobody needed cost four hours to write, and then cost every new joiner an afternoon of confusion about why log.info was not log.info. Both decisions came from the same principle. Only one of them was priced.

Field notes · what it looks like in real code
The problem you haveWhat it costsThe shape that fits
the SDK is called in six filesVendor churn lands in six places, each a separate edit.Adapter: one wrapper, one place the vendor can change.
four calls that always run togetherThe ritual is copied per caller and drifts apart.Facade: one coarse method that owns the sequence.
the if-chain grew a fifth armA variation point with a growing list of branches.Strategy: one implementation per arm.
class OrderManager wrapping one callA pattern applied with no problem behind it.Delete the wrapper; call the thing directly.

When this is the wrong move

Skip the adapter when there is one call site, no realistic replacement, and a test that already runs without the dependency — the wrapper is then pure ceremony, and ceremony is what gives patterns a bad name. Skip the facade when the ritual happens in exactly one place: a sequence with one caller is a function, and wrapping it in a class hides nothing from nobody. Skip the strategy when there are two stable arms. And treat a facade in a published library differently from one inside your own system: there, freezing a sequence behind one method is a compatibility commitment your users will hold you to for years, so the convenience you are offering is also a constraint you are accepting.

What people get wrong after reading this
Adapter and Facade are the same thing, since both wrap something.
They differ in what they change. An Adapter changes the vocabulary of one thing so it fits an interface you already have. A Facade changes the granularity of several things so one call replaces four. You can tell them apart by asking what would break: rename the vendor's methods and the adapter changes; reorder the steps and the facade changes.
An adapter should expose the SDK's full capability.
An adapter that mirrors the SDK method for method has adapted nothing — it is the SDK with your naming convention, and swapping vendors still rewrites every caller. The port should be shaped by what the caller needs, which is usually three methods where the vendor offers forty.
Patterns add complexity, so a codebase with fewer of them is simpler.
The complexity was already there, in the form of a decision repeated at every call site. A pattern names it and puts it in one place. What genuinely adds complexity is a pattern with no repeated decision behind it — and the way to tell the difference is to point at the repetition before you point at the pattern.
You choose one of the three.
They are orthogonal and frequently stack. A facade routinely selects a strategy and calls two adapters. "Which pattern is this?" is the wrong question when the honest answer is "three, at three different boundaries".

Skill: match the problem to the pattern

Your code calls a legacy SOAP service whose interface you can't change. Which pattern?

Every order triggers reserve, charge, ship, email in sequence. Which pattern?

You call one payment SDK from exactly one place and will not change vendors. An adapter is:

Practice on your own code

Find one recurring ritual (three or more lines repeated in several callers) and one interface mismatch in your codebase. Before applying anything, price both errors the way the review above does: what does it cost if you wrap it and never needed to, and what does it cost if you do not and later must? Then apply a Facade to the ritual and an Adapter to the mismatch. If you find neither, say so out loud — refusing to apply a pattern is the same discipline as applying one.

In the lab: the returns-resolution service contains all three problems — a growing branch over resolution outcomes, a third-party carrier client called from several places, and a four-step ritual copied into two callers. The phase-1 checkpoint asks you to label each one with the problem it actually is before naming any pattern, and to defend one case where the right answer is to do nothing.

Reveal: judgment on a tricky case

An EmailSender wraps both an SMTP library and a template system. Two subsystems, one recurring job ("send a templated email to a user"). That's a Facade — but with one eye on lesson 0009: the facade is also the natural port the domain will depend on.

Teach it back
  1. Explain the difference between an Adapter and a Facade to a junior in two sentences, using your own codebase as the example.
  2. Find one wrapper in your own code this week that has no repeated decision behind it, and name its cost: how many readers have paid a hop for it, and what it has protected you from so far.
  3. Predict what happens in a year to a facade that keeps acquiring workflows — which team ends up owning it, and what the merge queue looks like.

Your win

You can now state the problem first — varying behavior, foreign interface, repeated ritual — and reach for exactly one of the three shapes, for a reason you can defend in review.

Read and watch deeper

Describe a messy integration to your agent-teacher and ask which of the three problems it is — before either of you names a pattern.