Facade

Facade

A hotel concierge is a Facade. You walk up and say "I need a restaurant reservation, a taxi, and theater tickets." Behind the scenes, the concierge calls the restaurant, the taxi company, and the box office. You interact with one person instead of three separate services, each with its own phone number, hold music, and booking protocol. The concierge doesn’t add new capabilities — they simplify access to existing ones.

The Facade pattern provides a simplified interface to a complex subsystem. The facade class holds references to subsystem components (inventory, payment, shipping, notification) and exposes high-level methods that coordinate them. The client calls OrderFacade.PlaceOrderAsync(order) instead of manually orchestrating five services in the right sequence with the right error handling. The subsystems remain fully accessible for clients that need fine-grained control — the facade is a convenience, not a prison.

flowchart LR
    Client -->|PlaceOrder| OrderFacade
    OrderFacade --> InventoryService
    OrderFacade --> PaymentService
    OrderFacade --> ShippingService
    OrderFacade --> NotificationService
    OrderFacade --> AnalyticsService
Facade vs Adapter

Facade creates a new simplified interface for your convenience — it's about reducing complexity. Adapter makes an existing incompatible interface fit a target interface — it's about compatibility. Facade is optional (you could call the subsystems directly); Adapter is required (the interfaces are incompatible without it).

Problem

CheckoutController orchestrates 5 services directly. The controller knows too much:

[ApiController]
public class CheckoutController(
    IInventoryService inventory,
    IPaymentService payment,
    IShippingService shipping,
    INotificationService notification,
    IAnalyticsService analytics,
    IOrderRepository orderRepository) : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> CheckoutAsync(CheckoutRequest request)
    {
        // ⚠️ Controller orchestrates 5 services — knows the entire checkout workflow
        var order = await orderRepository.CreateDraftAsync(request.CustomerId, request.Items);

        // ⚠️ Inventory check
        foreach (var item in order.Items)
        {
            var available = await inventory.CheckStockAsync(item.ProductId, item.Quantity);
            if (!available)
                return BadRequest($"Product {item.ProductId} is out of stock");
        }

        // ⚠️ Payment
        var paymentResult = await payment.ChargeAsync(order.Total, request.PaymentMethod);
        if (!paymentResult.Success)
            return BadRequest("Payment failed");

        // ⚠️ Reserve inventory after payment
        await inventory.ReserveAsync(order.Items);

        // ⚠️ Create shipping label
        var shipment = await shipping.CreateLabelAsync(order, request.ShippingAddress);

        // ⚠️ Notifications and analytics — controller shouldn't know about these
        await notification.SendOrderConfirmationAsync(order, shipment.TrackingNumber);
        await analytics.TrackOrderPlacedAsync(order);

        await orderRepository.ConfirmAsync(order.Id, paymentResult.TransactionId, shipment.TrackingNumber);
        return Ok(new { OrderId = order.Id, TrackingNumber = shipment.TrackingNumber });
    }
}

Here's what breaks when requirements change: adding fraud detection requires editing the controller. Every endpoint that places orders (web, mobile API, B2B API) duplicates this orchestration.

Solution

OrderFacade encapsulates the checkout workflow. The controller has one dependency:

public record CheckoutResult(Guid OrderId, string TrackingNumber, decimal Total);

public class OrderFacade(
    IInventoryService inventory,
    IPaymentService payment,
    IShippingService shipping,
    INotificationService notification,
    IAnalyticsService analytics,
    IOrderRepository orderRepository)
{
    // ✅ Checkout workflow in one place — all callers use the same orchestration
    public async Task<CheckoutResult> PlaceOrderAsync(
        Customer customer,
        IReadOnlyList<OrderItem> items,
        Address shippingAddress,
        PaymentMethod paymentMethod)
    {
        var order = await orderRepository.CreateDraftAsync(customer.Id, items);

        foreach (var item in order.Items)
        {
            if (!await inventory.CheckStockAsync(item.ProductId, item.Quantity))
                throw new OutOfStockException(item.ProductId);
        }

        var paymentResult = await payment.ChargeAsync(order.Total, paymentMethod);
        if (!paymentResult.Success)
            throw new PaymentFailedException(paymentResult.FailureReason);

        await inventory.ReserveAsync(order.Items);
        var shipment = await shipping.CreateLabelAsync(order, shippingAddress);

        await orderRepository.ConfirmAsync(order.Id, paymentResult.TransactionId, shipment.TrackingNumber);

        // ✅ Fire-and-forget side effects — controller doesn't need to know about these
        _ = Task.WhenAll(
            notification.SendOrderConfirmationAsync(order, shipment.TrackingNumber),
            analytics.TrackOrderPlacedAsync(order));

        return new CheckoutResult(order.Id, shipment.TrackingNumber, order.Total);
    }
}

// ✅ Controller has one dependency — knows nothing about the checkout workflow
[ApiController]
public class CheckoutController(OrderFacade orderFacade) : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> CheckoutAsync(CheckoutRequest request)
    {
        try
        {
            var result = await orderFacade.PlaceOrderAsync(
                request.Customer, request.Items, request.ShippingAddress, request.PaymentMethod);
            return Ok(result);
        }
        catch (OutOfStockException ex) { return BadRequest($"Out of stock: {ex.ProductId}"); }
        catch (PaymentFailedException ex) { return BadRequest($"Payment failed: {ex.Reason}"); }
    }
}

// DI registration
builder.Services.AddScoped<OrderFacade>();

Adding fraud detection now means editing OrderFacade.PlaceOrderAsync in one place — all callers (web, mobile, B2B) get the update automatically.

You Already Use This

File static class — a facade over FileStream, StreamReader, StreamWriter, and Path. File.ReadAllTextAsync("data.json") hides stream creation, buffering, encoding, and disposal. You could do it manually; File makes it one line.

HttpClient — a facade over HttpMessageHandler, HttpRequestMessage, HttpResponseMessage, connection pooling, and DNS resolution. client.GetStringAsync(url) hides the entire HTTP machinery.

DbContext (EF Core) — a facade over DbConnection, DbCommand, change tracking, identity map, and SQL generation. context.Orders.Where(o => o.Status == OrderStatus.Pending).ToListAsync() hides all of it.

WebApplication minimal APIs — a facade over IApplicationBuilder, IEndpointRouteBuilder, IServiceProvider, and the hosting infrastructure. app.MapGet("/orders", handler) hides the routing pipeline setup.

Questions

References


Whats next