Skip to content
Commitwise
Menu
Design Patterns

Facade

Give a complicated subsystem one simple entry point, such as a checkout made of cart, inventory, payment and shipping services, keep the facade thin and its steps in a safe order, and stop it from growing into a god object, in Swift, Kotlin, Java and TypeScript.

beginner

The core idea with one simple example.Step by step, with more examples and exercises.A cheat sheet: key APIs, gotchas and trade-offs.Internals, trade-offs and production pitfalls.

Placing an order touches six classes: the cart, inventory, a payment SDK, a shipping quote service, the order repository and analytics. The checkout screen calls all six, in the right order, with the right error handling. So does the “buy again” button. So does the one-tap purchase from a notification. Three copies of the same choreography, and when the payment step changes, two of them get updated.

A Facade puts that choreography behind one simple method, placeOrder(cart), so every caller gets it right without knowing the six classes behind it.

New to some of these? Open what you need, or skip ahead.

Basics:What is an interface (or protocol)?

An interface (Kotlin, Java, TypeScript) or protocol (Swift) is a list of things a type promises it can do, without saying how. Code that only knows the interface can use any class that implements it, including a fake one in tests.

Basics:What is async/await?

Some work takes a while: a network request, reading a file. async/await lets you write that code top to bottom while the app keeps responding: let order = try await checkout.placeOrder(cart) waits for the result without blocking the screen. The Concurrency tutorial covers it in all four languages.

Part Not started

One door into a subsystem

A hotel reception desk is a facade. You don’t call housekeeping, the kitchen, the laundry and the taxi company yourself; you tell reception “a late checkout and a taxi at noon, please”, and they deal with everyone. The departments still exist and still do the work; you just don’t have to know them.

In code, a facade is a class with a few simple methods that hide a group of classes working together. Screens call checkout.placeOrder(cart); the facade calls inventory, payment, shipping and the rest, in the right order.

A Facade defines a higher-level interface over a subsystem, reducing coupling between clients and the subsystem’s many types. Unlike an Adapter it doesn’t make an existing interface fit another; it creates a new, narrower one for the common use cases. It typically holds no business rules of its own: it orchestrates (sequencing, transactions or compensation, error mapping) and delegates. In layered architectures it often appears as an application service or use case; at module boundaries, as the module’s public API.

Before: Every caller choreographs six classes

// In CheckoutViewModel, and again in BuyAgainViewModel and in the notification handler
let quote = try await shipping.quote(for: cart.items, to: address)
try await inventory.reserve(cart.items)
let payment = try await payments.charge(cents: cart.totalCents + quote.cents, token: token)
let order = try await orders.create(cart: cart, shipping: quote, paymentId: payment.id)
analytics.track(.purchase(orderId: order.id, cents: order.totalCents))
cart.clear()
// In CheckoutViewModel, and again in BuyAgainViewModel and in the notification handler
val quote = shipping.quote(cart.items, address)
inventory.reserve(cart.items)
val payment = payments.charge(cart.totalCents + quote.cents, token)
val order = orders.create(cart, quote, payment.id)
analytics.track(Event.Purchase(order.id, order.totalCents))
cart.clear()
// In CheckoutViewModel, and again in BuyAgainViewModel and in the notification handler
ShippingQuote quote = shipping.quote(cart.items(), address);
inventory.reserve(cart.items());
Payment payment = payments.charge(cart.totalCents() + quote.cents(), token);
Order order = orders.create(cart, quote, payment.id());
analytics.track(new PurchaseEvent(order.id(), order.totalCents()));
cart.clear();
// In CheckoutViewModel, and again in BuyAgainViewModel and in the notification handler
const quote = await shipping.quote(cart.items, address);
await inventory.reserve(cart.items);
const payment = await payments.charge(cart.totalCents + quote.cents, token);
const order = await orders.create(cart, quote, payment.id);
analytics.track({ type: "purchase", orderId: order.id, cents: order.totalCents });
cart.clear();

After: One facade, one call

final class CheckoutFacade {
    private let shipping: ShippingService
    private let inventory: InventoryService
    private let payments: PaymentGateway
    private let orders: OrderRepository
    private let analytics: Analytics

    init(shipping: ShippingService, inventory: InventoryService, payments: PaymentGateway,
         orders: OrderRepository, analytics: Analytics) {
        self.shipping = shipping; self.inventory = inventory; self.payments = payments
        self.orders = orders; self.analytics = analytics
    }

    func placeOrder(_ cart: Cart, address: Address, token: String) async throws -> Order {
        let quote = try await shipping.quote(for: cart.items, to: address)
        let reservation = try await inventory.reserve(cart.items)
        do {
            let payment = try await payments.charge(cents: cart.totalCents + quote.cents, token: token)
            let order = try await orders.create(cart: cart, shipping: quote, paymentId: payment.id)
            analytics.track(.purchase(orderId: order.id, cents: order.totalCents))
            return order
        } catch {
            await inventory.release(reservation)         // undo the step that succeeded
            throw error
        }
    }
}

// Every caller:
let order = try await checkout.placeOrder(cart, address: address, token: token)
class CheckoutFacade(
    private val shipping: ShippingService,
    private val inventory: InventoryService,
    private val payments: PaymentGateway,
    private val orders: OrderRepository,
    private val analytics: Analytics,
) {
    suspend fun placeOrder(cart: Cart, address: Address, token: String): Order {
        val quote = shipping.quote(cart.items, address)
        val reservation = inventory.reserve(cart.items)
        return try {
            val payment = payments.charge(cart.totalCents + quote.cents, token)
            orders.create(cart, quote, payment.id).also {
                analytics.track(Event.Purchase(it.id, it.totalCents))
            }
        } catch (e: Exception) {
            inventory.release(reservation)              // undo the step that succeeded
            throw e
        }
    }
}

// Every caller:
val order = checkout.placeOrder(cart, address, token)
public final class CheckoutFacade {
    private final ShippingService shipping;
    private final InventoryService inventory;
    private final PaymentGateway payments;
    private final OrderRepository orders;
    private final Analytics analytics;

    public CheckoutFacade(ShippingService shipping, InventoryService inventory, PaymentGateway payments,
                          OrderRepository orders, Analytics analytics) {
        this.shipping = shipping; this.inventory = inventory; this.payments = payments;
        this.orders = orders; this.analytics = analytics;
    }

    public Order placeOrder(Cart cart, Address address, String token) {
        ShippingQuote quote = shipping.quote(cart.items(), address);
        Reservation reservation = inventory.reserve(cart.items());
        try {
            Payment payment = payments.charge(cart.totalCents() + quote.cents(), token);
            Order order = orders.create(cart, quote, payment.id());
            analytics.track(new PurchaseEvent(order.id(), order.totalCents()));
            return order;
        } catch (RuntimeException e) {
            inventory.release(reservation);                 // undo the step that succeeded
            throw e;
        }
    }
}

// Every caller:
Order order = checkout.placeOrder(cart, address, token);
export class CheckoutFacade {
  constructor(
    private readonly shipping: ShippingService,
    private readonly inventory: InventoryService,
    private readonly payments: PaymentGateway,
    private readonly orders: OrderRepository,
    private readonly analytics: Analytics,
  ) {}

  async placeOrder(cart: Cart, address: Address, token: string): Promise<Order> {
    const quote = await this.shipping.quote(cart.items, address);
    const reservation = await this.inventory.reserve(cart.items);
    try {
      const payment = await this.payments.charge(cart.totalCents + quote.cents, token);
      const order = await this.orders.create(cart, quote, payment.id);
      this.analytics.track({ type: "purchase", orderId: order.id, cents: order.totalCents });
      return order;
    } catch (e) {
      await this.inventory.release(reservation);        // undo the step that succeeded
      throw e;
    }
  }
}

// Every caller:
const order = await checkout.placeOrder(cart, address, token);
  • Callers know one class. The view models no longer depend on six services, so they’re simpler to read and to test (one fake facade instead of six fakes).
  • The order of steps lives in one place: quote, reserve stock, charge, create the order. When it changes, it changes once.
  • Failures are handled once: if the payment fails, the facade releases the reserved stock, so no caller can forget.
  • The subsystem stays available. An admin screen that only needs inventory can still use it directly; the facade is a convenience, not a wall.
Spot the bugCharged, but no order

Tap the line with the bug.

func placeOrder(_ cart: Cart, address: Address, token: String) async throws -> Order {
    let quote = try await shipping.quote(for: cart.items, to: address)
    let payment = try await payments.charge(cents: cart.totalCents + quote.cents, token: token)
    try await inventory.reserve(cart.items)
    return try await orders.create(cart: cart, shipping: quote, paymentId: payment.id)
}
suspend fun placeOrder(cart: Cart, address: Address, token: String): Order {
    val quote = shipping.quote(cart.items, address)
    val payment = payments.charge(cart.totalCents + quote.cents, token)
    inventory.reserve(cart.items)
    return orders.create(cart, quote, payment.id)
}
public Order placeOrder(Cart cart, Address address, String token) {
    ShippingQuote quote = shipping.quote(cart.items(), address);
    Payment payment = payments.charge(cart.totalCents() + quote.cents(), token);
    inventory.reserve(cart.items());
    return orders.create(cart, quote, payment.id());
}
async placeOrder(cart: Cart, address: Address, token: string): Promise<Order> {
  const quote = await this.shipping.quote(cart.items, address);
  const payment = await this.payments.charge(cart.totalCents + quote.cents, token);
  await this.inventory.reserve(cart.items);
  return this.orders.create(cart, quote, payment.id);
}

Predict the output

A class wraps a vendor's image SDK. It has the same methods as the SDK but retries failed downloads three times. Which pattern is it?

Exerciseeasy

A media player facade

To play a podcast episode, a screen currently: asks AudioSession to activate for playback, downloads the file with Downloader unless FileCache has it, creates a Player with the file URL, seeks to the position saved in ProgressStore, starts playback, and updates NowPlayingInfo (the lock-screen controls). Design a PodcastPlayer facade: which methods does it expose, and what stays hidden?

›

Think about what a screen wants to say, not what the classes can do: “play this episode”, “pause”, “skip 30 seconds”. Everything about files, sessions and lock-screen info is detail.

SolutionTry it yourself first
final class PodcastPlayer {
    func play(_ episode: Episode) async throws {
        try audioSession.activateForPlayback()
        let file = try await cache.file(for: episode.id) ?? downloader.download(episode.url)
        player.load(file)
        player.seek(to: progress.position(for: episode.id))
        player.play()
        nowPlaying.update(episode, position: player.position)
    }
    func pause() { player.pause(); progress.save(player.position, for: player.episodeId) }
    func skip(seconds: Double) { player.seek(to: player.position + seconds) }
    // collaborators injected through init
}
class PodcastPlayer(/* collaborators injected */) {
    suspend fun play(episode: Episode) {
        audioSession.activateForPlayback()
        val file = cache.file(episode.id) ?: downloader.download(episode.url)
        player.load(file)
        player.seekTo(progress.position(episode.id))
        player.play()
        nowPlaying.update(episode, player.position)
    }
    fun pause() { player.pause(); progress.save(player.position, player.episodeId) }
    fun skip(seconds: Long) = player.seekTo(player.position + seconds)
}
public final class PodcastPlayer {
    public CompletableFuture<Void> play(Episode episode) {
        audioSession.activateForPlayback();
        return cache.file(episode.id())
                .map(CompletableFuture::completedFuture)
                .orElseGet(() -> downloader.download(episode.url()))
                .thenAccept(file -> {
                    player.load(file);
                    player.seekTo(progress.position(episode.id()));
                    player.play();
                    nowPlaying.update(episode, player.position());
                });
    }
    public void pause() { player.pause(); progress.save(player.position(), player.episodeId()); }
    public void skip(long seconds) { player.seekTo(player.position() + seconds); }
    // collaborators injected through the constructor
}
export class PodcastPlayer {
  async play(episode: Episode): Promise<void> {
    await this.audioSession.activateForPlayback();
    const file = (await this.cache.file(episode.id)) ?? (await this.downloader.download(episode.url));
    this.player.load(file);
    this.player.seek(this.progress.position(episode.id));
    this.player.play();
    this.nowPlaying.update(episode, this.player.position);
  }
  pause(): void { this.player.pause(); this.progress.save(this.player.position, this.player.episodeId); }
  skip(seconds: number): void { this.player.seek(this.player.position + seconds); }
  // collaborators injected through the constructor
}

The facade exposes intents (play, pause, skip). Sessions, caching, downloads, saved progress and lock-screen info are hidden, so the episode screen, the mini player and the car-play screen all behave the same way.

Facade, use case, service

The same shape goes by many names: an application service in layered architecture, a use case or interactor in Clean Architecture, a module API in modular apps. What they share: one entry point per user intent, orchestration without business rules, and dependencies on abstractions so the facade can be tested with fakes. A facade over a remote system (a gateway) also maps transport errors and DTOs, overlapping with Adapter; that’s fine, as long as one class doesn’t take on both a dozen intents and every translation.

  • One higher-level entry point over a subsystem: orchestrates and delegates, holds no domain rules; subsystem stays accessible.
  • Owns sequencing and failure handling: reversible steps first, irreversible ones (charges) late, compensation on failure.
  • vs Adapter (fit a different existing interface), Decorator (same interface, added behavior), Proxy (same interface, controlled access).
  • Appears as application services, use cases, module APIs, gateways.

Check this part

  1. What does a facade do?

  2. Can code still use the subsystem classes directly when there's a facade?

  3. In what order should a checkout facade charge the card and reserve stock?

A facade is one class with a few high-level methods that coordinates a subsystem of many classes. Callers use the facade; the subsystem classes stay available for the rare caller that needs more control. The facade owns the order of steps and what happens when one fails.

Part Not started

Keeping a facade thin

A reception desk works because it handles a few common requests. If it also cooked, cleaned rooms and did the accounting, it would be the slowest, busiest part of the hotel, and every change would go through it.

Facades drift the same way. AppFacade starts with placeOrder, then gets login, updateProfile, searchProducts, and fifty more, until every screen depends on it, every test needs it, and nobody dares to change it. That’s a god object, the opposite of what a facade is for.

Signs a facade has become a god object: dozens of methods spanning unrelated intents, many constructor dependencies, business rules creeping in (“if the user is premium, skip the fee”), and a merge conflict every sprint. Split by use case or bounded context (CheckoutFacade, AccountFacade), keep invariants in domain types, and let facades only sequence. Watch side effects too: a facade that calls several independent subsystems in a row couples their failures unless it isolates them.

final class CheckoutFacade {
    func placeOrder(_ cart: Cart, address: Address, token: String) async throws -> Order { /* … */ }
    func quote(_ cart: Cart, address: Address) async throws -> ShippingQuote { /* … */ }
}

final class AccountFacade {
    func signIn(email: String, password: String) async throws -> User { /* … */ }
    func signOut() async { /* … */ }
}
class CheckoutFacade(/* … */) {
    suspend fun placeOrder(cart: Cart, address: Address, token: String): Order = TODO()
    suspend fun quote(cart: Cart, address: Address): ShippingQuote = TODO()
}

class AccountFacade(/* … */) {
    suspend fun signIn(email: String, password: String): User = TODO()
    suspend fun signOut(): Unit = TODO()
}
public final class CheckoutFacade {
    public Order placeOrder(Cart cart, Address address, String token) { /* … */ }
    public ShippingQuote quote(Cart cart, Address address) { /* … */ }
}

public final class AccountFacade {
    public User signIn(String email, String password) { /* … */ }
    public void signOut() { /* … */ }
}
export class CheckoutFacade {
  async placeOrder(cart: Cart, address: Address, token: string): Promise<Order> { /* … */ }
  async quote(cart: Cart, address: Address): Promise<ShippingQuote> { /* … */ }
}

export class AccountFacade {
  async signIn(email: string, password: string): Promise<User> { /* … */ }
  async signOut(): Promise<void> { /* … */ }
}
  • One facade per area of the app, with a handful of methods each.
  • No business rules in the facade: “orders over 50 EUR ship free” belongs in the ShippingService or a pricing class, where it can be tested on its own.
  • If a screen needs something unusual, it can use the subsystem directly instead of adding the hundredth method to the facade.

Production puzzles

Spot the bugOne vendor down, all analytics gone

Tap the line with the bug.

final class AnalyticsFacade {
    func track(_ event: Event) throws {
        try firebase.log(event.name, event.params)
        try amplitude.track(event.name, event.params)
        try ourBackend.send(event)
    }
}
class AnalyticsFacade(/* … */) {
    fun track(event: Event) {
        firebase.log(event.name, event.params)
        amplitude.track(event.name, event.params)
        ourBackend.send(event)
    }
}
public final class AnalyticsFacade {
    public void track(Event event) {
        firebase.log(event.name(), event.params());
        amplitude.track(event.name(), event.params());
        ourBackend.send(event);
    }
}
export class AnalyticsFacade {
  track(event: AppEvent): void {
    this.firebase.log(event.name, event.params);
    this.amplitude.track(event.name, event.params);
    this.ourBackend.send(event);
  }
}

  • God-object smells: unrelated intents, many dependencies, business rules inside, constant merge conflicts. Split by use case or context.
  • Facades sequence; domain types hold invariants.
  • Fan-out to independent side effects (analytics, notifications) must isolate failures per destination; never fail the user action for telemetry.

Check this part

  1. A facade has grown to 80 methods used by every screen. What is it now?

  2. Where does the rule "orders over 50 EUR ship free" belong?

  3. A facade sends one event to three analytics vendors. What must it guarantee?

A facade that collects every method of every subsystem becomes a god object that everything depends on. Keep it focused on a few intents, split facades by use case, keep rules in the domain classes, and isolate independent side effects so one failing subsystem doesn't block the others.

Key takeaways

  • A facade gives a group of classes one simple entry point, like a hotel’s reception desk.
  • It owns the order of steps and undoes earlier steps when a later one fails.
  • The classes behind it stay usable; the facade is a convenience.
  • Keep facades small and focused; one for everything becomes a god object.
  • Orchestration without business rules; one facade per use case or context.
  • Irreversible steps last, with compensation; isolate independent side effects.
  • Facade simplifies, Adapter translates, Decorator adds behavior, Proxy controls access.
  • Facades are the natural seam for tests: one fake instead of a whole subsystem.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its cards are now in your Bites review ⚡
Esc

Type to search the whole site.