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.