Learnixo
Back to blog
Backend Systemsbeginner

Facade — Simple Interface to Complex Subsystems

The Facade pattern in C#: provide a simplified, unified interface to a complex subsystem. Practical examples with order processing, email + SMS + audit combined into one service call.

Asma Hafeez KhanMay 24, 20263 min read
csharpdesign-patternsfacadestructuraldotnet
Share:š•

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."

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.