Learnixo

.NET & C# Development · Lesson 39 of 229

Facade — Simple Interface to Complex Subsystems

Facade — Simple Interface to Complex Subsystems

Facade provides a simplified interface to a complex set of classes, libraries, or subsystems. Callers use the facade; the complexity is hidden behind it.


The Problem

C#
// Without Facade: caller must coordinate 6 services
public class CheckoutController
{
    public async Task<IActionResult> Checkout(CartDto cart)
    {
        // Caller knows too much about the internal steps
        await _inventory.ReserveAsync(cart.Items);
        var order   = await _orders.CreateAsync(cart);
        var payment = await _payments.ChargeAsync(cart.Card, order.Total);
        if (!payment.Success)
        {
            await _inventory.ReleaseAsync(cart.Items);
            await _orders.CancelAsync(order.Id);
            return BadRequest(payment.Error);
        }
        await _emails.SendConfirmationAsync(order);
        await _audit.LogAsync("checkout", order.Id);
        return Ok(order.Id);
    }
}
// Controller now owns the checkout orchestration — wrong layer

Facade Implementation

C#
// Facade — hides the multi-step orchestration
public class CheckoutFacade(
    IInventoryService  inventory,
    IOrderRepository   orders,
    IPaymentGateway    payments,
    IEmailService      emails,
    IAuditService      audit)
{
    public async Task<CheckoutResult> ProcessAsync(CartDto cart)
    {
        // Reserve inventory first (can be released on failure)
        await inventory.ReserveAsync(cart.Items);

        Order order;
        try
        {
            order = await orders.CreateAsync(cart);
            var payment = await payments.ChargeAsync(cart.Card, order.Total);

            if (!payment.Success)
            {
                await inventory.ReleaseAsync(cart.Items);
                await orders.CancelAsync(order.Id);
                return CheckoutResult.Failed(payment.Error);
            }
        }
        catch
        {
            await inventory.ReleaseAsync(cart.Items);
            throw;
        }

        // Fire-and-forget non-critical steps
        await emails.SendConfirmationAsync(order);
        await audit.LogAsync("checkout_completed", order.Id);

        return CheckoutResult.Success(order.Id);
    }
}

// Simple result type
public record CheckoutResult(bool IsSuccess, int? OrderId, string? Error)
{
    public static CheckoutResult Success(int id)          => new(true, id, null);
    public static CheckoutResult Failed(string error)     => new(false, null, error);
}

// Controller is now trivially simple
public class CheckoutController(CheckoutFacade checkout)
{
    public async Task<IActionResult> Checkout(CartDto cart)
    {
        var result = await checkout.ProcessAsync(cart);
        return result.IsSuccess ? Ok(result.OrderId) : BadRequest(result.Error);
    }
}

Layered Facades

C#
// Facade for external integrations — wraps multiple APIs
public class NotificationFacade(
    IEmailService email,
    ISmsService sms,
    IPushService push)
{
    public async Task NotifyOrderShippedAsync(Order order)
    {
        var tasks = new List<Task>();

        if (order.Customer.EmailOptIn)
            tasks.Add(email.SendShipmentAsync(order));

        if (order.Customer.SmsOptIn)
            tasks.Add(sms.SendTrackingAsync(order));

        if (order.Customer.PushOptIn)
            tasks.Add(push.SendAsync(order.Customer.DeviceToken, "Order shipped!"));

        await Task.WhenAll(tasks);
    }
}

// Report generation facade
public class ReportingFacade(
    IDataExtractor extractor,
    IReportFormatter formatter,
    IReportStorage storage)
{
    public async Task<string> GenerateAndStoreAsync(ReportRequest request)
    {
        var data      = await extractor.ExtractAsync(request.Query);
        var report    = await formatter.FormatAsync(data, request.Format);
        var location  = await storage.StoreAsync(report, request.Name);
        return location;
    }
}

When to Use

✓ Controller or endpoint delegates to a complex multi-step flow
✓ You have a large library that's hard to use correctly
✓ Layering — Application layer facades hide Infrastructure details
✓ Testing — Facade is easy to mock in unit tests for the caller

✗ When you only have one simple service — no need for a facade
✗ When the "simplification" removes necessary control from callers

Facade vs Mediator

Facade:   one-way simplification — the facade calls subsystems; subsystems don't know the facade
Mediator: bi-directional coordination — subsystems communicate via the mediator

Use Facade for: hiding complexity from the outside
Use Mediator for: decoupling subsystems from each other

Interview Answer

"The Facade pattern provides a single simple entry point to a complex subsystem. In ASP.NET Core, the classic use is an Application Service or Use Case class that sits between the controller and multiple infrastructure services — the controller calls one method, the facade orchestrates inventory, payments, email, and audit internally. Benefits: controllers stay thin and testable, the orchestration logic lives in one place, and the controller doesn't need to know about or depend on the subsystem's internal complexity. Key distinction from Mediator: a Facade is one-way (callers use it, subsystems don't know it exists); a Mediator is a hub where subsystems communicate with each other through it. In Clean Architecture, the Application layer use case handlers are effectively facades."