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.
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
// 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 layerFacade Implementation
// 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
// 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 callersFacade 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 otherInterview 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?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.