Skip to content

Facade ​

Facade gives a feature a small interface over several cooperating subsystems. The subsystems still do the work; the facade owns the sequence and result presented to the caller.

When it helps in iOS ​

A checkout button should not know how to reserve inventory, charge a payment method, and create a receipt. Putting that sequence in a facade keeps the view model focused on loading and displaying UI state.

Example: a checkout operation ​

swift
import Foundation

protocol InventoryReserving {
    func reserve(itemID: String) async throws -> String
}

protocol PaymentCharging {
    func charge(amount: Decimal, reservationID: String) async throws -> String
}

protocol ReceiptCreating {
    func create(for chargeID: String) async throws -> URL
}

struct CheckoutFacade {
    let inventory: any InventoryReserving
    let payments: any PaymentCharging
    let receipts: any ReceiptCreating

    func buy(itemID: String, amount: Decimal) async throws -> URL {
        let reservationID = try await inventory.reserve(itemID: itemID)
        let chargeID = try await payments.charge(
            amount: amount,
            reservationID: reservationID
        )
        return try await receipts.create(for: chargeID)
    }
}

// The checkout view model calls try await checkout.buy(...).

The caller asks for one outcome, a receipt URL. It does not coordinate three services or know their intermediate IDs.

How to use it in a real app ​

Inject the facade into the checkout view model. Show a loading state while buy runs, then present the receipt or an error. A real payment flow also needs idempotency keys, cancellation handling, and compensation if a later step fails after charging. Design those rules with the backend; the example shows only the facade boundary.

Watch for: Do not let a facade become a giant service that owns every feature. Keep its interface centered on one user operation.

Trace the operation ​

The checkout view model asks for a receipt URL. The facade converts that single request into reserve, charge, then receipt creation, passing each returned ID to the next step. The view model does not need to know those intermediate types.

A failed receipt step after a successful charge requires a recovery plan; blindly retrying the whole buy method can double-charge. In production, design idempotency and compensation with the backend and expose a result that can represent a pending receipt. Test the ordering of subsystem calls and the error at each step.

Follow the orchestration boundary ​

CheckoutFacade.buy is the one operation visible to its caller. It reserves inventory, sends the reservation ID to payment, and sends the charge ID to receipt creation. Each subsystem keeps its specialized job; the facade owns the order and mapping between them. The returned receipt URL is a feature-level result that a view model can present without knowing intermediate IDs.

From example to an iOS checkout screen ​

The view model sets a submitting state, awaits buy, and then navigates to the receipt or displays an actionable failure. Disable repeated taps, but also give the backend an idempotency key because UI controls alone cannot prevent duplicate requests. If inventory reservation expires, surface that distinctly from a declined payment. If receipt creation fails after charging, the app must not simply restart the whole operation; it may need to show a pending receipt and reconcile later.

Failure modes and verification ​

Test each step failing and assert which later services were not called. Test a successful sequence and the exact IDs forwarded. Cancellation may occur while a charge is in flight; cancellation of the UI task does not guarantee the backend charge stopped. A facade is useful for one coherent operation, but an app-wide facade with unrelated methods becomes another monolith.

Next: Strategy →

Released under the MIT License.