ExampleDifficulty: Intermediate2 min

Facade Example: Placing an Order Across Stock, Payment and Shipping

place_order() checks stock, charges the card and books a courier, so the web handler calls one method.

Placing an order is a natural Facade, and this example maps the template onto it. A shop's checkout handler needs three services in order: reserve the stock, charge the card, book a courier. The sequence has one rule that is easy to forget, release the stock when the payment is declined, and the facade is the one place that rule is written.

The template's participants in the order example
TemplateOrder example
FacadeOrderFacade, with place_order
Subsystem classesInventory, PaymentService and Shipping
ClientThe web handler that receives the order form
py
class Inventory:
    def __init__(self, stock: dict[str, int]) -> None:
        self._stock = stock

    def reserve(self, sku: str, quantity: int) -> bool:
        if self._stock.get(sku, 0) < quantity:
            return False
        self._stock[sku] -= quantity
        print(f"inventory: reserved {quantity} x {sku}")
        return True

    def release(self, sku: str, quantity: int) -> None:
        self._stock[sku] += quantity
        print(f"inventory: released {quantity} x {sku}")


class PaymentService:
    def charge(self, card: str, pence: int) -> bool:
        approved = not card.endswith("0000")
        print(f"payment: {pence} pence on card ending {card[-4:]}, {'approved' if approved else 'declined'}")
        return approved


class Shipping:
    def book(self, sku: str, address: str) -> str:
        print(f"shipping: courier booked for {sku} to {address}")
        return "SHIP-7781"


class OrderFacade:
    def __init__(self, inventory: Inventory, payments: PaymentService, shipping: Shipping) -> None:
        self._inventory = inventory
        self._payments = payments
        self._shipping = shipping

    def place_order(self, sku: str, quantity: int, card: str, pence: int, address: str) -> str:
        if not self._inventory.reserve(sku, quantity):
            return "out of stock"
        if not self._payments.charge(card, pence):
            self._inventory.release(sku, quantity)
            return "payment declined"
        return self._shipping.book(sku, address)


shop = OrderFacade(Inventory({"LAMP-01": 3}), PaymentService(), Shipping())
print(shop.place_order("LAMP-01", 1, "4111111111111111", 3999, "Leeds"))
print(shop.place_order("LAMP-01", 1, "4000000000000000", 3999, "York"))
OutputRan with python:3.12-slim (Python 3.12.13)
inventory: reserved 1 x LAMP-01
payment: 3999 pence on card ending 1111, approved
shipping: courier booked for LAMP-01 to Leeds
SHIP-7781
inventory: reserved 1 x LAMP-01
payment: 3999 pence on card ending 0000, declined
inventory: released 1 x LAMP-01
payment declined
One method, three services, and the release when the card is declined.

The first order reserves a lamp, charges the card and returns a shipment reference. The second card is declined, so the facade releases the reserved lamp before it reports the failure, and stock is back where it started. Without the facade, every handler that places an order would need to remember that release, and the one that forgot would lose stock.

The facade holds the order of calls and the clean-up between them, and nothing about stock levels, card checks or couriers. Those stay in the three services, where a stock report or a refund can use them without going through place_order.