← Back to all posts
dotnetdesign-patternsaspnet-corearchitecturecsharp

Facade Pattern in .NET

Facade Pattern in .NET

The Problem

As .NET applications grow, business workflows often depend on multiple services:

  • Repositories
  • External APIs
  • Caching
  • Logging
  • Messaging
  • Validation

Without a clear orchestration layer, callers (controllers, endpoints, background jobs) start coordinating everything directly:

public async Task<IResult> PlaceOrder(
    PlaceOrderRequest request,
    IInventoryService inventory,
    IPaymentGateway payment,
    IShippingService shipping,
    IOrderRepository orders,
    INotificationService notifications)
{
    var isAvailable = await inventory.ReserveAsync(request.Items);
    if (!isAvailable) return Results.BadRequest("Insufficient stock");

    var paymentResult = await payment.ChargeAsync(request.CustomerId, request.Total);
    if (!paymentResult.Success) return Results.BadRequest("Payment failed");

    var order = await orders.CreateAsync(request, paymentResult.TransactionId);
    await shipping.ScheduleAsync(order.Id, request.Address);
    await notifications.SendOrderConfirmationAsync(order.Id, request.CustomerId);

    return Results.Ok(order.Id);
}

This creates common issues:

  • Controllers become orchestration hubs
  • Business flow is duplicated across entry points
  • Error handling is inconsistent
  • Testing requires mocking too many dependencies

The Solution: Facade Pattern

The Facade Pattern provides a single, simplified interface in front of a complex subsystem.

Instead of every caller knowing every dependency and step, they call one cohesive API.

Think of the facade as:

  • A workflow orchestrator
  • A boundary around complexity
  • A stable contract for the application layer

When Facade Is a Great Fit

Use Facade when:

  • One business action spans multiple dependencies
  • You want a cleaner API for controllers/endpoints
  • You need to standardize sequencing and error handling
  • You want to hide subsystem details from callers

Example: Order Processing Facade

Step 1: Define result model

public sealed record PlaceOrderResult(
    bool Success,
    string? Error,
    int? OrderId = null);

Step 2: Define facade interface

public interface IOrderFacade
{
    Task<PlaceOrderResult> PlaceOrderAsync(PlaceOrderRequest request, CancellationToken ct = default);
}

Step 3: Implement facade

public sealed class OrderFacade : IOrderFacade
{
    private readonly IInventoryService _inventory;
    private readonly IPaymentGateway _payment;
    private readonly IShippingService _shipping;
    private readonly IOrderRepository _orders;
    private readonly INotificationService _notifications;
    private readonly ILogger<OrderFacade> _logger;

    public OrderFacade(
        IInventoryService inventory,
        IPaymentGateway payment,
        IShippingService shipping,
        IOrderRepository orders,
        INotificationService notifications,
        ILogger<OrderFacade> logger)
    {
        _inventory = inventory;
        _payment = payment;
        _shipping = shipping;
        _orders = orders;
        _notifications = notifications;
        _logger = logger;
    }

    public async Task<PlaceOrderResult> PlaceOrderAsync(PlaceOrderRequest request, CancellationToken ct = default)
    {
        var available = await _inventory.ReserveAsync(request.Items, ct);
        if (!available)
        {
            return new PlaceOrderResult(false, "Insufficient stock");
        }

        var charge = await _payment.ChargeAsync(request.CustomerId, request.Total, ct);
        if (!charge.Success)
        {
            return new PlaceOrderResult(false, "Payment failed");
        }

        var order = await _orders.CreateAsync(request, charge.TransactionId!, ct);
        await _shipping.ScheduleAsync(order.Id, request.Address, ct);
        await _notifications.SendOrderConfirmationAsync(order.Id, request.CustomerId, ct);

        _logger.LogInformation("Order {OrderId} placed for customer {CustomerId}", order.Id, request.CustomerId);

        return new PlaceOrderResult(true, null, order.Id);
    }
}

Step 4: Use from endpoint

app.MapPost("/orders", async (
    PlaceOrderRequest request,
    IOrderFacade facade,
    CancellationToken ct) =>
{
    var result = await facade.PlaceOrderAsync(request, ct);
    return result.Success
        ? Results.Ok(new { orderId = result.OrderId })
        : Results.BadRequest(new { error = result.Error });
});

Now the endpoint stays thin, and the workflow logic is centralized.

Facade vs Other Patterns

Facade vs Mediator

  • Facade: one class exposes a simplified API over a specific subsystem/workflow.
  • Mediator: routes many request/command types to handlers; broader dispatch pattern.

If your need is “hide complexity behind one clear operation,” Facade is often simpler.

Facade vs Service Layer

A service layer can include multiple business services. A facade is often a focused entry point that coordinates across services.

In practice, facades can live inside your application/service layer.

Error Handling and Transactions

A facade is a great place for consistent error mapping and compensation behavior.

For example:

  • If payment succeeds but shipping fails, trigger a refund workflow
  • If reservation succeeds but order creation fails, release inventory

You can encapsulate this once in the facade instead of repeating it in every caller.

DI Registration

builder.Services.AddScoped<IOrderFacade, OrderFacade>();

Simple registration, single abstraction for callers.

Testing the Facade

Facade classes are straightforward to unit test because they encapsulate one workflow.

[Fact]
public async Task PlaceOrderAsync_WhenInventoryIsMissing_ReturnsFailure()
{
    var inventory = new Mock<IInventoryService>();
    inventory.Setup(x => x.ReserveAsync(It.IsAny<IReadOnlyList<OrderItem>>(), It.IsAny<CancellationToken>()))
             .ReturnsAsync(false);

    var facade = new OrderFacade(
        inventory.Object,
        Mock.Of<IPaymentGateway>(),
        Mock.Of<IShippingService>(),
        Mock.Of<IOrderRepository>(),
        Mock.Of<INotificationService>(),
        Mock.Of<ILogger<OrderFacade>>());

    var result = await facade.PlaceOrderAsync(new PlaceOrderRequest());

    Assert.False(result.Success);
    Assert.Equal("Insufficient stock", result.Error);
}

You test behavior at the workflow boundary instead of testing orchestration indirectly through controllers.

Common Mistakes

  • Turning facade into a giant “god class” with unrelated workflows
  • Putting low-level infrastructure details in callers instead of inside facade
  • Returning dozens of primitive values instead of clear result models
  • Skipping cancellation token propagation

Keep each facade cohesive and business-focused.

Summary

The Facade Pattern helps you build cleaner .NET applications by introducing a simple API over complex orchestration.

Key benefits:

  • Thin controllers/endpoints
  • Centralized workflow logic
  • Better consistency for error handling
  • Easier unit testing

If you notice callers coordinating many services for one use case, that is usually a strong signal to introduce a facade.