Complex subsystems accumulate classes, configuration steps, and coordination logic that callers shouldn't need to understand. The facade pattern wraps that complexity behind a simplified interface, but a poorly designed facade introduces its own problems. Facades balloon into god objects. They hide too much or too little. They become untestable monoliths that change every time any subsystem shifts. Facade pattern best practices in C# address these pitfalls directly -- they guide you toward facades that simplify without hiding critical behavior, stay testable through dependency injection, and remain maintainable as the subsystem behind them evolves.
This guide covers the practical facade pattern decisions that determine whether your simplified interface stays clean or degrades into an unmanageable dumping ground. We'll work through avoiding god facades, applying interface segregation, honoring single responsibility, wiring facades with dependency injection, testing facade classes, naming conventions, knowing when to split a facade, and handling async facade methods. Every section includes focused C# examples comparing well-structured facade pattern approaches against implementations heading for trouble.
Avoid God Facades
The most common facade pattern mistake is letting a single facade class grow until it orchestrates every operation in your subsystem. This creates a god object -- a class that knows too much, does too much, and changes for too many reasons. A facade should simplify access to a cohesive group of operations, not become a centralized control panel for your entire application layer.
Here's a facade that has grown beyond its useful scope:
// Avoid: god facade covering unrelated concerns
public class ECommerceService
{
private readonly OrderProcessor _orders;
private readonly PaymentGateway _payments;
private readonly InventoryManager _inventory;
private readonly ShippingCalculator _shipping;
private readonly EmailNotifier _email;
private readonly ReportGenerator _reports;
private readonly UserManager _users;
private readonly ReviewService _reviews;
public ECommerceService(
OrderProcessor orders,
PaymentGateway payments,
InventoryManager inventory,
ShippingCalculator shipping,
EmailNotifier email,
ReportGenerator reports,
UserManager users,
ReviewService reviews)
{
_orders = orders;
_payments = payments;
_inventory = inventory;
_shipping = shipping;
_email = email;
_reports = reports;
_users = users;
_reviews = reviews;
}
public void PlaceOrder(OrderRequest request) { /* ... */ }
public void ProcessRefund(string orderId) { /* ... */ }
public void GenerateSalesReport(DateRange range) { /* ... */ }
public void RegisterUser(UserRegistration reg) { /* ... */ }
public void SubmitReview(ReviewData data) { /* ... */ }
public void UpdateInventory(string sku, int qty) { /* ... */ }
}
This facade handles orders, refunds, reports, user registration, reviews, and inventory updates. Those are distinct domain concerns that change for different reasons and serve different consumers. When the reporting requirements change, this class changes. When order processing evolves, this class changes again. Eight constructor dependencies signal that the facade is doing too much.
Split the god facade into focused facades, each covering a cohesive set of operations:
// Prefer: focused facade per domain area
public sealed class OrderFacade
{
private readonly OrderProcessor _orders;
private readonly PaymentGateway _payments;
private readonly InventoryManager _inventory;
private readonly EmailNotifier _email;
public OrderFacade(
OrderProcessor orders,
PaymentGateway payments,
InventoryManager inventory,
EmailNotifier email)
{
_orders = orders;
_payments = payments;
_inventory = inventory;
_email = email;
}
public OrderResult PlaceOrder(OrderRequest request)
{
_inventory.Reserve(request.Items);
var payment = _payments.Charge(request.PaymentInfo);
var order = _orders.Create(request, payment.TransactionId);
_email.SendOrderConfirmation(order);
return new OrderResult(order.Id, payment.TransactionId);
}
}
Each facade pattern class coordinates a bounded set of subsystem components around a single domain concept. Consumers only depend on the facade that covers their use case, and changes to unrelated features don't ripple through a shared class. This approach parallels how the strategy design pattern isolates variation behind focused interfaces -- different concerns get different entry points.
Apply Interface Segregation to Facade Contracts
When you define an interface for your facade pattern, keep it narrow. A facade interface with fifteen methods forces every consumer to depend on operations they don't use. This makes mocking painful in tests and couples unrelated parts of your application to a shared contract.
Consider an over-broad interface:
// Avoid: fat interface covering all facade operations
public interface IOrderManagement
{
OrderResult PlaceOrder(OrderRequest request);
void CancelOrder(string orderId);
RefundResult ProcessRefund(string orderId);
OrderStatus GetOrderStatus(string orderId);
IReadOnlyList<Order> GetOrderHistory(string customerId);
ShippingEstimate EstimateShipping(ShippingRequest request);
void UpdateShippingAddress(string orderId, Address address);
}
A controller that only needs to place orders depends on the entire seven-method interface. A reporting service that only reads order history pulls in methods for cancellation, refunds, and shipping. The facade pattern contract is too broad.
Split the interface based on consumer needs:
// Prefer: segregated interfaces for distinct consumer needs
public interface IOrderPlacement
{
OrderResult PlaceOrder(OrderRequest request);
}
public interface IOrderQueries
{
OrderStatus GetOrderStatus(string orderId);
IReadOnlyList<Order> GetOrderHistory(string customerId);
}
public interface IOrderCancellation
{
void CancelOrder(string orderId);
RefundResult ProcessRefund(string orderId);
}
Your facade pattern implementation can implement multiple segregated interfaces if the operations share subsystem dependencies:
public sealed class OrderFacade :
IOrderPlacement,
IOrderQueries,
IOrderCancellation
{
private readonly OrderProcessor _orders;
private readonly PaymentGateway _payments;
private readonly InventoryManager _inventory;
public OrderFacade(
OrderProcessor orders,
PaymentGateway payments,
InventoryManager inventory)
{
_orders = orders;
_payments = payments;
_inventory = inventory;
}
public OrderResult PlaceOrder(OrderRequest request)
{
_inventory.Reserve(request.Items);
var payment = _payments.Charge(request.PaymentInfo);
return new OrderResult(
_orders.Create(request, payment.TransactionId).Id,
payment.TransactionId);
}
public OrderStatus GetOrderStatus(string orderId)
=> _orders.GetStatus(orderId);
public IReadOnlyList<Order> GetOrderHistory(string customerId)
=> _orders.GetByCustomer(customerId);
public void CancelOrder(string orderId)
{
_orders.Cancel(orderId);
_inventory.Release(orderId);
}
public RefundResult ProcessRefund(string orderId)
{
var order = _orders.Get(orderId);
return _payments.Refund(order.TransactionId, order.Total);
}
}
Each consumer depends only on the slice of the facade pattern that it needs. A checkout controller takes IOrderPlacement. A dashboard service takes IOrderQueries. Tests mock only the methods the test subject actually calls. This segregation principle applies broadly across structural patterns -- the same thinking guides how the adapter pattern defines narrow translation interfaces between incompatible types.
Honor Single Responsibility in Facade Methods
A facade pattern method should orchestrate subsystem calls for a single use case. It coordinates -- it doesn't implement business logic. The moment your facade method starts validating inputs, computing business rules, or transforming data, it's absorbing responsibilities that belong in the subsystem classes or dedicated services.
Here's a facade method that does too much internally:
// Avoid: business logic inside the facade method
public OrderResult PlaceOrder(OrderRequest request)
{
// Validation logic belongs in a validator
if (request.Items == null || request.Items.Count == 0)
{
throw new ArgumentException("Order must have items");
}
// Discount calculation belongs in a pricing service
decimal total = 0;
foreach (var item in request.Items)
{
var price = item.UnitPrice * item.Quantity;
if (item.Quantity > 10)
{
price *= 0.9m; // 10% bulk discount
}
total += price;
}
// Tax calculation belongs in a tax service
var tax = total * 0.08m;
total += tax;
var payment = _payments.Charge(total, request.PaymentToken);
var order = _orders.Create(request, payment.TransactionId);
return new OrderResult(order.Id, payment.TransactionId);
}
The facade is computing discounts, calculating taxes, and validating inputs. Those are business rules, not coordination logic. Push them into the subsystem where they belong:
// Prefer: facade orchestrates, subsystems implement logic
public OrderResult PlaceOrder(OrderRequest request)
{
_validator.Validate(request);
var pricedItems = _pricingEngine.CalculatePrices(request.Items);
var total = _taxCalculator.ApplyTax(pricedItems.Subtotal);
var payment = _payments.Charge(total, request.PaymentToken);
var order = _orders.Create(request, payment.TransactionId);
return new OrderResult(order.Id, payment.TransactionId);
}
The facade pattern method reads like a script: validate, price, tax, charge, create. Each step delegates to a subsystem component that owns that responsibility. If discount rules change, you modify the pricing engine. If tax logic changes, you modify the tax calculator. The facade itself only changes when the orchestration sequence changes -- when steps are added, removed, or reordered. This clear separation is the same principle that drives the command design pattern, where each discrete operation is encapsulated independently from the code that triggers it.
Wire Facades with Dependency Injection
Facade pattern classes depend on subsystem components, and those dependencies should arrive through the constructor. Hard-coding new calls inside a facade couples it to specific implementations and makes it impossible to test in isolation. Constructor injection through a dependency injection container keeps facade pattern classes loosely coupled and swappable.
Here's a facade that creates its own dependencies:
// Avoid: facade creating its own dependencies
public class NotificationFacade
{
private readonly SmtpEmailSender _email;
private readonly TwilioSmsSender _sms;
private readonly FirebasePushSender _push;
public NotificationFacade()
{
_email = new SmtpEmailSender("smtp.example.com", 587);
_sms = new TwilioSmsSender("account-sid", "auth-token");
_push = new FirebasePushSender("server-key");
}
public void NotifyUser(string userId, string message)
{
_email.Send(userId, message);
_sms.Send(userId, message);
_push.Send(userId, message);
}
}
This class is locked to SMTP, Twilio, and Firebase. You can't test it without live credentials. You can't swap a notification channel without modifying the class. Apply inversion of control and inject abstractions instead:
// Prefer: dependencies injected through constructor
public sealed class NotificationFacade : INotificationFacade
{
private readonly IEmailSender _email;
private readonly ISmsSender _sms;
private readonly IPushNotificationSender _push;
public NotificationFacade(
IEmailSender email,
ISmsSender sms,
IPushNotificationSender push)
{
_email = email ?? throw new ArgumentNullException(nameof(email));
_sms = sms ?? throw new ArgumentNullException(nameof(sms));
_push = push ?? throw new ArgumentNullException(nameof(push));
}
public void NotifyUser(string userId, string message)
{
_email.Send(userId, message);
_sms.Send(userId, message);
_push.Send(userId, message);
}
}
Register the facade and its dependencies in the composition root:
services.AddScoped<IEmailSender, SmtpEmailSender>();
services.AddScoped<ISmsSender, TwilioSmsSender>();
services.AddScoped<IPushNotificationSender, FirebasePushSender>();
services.AddScoped<INotificationFacade, NotificationFacade>();
The container resolves the full dependency graph. Swapping SMS providers means changing one registration line. Testing the facade means passing mock implementations through the constructor. The facade pattern benefits enormously from DI because facades tend to have several dependencies by design -- they coordinate multiple subsystem components, and injection keeps that coordination flexible.
Test Facade Classes Effectively
Testing facade pattern classes verifies the orchestration logic -- the order of subsystem calls, the data passed between them, and the result returned to the caller. You're not testing each subsystem in isolation here. You're testing that the facade wires them together correctly.
Start with a facade that has injectable dependencies:
public sealed class CheckoutFacade : ICheckoutFacade
{
private readonly IInventoryService _inventory;
private readonly IPaymentService _payment;
private readonly IOrderService _orders;
public CheckoutFacade(
IInventoryService inventory,
IPaymentService payment,
IOrderService orders)
{
_inventory = inventory;
_payment = payment;
_orders = orders;
}
public CheckoutResult Checkout(CheckoutRequest request)
{
if (!_inventory.AreItemsAvailable(request.Items))
{
return CheckoutResult.OutOfStock();
}
_inventory.Reserve(request.Items);
var charge = _payment.Charge(
request.Total,
request.PaymentToken);
var order = _orders.Create(request, charge.TransactionId);
return CheckoutResult.Success(order.Id);
}
}
The test verifies the orchestration sequence and the happy path result:
[Fact]
public void Checkout_WhenItemsAvailable_CreatesOrderAndCharges()
{
var inventory = new Mock<IInventoryService>();
inventory.Setup(i => i.AreItemsAvailable(It.IsAny<List<Item>>()))
.Returns(true);
var payment = new Mock<IPaymentService>();
payment.Setup(p => p.Charge(It.IsAny<decimal>(), It.IsAny<string>()))
.Returns(new ChargeResult("txn-123"));
var orders = new Mock<IOrderService>();
orders.Setup(o => o.Create(
It.IsAny<CheckoutRequest>(), "txn-123"))
.Returns(new Order { Id = "order-456" });
var facade = new CheckoutFacade(
inventory.Object,
payment.Object,
orders.Object);
var result = facade.Checkout(new CheckoutRequest
{
Items = new List<Item> { new("SKU-1", 2) },
Total = 59.99m,
PaymentToken = "tok_test"
});
Assert.True(result.IsSuccess);
Assert.Equal("order-456", result.OrderId);
inventory.Verify(i => i.Reserve(It.IsAny<List<Item>>()), Times.Once);
}
Test the failure path separately to ensure the facade handles subsystem failures correctly:
[Fact]
public void Checkout_WhenItemsUnavailable_ReturnsOutOfStock()
{
var inventory = new Mock<IInventoryService>();
inventory.Setup(i => i.AreItemsAvailable(It.IsAny<List<Item>>()))
.Returns(false);
var payment = new Mock<IPaymentService>();
var orders = new Mock<IOrderService>();
var facade = new CheckoutFacade(
inventory.Object,
payment.Object,
orders.Object);
var result = facade.Checkout(new CheckoutRequest
{
Items = new List<Item> { new("SKU-1", 2) },
Total = 59.99m,
PaymentToken = "tok_test"
});
Assert.False(result.IsSuccess);
payment.Verify(
p => p.Charge(It.IsAny<decimal>(), It.IsAny<string>()),
Times.Never);
orders.Verify(
o => o.Create(It.IsAny<CheckoutRequest>(), It.IsAny<string>()),
Times.Never);
}
Notice that the out-of-stock test verifies that payment and order creation are never called. Facade pattern tests should validate not just what happens, but also what doesn't happen -- confirming the orchestration short-circuits when a precondition fails.
Follow Consistent Naming Conventions
Facade pattern naming should communicate what subsystem the facade simplifies and that it's a facade. Vague names like Manager, Service, or Helper obscure the structural role the class plays.
A naming convention that works well for facade pattern classes:
// Pattern: {Domain}Facade or {Subsystem}Facade
public sealed class OrderFacade : IOrderFacade { }
public sealed class NotificationFacade : INotificationFacade { }
public sealed class ReportingFacade : IReportingFacade { }
public sealed class UserOnboardingFacade : IUserOnboardingFacade { }
The domain concept comes first, followed by Facade. This groups facade classes visually when browsing alphabetically and immediately signals the class's structural purpose. Interfaces follow the same pattern with an I prefix.
Avoid names that disguise the facade pattern role:
// Avoid: names that hide structural intent
public sealed class OrderManager : IOrderManager { }
public sealed class NotificationHelper : INotificationHelper { }
public sealed class ReportingService : IReportingService { }
OrderManager could be a repository, a service, a controller, or a facade -- the name doesn't tell you. NotificationHelper is even worse because "helper" implies utility methods, not subsystem coordination. When you name your facade pattern classes explicitly, developers scanning the codebase can immediately distinguish between domain services that implement business rules and facades that coordinate subsystem access.
Know When to Split a Facade
A facade pattern class starts focused and grows as the subsystem behind it gains features. Recognizing the signs that a facade needs splitting prevents the drift toward god objects described earlier.
Watch for these indicators that your facade pattern class should be divided:
The constructor takes more than four or five dependencies. Each dependency represents a subsystem component that the facade coordinates, and a long parameter list usually means the facade spans multiple cohesive groups of operations. The class has methods that serve unrelated consumers -- an API controller uses three methods, a background job uses two completely different methods, and a reporting dashboard uses yet another. Methods within the same facade don't share subsystem dependencies -- PlaceOrder uses inventory and payments, while GenerateReport uses a report engine and data warehouse.
When you identify these signals, split along domain boundaries:
// Before: one facade doing too many things
public sealed class CustomerFacade
{
public void RegisterCustomer(Registration reg) { /* ... */ }
public void UpdateProfile(string id, Profile p) { /* ... */ }
public void PlaceOrder(string customerId, OrderReq r) { /* ... */ }
public CreditReport RunCreditCheck(string customerId) { /* ... */ }
public IReadOnlyList<Invoice> GetInvoices(string id) { /* ... */ }
}
// After: split into focused facade pattern classes
public sealed class CustomerRegistrationFacade
{
public void RegisterCustomer(Registration reg) { /* ... */ }
public void UpdateProfile(string id, Profile p) { /* ... */ }
}
public sealed class CustomerOrderFacade
{
public OrderResult PlaceOrder(string customerId, OrderReq r)
{ /* ... */ }
}
public sealed class CustomerFinanceFacade
{
public CreditReport RunCreditCheck(string customerId) { /* ... */ }
public IReadOnlyList<Invoice> GetInvoices(string id) { /* ... */ }
}
Each resulting facade pattern class has a tight dependency set and serves a specific consumer group. Registration and profile management share user-management subsystem dependencies. Order placement coordinates inventory and payment. Finance operations work with credit bureaus and billing systems. The decorator design pattern shares this philosophy of keeping responsibilities narrowly focused -- when behavior grows, you compose new decorators rather than inflating existing classes.
Handle Async Facade Methods Properly
Modern C# applications interact with databases, HTTP APIs, and message queues that expose asynchronous operations. Your facade pattern methods should be async when any subsystem call they coordinate is async. Blocking on async code with .Result or .GetAwaiter().GetResult() risks deadlocks, especially in ASP.NET request pipelines.
Here's a facade that blocks on async subsystem calls:
// Avoid: blocking on async calls inside the facade
public sealed class OrderFacade
{
private readonly IInventoryService _inventory;
private readonly IPaymentService _payment;
public OrderFacade(
IInventoryService inventory,
IPaymentService payment)
{
_inventory = inventory;
_payment = payment;
}
public OrderResult PlaceOrder(OrderRequest request)
{
// Deadlock risk in ASP.NET contexts
var available = _inventory
.CheckAvailabilityAsync(request.Items)
.GetAwaiter()
.GetResult();
var charge = _payment
.ChargeAsync(request.Total, request.PaymentToken)
.GetAwaiter()
.GetResult();
return new OrderResult(charge.TransactionId);
}
}
Expose the facade pattern method as async and await subsystem calls properly:
// Prefer: fully async facade method
public sealed class OrderFacade : IOrderFacade
{
private readonly IInventoryService _inventory;
private readonly IPaymentService _payment;
private readonly IOrderService _orders;
public OrderFacade(
IInventoryService inventory,
IPaymentService payment,
IOrderService orders)
{
_inventory = inventory;
_payment = payment;
_orders = orders;
}
public async Task<OrderResult> PlaceOrderAsync(
OrderRequest request,
CancellationToken cancellationToken = default)
{
var available = await _inventory
.CheckAvailabilityAsync(request.Items, cancellationToken);
if (!available)
{
return OrderResult.OutOfStock();
}
await _inventory.ReserveAsync(request.Items, cancellationToken);
var charge = await _payment.ChargeAsync(
request.Total,
request.PaymentToken,
cancellationToken);
var order = await _orders.CreateAsync(
request,
charge.TransactionId,
cancellationToken);
return OrderResult.Success(order.Id);
}
}
Notice three facade pattern conventions in the async version. First, the method name ends with Async to follow the Task-based Asynchronous Pattern naming convention. Second, it accepts a CancellationToken and passes it through to every subsystem call so the entire orchestration can be cancelled cooperatively. Third, each await is sequential because the operations have ordering dependencies -- you check inventory before charging, and you charge before creating the order.
When subsystem calls are independent, you can run them concurrently with Task.WhenAll to improve facade pattern performance:
public async Task<DashboardData> GetDashboardAsync(
string userId,
CancellationToken cancellationToken = default)
{
var ordersTask = _orders.GetRecentAsync(userId, cancellationToken);
var balanceTask = _billing.GetBalanceAsync(userId, cancellationToken);
var notificationsTask = _notifications
.GetUnreadAsync(userId, cancellationToken);
await Task.WhenAll(ordersTask, balanceTask, notificationsTask);
return new DashboardData(
ordersTask.Result,
balanceTask.Result,
notificationsTask.Result);
}
The facade coordinates three independent subsystem queries in parallel and combines the results. This is a significant advantage of facades for read-heavy operations -- callers get a single method call while the facade optimizes the underlying parallelism.
Organize Facades in Your Project Structure
Where you place facade pattern classes in your solution matters for maintainability. Facades sit between your API layer and your domain or infrastructure components, so they deserve a clear location that reflects that intermediary role.
A practical project structure for facade pattern organization:
src/
├── MyApp.Domain/
│ ├── Interfaces/
│ │ ├── IInventoryService.cs
│ │ ├── IPaymentService.cs
│ │ └── IOrderService.cs
│ └── Models/
│ ├── OrderRequest.cs
│ └── OrderResult.cs
├── MyApp.Application/
│ ├── Facades/
│ │ ├── IOrderFacade.cs
│ │ ├── OrderFacade.cs
│ │ ├── INotificationFacade.cs
│ │ └── NotificationFacade.cs
│ └── DependencyInjection/
│ └── ServiceCollectionExtensions.cs
├── MyApp.Infrastructure/
│ ├── Payment/
│ │ └── StripePaymentService.cs
│ └── Inventory/
│ └── WarehouseInventoryService.cs
└── MyApp.Api/
└── Program.cs
Facade interfaces and implementations live in an application layer between the domain and API projects. The domain defines subsystem interfaces. Infrastructure implements those interfaces against concrete external systems. Facades in the application layer orchestrate the subsystem interfaces without depending on infrastructure details.
Centralize DI registration for facades in an extension method:
// In MyApp.Application
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationFacades(
this IServiceCollection services)
{
services.AddScoped<IOrderFacade, OrderFacade>();
services.AddScoped<INotificationFacade, NotificationFacade>();
services.AddScoped<IReportingFacade, ReportingFacade>();
return services;
}
}
// In Program.cs
builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddApplicationFacades();
This facade pattern organization keeps your composition root clean and makes it obvious which facades are available in the system. Adding a new facade means adding a class to the Facades folder and a single registration line. The dependency direction stays consistent: API references Application, Application references Domain, Infrastructure references Domain, and the API wires everything together at startup.
Frequently Asked Questions
What is the difference between a facade and a service class?
A facade pattern class simplifies access to a subsystem composed of multiple collaborating classes. It coordinates calls across those classes to provide a streamlined interface. A service class typically implements specific business logic or domain rules. The distinction matters because a facade should contain orchestration logic -- calling subsystem methods in the right order -- while a service contains implementation logic. When your "service" is mainly coordinating other services without adding its own business rules, it's functioning as a facade and should be named accordingly.
How many dependencies should a facade have?
Aim for three to five constructor dependencies. Fewer than three usually means the facade isn't coordinating enough to justify the pattern -- a direct call to the single dependency would be simpler. More than five signals that the facade covers too many concerns and should be split. This isn't a rigid rule, but it's a reliable heuristic. Each dependency represents a subsystem component in the facade pattern, and the total count reflects how many moving parts the facade orchestrates.
Should facade methods throw exceptions or return result objects?
Both approaches work, but result objects give callers more control. With exceptions, callers must know which exception types to catch, and forgetting a catch block means unhandled crashes. With result objects like CheckoutResult.Success() or CheckoutResult.OutOfStock(), the return type communicates possible outcomes explicitly. The facade pattern coordinates multiple subsystem calls, so failures are expected and routine -- result objects handle them more gracefully than exceptions for expected failure paths. Reserve exceptions for truly unexpected scenarios like infrastructure failures or programming errors.
Can a facade call another facade?
Avoid it. When one facade calls another, you create a layered orchestration chain that's hard to debug and reason about. If two facades share subsystem calls, extract the shared coordination into a lower-level service that both facades depend on. Facade-to-facade calls also create hidden coupling -- changing the inner facade's behavior silently affects every outer facade that calls it. Keep your facade pattern architecture flat: facades coordinate subsystem components directly, and controllers or application entry points call facades.
How do I version a facade when requirements change?
When a facade pattern method signature needs to change, prefer adding a new method over modifying the existing one. Existing consumers continue calling the original method while new consumers use the updated version. Once all consumers migrate, deprecate and remove the old method. For breaking changes that affect the entire facade pattern contract, consider creating a new interface version (IOrderFacadeV2) alongside the original. Register both in DI so consumers can migrate incrementally. This is safer than modifying the existing contract and breaking all callers at once.
When should I use the facade pattern instead of the adapter pattern?
The facade pattern simplifies a complex subsystem behind a unified interface -- it reduces the number of classes a caller interacts with. The adapter pattern converts one interface into another so incompatible types can work together. Use a facade when your caller needs to coordinate multiple subsystem classes. Use an adapter when your caller needs a single class whose interface doesn't match expectations. You'll sometimes combine both: the facade provides the simplified entry point, and adapters within the subsystem translate individual external dependencies into your domain types.
How do I handle cross-cutting concerns like logging in a facade?
Don't embed logging, metrics, or tracing directly in your facade pattern methods. These cross-cutting concerns obscure the orchestration logic and make the facade harder to test. Instead, use the decorator pattern to wrap your facade interface with logging behavior. A LoggingOrderFacade implements IOrderFacade, logs the call details, delegates to the real OrderFacade, and logs the result. Register the decorator in DI so the logging layer is transparent to consumers. This keeps the facade pattern class focused on coordination while logging stays in its own class.
Wrapping Up Facade Pattern Best Practices
Applying these facade pattern best practices in C# will help you build subsystem interfaces that stay clean as your application grows. The core themes carry through every section: keep facades focused on a single domain area, define narrow interfaces that match consumer needs, let facade methods orchestrate without implementing business logic, and inject all dependencies through the constructor so everything remains testable and swappable.
Start with the simplest facade that hides subsystem complexity -- a sealed class with three or four injected dependencies and one or two orchestration methods. Apply interface segregation when different consumers need different slices. Make facade methods async from the start when any subsystem interaction is asynchronous, and always pass CancellationToken through the call chain. Watch your dependency count -- when it climbs past five, the facade is probably covering too many concerns and needs splitting. The goal isn't abstraction for its own sake -- it's a maintainable coordination layer where subsystem complexity stays hidden and every facade method tells a clear story about what the application is doing.

