Structural Beginner 7 min read

Facade Pattern in C#: One Service for Many Subsystems

Facade pattern in C# / .NET 10: collapse six injections into one application service that fans out, without leaking the inner classes back to the caller.

Table of contents
  1. What problem does the Facade pattern solve in C#?
  2. How does the Facade structure look in .NET 10?
  3. When should the Facade be its own interface vs just a class?
  4. When does the Facade pattern misfire?
  5. How does Facade compare to Adapter and Mediator?
  6. What does a real .NET 10 example look like?
  7. Where should you read next in this series?

The CheckoutController started with one job and one dependency. Six months later, its constructor takes:

public CheckoutController(
    ICartRepository       carts,
    IInventoryService     inventory,
    ITaxCalculator        tax,
    IPaymentProcessor     payment,
    IShippingService      shipping,
    INotificationService  notifications,
    IOrderRepository      orders)
{ /* ... */ }

Seven injections. The single Checkout() action method is sixty lines long. It loads the cart, validates inventory, computes tax, charges the card, books shipping, persists the order, and sends a confirmation email. None of those steps is hard. Together they are a maze, and the maze is the controller's problem now.

The Facade pattern is the answer. Pull the orchestration into a single application service — let us call it ICheckoutService — and let the controller inject one thing. Inside the service, the seven collaborators do their work. Outside, the seven collaborators stop existing as far as callers are concerned. This is the most quietly useful structural pattern in .NET: most well-organised codebases use it without naming it.

What problem does the Facade pattern solve in C#?

The pattern earns its place when a subsystem of cooperating classes has grown beyond what callers should reasonably coordinate. Three concrete shapes:

  1. Use-case orchestration. Checkout, signup, refund — each is a sequence of calls into different subsystems. The Facade gives each use case a name.
  2. Library entry point. A library exposes ten classes for advanced users but ninety percent of callers want one method. The Facade is the friendly door that hides the rooms behind it.
  3. Legacy refuge. A legacy system has thirty interlocking modules. New code wraps the four it actually uses in a Facade so the rest never bleed in.

What is not a Facade problem: "I want one foreign type to fit one local interface" — that is Adapter. "I want to add behaviour around an existing call" — that is Decorator. Facade is specifically about reducing the surface area the caller has to know.

How does the Facade structure look in .NET 10?

A regular class that implements an interface and accepts the inner collaborators in its constructor. Each public method is one use case and orchestrates two or more inner calls:

public sealed record CheckoutRequest(string CartId, string PaymentMethod);
public sealed record CheckoutResult(string OrderId, decimal TotalCharged, string ConfirmationEmail);

public interface ICheckoutService
{
    Task<CheckoutResult> CheckoutAsync(CheckoutRequest req, CancellationToken ct);
}

public sealed class CheckoutService : ICheckoutService
{
    private readonly ICartRepository      _carts;
    private readonly IInventoryService    _inventory;
    private readonly ITaxCalculator       _tax;
    private readonly IPaymentProcessor    _payment;
    private readonly IShippingService     _shipping;
    private readonly INotificationService _notifications;
    private readonly IOrderRepository     _orders;
    private readonly ILogger<CheckoutService> _log;

    public CheckoutService(/* all of the above */) { /* ... */ }

    public async Task<CheckoutResult> CheckoutAsync(CheckoutRequest req, CancellationToken ct)
    {
        var cart   = await _carts.GetAsync(req.CartId, ct);
        await _inventory.ReserveAsync(cart.Items, ct);
        var totals = _tax.Compute(cart);
        var charge = await _payment.PayAsync(totals.GrandTotal, cart.CustomerId, req.CartId, ct);
        if (charge.Status != "succeeded")
            throw new CheckoutFailedException(charge.Error);

        var order = await _orders.CreateAsync(cart, totals, charge.TransactionId!, ct);
        await _shipping.BookAsync(order, ct);
        await _notifications.SendConfirmationAsync(order, ct);

        return new CheckoutResult(order.Id, totals.GrandTotal, order.Customer.Email);
    }
}

The controller now reads:

[ApiController, Route("checkout")]
public sealed class CheckoutController : ControllerBase
{
    private readonly ICheckoutService _checkout;
    public CheckoutController(ICheckoutService checkout) => _checkout = checkout;

    [HttpPost]
    public async Task<ActionResult<CheckoutResult>> Post(CheckoutRequest req, CancellationToken ct)
        => Ok(await _checkout.CheckoutAsync(req, ct));
}

The structural picture:

flowchart LR
    Caller[CheckoutController]
    Caller --> Facade[ICheckoutService]
    Facade --> Cart[ICartRepository]
    Facade --> Inv[IInventoryService]
    Facade --> Tax[ITaxCalculator]
    Facade --> Pay[IPaymentProcessor]
    Facade --> Ship[IShippingService]
    Facade --> Email[INotificationService]
    Facade --> Order[IOrderRepository]

One arrow from caller to facade; six arrows from facade to inner collaborators. The simplification lives at the boundary the caller sees.

When should the Facade be its own interface vs just a class?

A Facade is just a class with collaborators; whether it has its own interface is a separate decision:

In modern .NET 10, most application services have an interface because tests use it. Default to having one; drop it when you can demonstrate it has never been useful.

When does the Facade pattern misfire?

Three real-world traps:

How does Facade compare to Adapter and Mediator?

Pattern Number of inner objects Changes interface? Adds behaviour?
Facade Many Yes (many → one) No (orchestration only)
Adapter One Yes (mismatch → match) No
Mediator Many (peers, not subordinates) Yes (peers → bus) No

The cleanest split: Adapter is one-to-one with shape changes; Facade is many-to-one with orchestration; Mediator is many-to-many through a hub. A Facade owns the relationships and dictates the order of calls; Mediator just brokers messages between equals.

What does a real .NET 10 example look like?

A complete shape: the same checkout, with transactional co-ordination and a DTO boundary. The Facade is the only public type; the inner classes are registered separately and stay internal to their assemblies.

// Public contract (what callers see) ------------------------------
public sealed record CheckoutRequest(string CartId, string PaymentMethod);
public sealed record CheckoutResult(string OrderId, decimal TotalCharged, string ConfirmationEmail);

public interface ICheckoutService
{
    Task<CheckoutResult> CheckoutAsync(CheckoutRequest req, CancellationToken ct);
}

// Implementation (internal) ---------------------------------------
internal sealed class CheckoutService : ICheckoutService
{
    private readonly ICartRepository      _carts;
    private readonly IInventoryService    _inventory;
    private readonly ITaxCalculator       _tax;
    private readonly IPaymentProcessor    _payment;
    private readonly IShippingService     _shipping;
    private readonly INotificationService _notifications;
    private readonly IOrderRepository     _orders;
    private readonly IUnitOfWork          _uow;
    private readonly ILogger<CheckoutService> _log;

    public CheckoutService(
        ICartRepository carts, IInventoryService inventory, ITaxCalculator tax,
        IPaymentProcessor payment, IShippingService shipping,
        INotificationService notifications, IOrderRepository orders,
        IUnitOfWork uow, ILogger<CheckoutService> log)
    {
        _carts = carts; _inventory = inventory; _tax = tax;
        _payment = payment; _shipping = shipping; _notifications = notifications;
        _orders = orders; _uow = uow; _log = log;
    }

    public async Task<CheckoutResult> CheckoutAsync(CheckoutRequest req, CancellationToken ct)
    {
        await using var tx = await _uow.BeginAsync(ct);

        var cart = await _carts.GetAsync(req.CartId, ct)
            ?? throw new NotFoundException($"Cart {req.CartId} not found");

        await _inventory.ReserveAsync(cart.Items, ct);

        var totals = _tax.Compute(cart);

        var charge = await _payment.PayAsync(totals.GrandTotal, cart.CustomerId, req.CartId, ct);
        if (charge.Status != "succeeded")
            throw new CheckoutFailedException(charge.Error ?? "Unknown");

        var order = await _orders.CreateAsync(cart, totals, charge.TransactionId!, ct);
        await _shipping.BookAsync(order, ct);

        await tx.CommitAsync(ct);

        // Notifications happen after commit — failing to send an email must not
        // roll back a successful order.
        await _notifications.SendConfirmationAsync(order, ct);

        _log.LogInformation("Order {OrderId} created for cart {CartId}", order.Id, req.CartId);
        return new CheckoutResult(order.Id, totals.GrandTotal, order.Customer.Email);
    }
}

// Composition root ------------------------------------------------
builder.Services.AddScoped<ICheckoutService, CheckoutService>();
// inner collaborators registered as their own scoped/singleton services

// Caller (the controller from earlier) ----------------------------
[ApiController, Route("checkout")]
public sealed class CheckoutController : ControllerBase
{
    private readonly ICheckoutService _checkout;
    public CheckoutController(ICheckoutService checkout) => _checkout = checkout;

    [HttpPost]
    public async Task<ActionResult<CheckoutResult>> Post(CheckoutRequest req, CancellationToken ct)
        => Ok(await _checkout.CheckoutAsync(req, ct));
}

The decision worth highlighting: notifications happen after commit. The Facade is the only place that knows that decision exists, because the Facade is the only place that orchestrates both. Hiding that knowledge inside the Facade is the entire point.

A pragmatic note: Facade is the most under-named pattern in real codebases. Most application services are Facades; most teams just call them "the X service". That is fine — the pattern earns its keep regardless of what you call it. The benefit of knowing the name is recognising the failure modes (god classes, leaking returns, one-to-one passthroughs) earlier.

Frequently asked questions

What is the difference between Facade and Adapter?
Adapter is one-to-one — it makes one foreign class fit one local interface. Facade is many-to-one — it gathers several existing classes behind a new, simpler interface that the caller wanted in the first place. Adapter is for mismatched shapes; Facade is for too many shapes the caller does not want to coordinate.
Is an application service the same thing as a Facade?
Practically yes. Domain-driven design's application service sits between controllers and the domain, orchestrates a few aggregates, and exposes one method per use case. That is exactly the Facade pattern. The framing differs — application services emphasise transaction boundaries and use cases — but the structural shape is identical.
Can a Facade leak its internals through return types?
It can, and that is the most common way the pattern fails. If CheckoutFacade.Checkout() returns the same Order aggregate the inner repository handed it, every caller now depends on the inner type. Convert to a small DTO at the Facade boundary so callers depend only on data they actually need.
When does adding a Facade make code harder to follow?
When the Facade is a one-to-one wrapper that adds nothing — same parameters in, same result out, just more files. Or when the Facade has many unrelated methods because it has become a service locator dressed up as a service. A Facade earns its keep when each method orchestrates two or more inner calls; below that bar, inject the inner class directly.