Facade Pattern Real-World Example in C#: Complete Implementation
Most facade pattern tutorials show you a "home theater" example with a remote control that turns on five devices. That's fine for grasping the concept, but it won't help when you're knee-deep in an e-commerce system where placing a single order requires coordinating payment processing, inventory management, shipping estimation, and customer notifications. This article builds a complete facade pattern real-world example in C# -- an order processing system where a single facade class shields your application code from the complexity of multiple subsystems working together.
By the end, you'll have a compilable implementation covering the full evolution: the problem that motivates the facade pattern, subsystem design, facade implementation, unit testing, and dependency injection registration. Every piece connects. If you want to see how the facade pattern fits alongside other structural patterns like the adapter pattern, this is the article that shows you.
For more on the facade pattern including code demos, watch this walkthrough:
The Problem: Coordinating Multiple Subsystems for Order Processing
You're building an e-commerce platform. When a customer places an order, your application needs to do several things in sequence: validate that the items are in stock, charge the customer's payment method, reserve the inventory, and send a confirmation email. Each of these responsibilities lives in a separate service with its own interface and error handling.
Here's what happens when the calling code talks to every subsystem directly:
public class OrderController
{
private readonly PaymentService _payment;
private readonly InventoryService _inventory;
private readonly NotificationService _notifications;
public async Task<string> PlaceOrder(
string customerId,
List<OrderItem> items,
PaymentDetails paymentInfo)
{
// Check stock for each item
foreach (var item in items)
{
bool inStock = await _inventory
.CheckStockAsync(item.Sku, item.Quantity);
if (!inStock)
{
throw new InvalidOperationException(
$"Item {item.Sku} is out of stock.");
}
}
// Calculate total
decimal total = items.Sum(
i => i.UnitPrice * i.Quantity);
// Charge payment
var charge = await _payment.ChargeAsync(
paymentInfo.CardToken,
total,
"usd");
if (!charge.Success)
{
throw new InvalidOperationException(
"Payment failed.");
}
// Reserve inventory
foreach (var item in items)
{
await _inventory.ReserveAsync(
item.Sku, item.Quantity);
}
// Send confirmation
await _notifications.SendOrderConfirmationAsync(
customerId,
charge.TransactionId,
items);
return charge.TransactionId;
}
}
Every controller, background job, or API endpoint that needs to place an order must repeat this exact coordination logic. The controller knows about payment tokens, SKU-level inventory checks, and email sending. Testing means mocking three services per test. If the business adds fraud detection or tax calculation, every caller needs updating.
The facade pattern solves this by placing a single class in front of these subsystems. The facade handles the orchestration. Callers make one method call. That's it.
Designing the Subsystem Services
Before building the facade, let's define the subsystems it will coordinate. In a real application, these services might call external APIs, query databases, or publish messages. For this facade pattern example, we'll keep them focused on their individual responsibilities.
The Payment Service
The payment service handles charging customers and processing refunds. It has its own types and error handling:
public sealed class PaymentResult
{
public bool Success { get; init; }
public string TransactionId { get; init; } = "";
public string ErrorMessage { get; init; } = "";
}
public interface IPaymentService
{
Task<PaymentResult> ChargeAsync(
string cardToken,
decimal amount,
string currency);
Task<PaymentResult> RefundAsync(
string transactionId,
decimal amount);
}
public sealed class PaymentService : IPaymentService
{
public Task<PaymentResult> ChargeAsync(
string cardToken,
decimal amount,
string currency)
{
// Simulates calling a payment gateway
return Task.FromResult(new PaymentResult
{
Success = true,
TransactionId = $"txn_{Guid.NewGuid():N}"
});
}
public Task<PaymentResult> RefundAsync(
string transactionId,
decimal amount)
{
return Task.FromResult(new PaymentResult
{
Success = true,
TransactionId = transactionId
});
}
}
The Inventory Service
The inventory service tracks stock levels and handles reservations. It uses SKU-based lookups and quantity management:
public interface IInventoryService
{
Task<bool> CheckStockAsync(
string sku,
int quantity);
Task ReserveAsync(
string sku,
int quantity);
Task ReleaseAsync(
string sku,
int quantity);
}
public sealed class InventoryService : IInventoryService
{
private readonly Dictionary<string, int> _stock = new()
{
["SKU-001"] = 50,
["SKU-002"] = 120,
["SKU-003"] = 5
};
public Task<bool> CheckStockAsync(
string sku,
int quantity)
{
bool available = _stock.TryGetValue(
sku, out int current) && current >= quantity;
return Task.FromResult(available);
}
public Task ReserveAsync(
string sku,
int quantity)
{
if (_stock.ContainsKey(sku))
{
_stock[sku] -= quantity;
}
return Task.CompletedTask;
}
public Task ReleaseAsync(
string sku,
int quantity)
{
if (_stock.ContainsKey(sku))
{
_stock[sku] += quantity;
}
return Task.CompletedTask;
}
}
The Notification Service
The notification service sends emails, push notifications, or SMS messages. For the facade pattern, the key point is that callers shouldn't need to know which notification channel is used or how messages are formatted:
public interface INotificationService
{
Task SendOrderConfirmationAsync(
string customerId,
string orderId,
decimal totalAmount);
Task SendPaymentFailureAsync(
string customerId,
string reason);
Task SendRefundConfirmationAsync(
string customerId,
string orderId,
decimal refundAmount);
}
public sealed class NotificationService
: INotificationService
{
public Task SendOrderConfirmationAsync(
string customerId,
string orderId,
decimal totalAmount)
{
// Simulates sending email/SMS
Console.WriteLine(
$"Order {orderId} confirmed for " +
$"customer {customerId}. " +
$"Total: {totalAmount:C}");
return Task.CompletedTask;
}
public Task SendPaymentFailureAsync(
string customerId,
string reason)
{
Console.WriteLine(
$"Payment failed for {customerId}: " +
$"{reason}");
return Task.CompletedTask;
}
public Task SendRefundConfirmationAsync(
string customerId,
string orderId,
decimal refundAmount)
{
Console.WriteLine(
$"Refund of {refundAmount:C} issued for " +
$"order {orderId}");
return Task.CompletedTask;
}
}
Each of these subsystems is independently testable, independently deployable, and independently comprehensible. The problem isn't the subsystems themselves -- it's the orchestration logic that connects them. That's exactly the problem the facade pattern addresses.
Defining the Facade's Domain Types
Before implementing the facade, we need types that represent what the caller actually cares about. These types belong to the facade layer, not to any individual subsystem. This keeps the facade pattern's API clean and decoupled from subsystem internals:
public sealed record OrderItem(
string Sku,
int Quantity,
decimal UnitPrice);
public sealed record PaymentDetails(
string CardToken);
public sealed record OrderRequest(
string CustomerId,
List<OrderItem> Items,
PaymentDetails Payment);
public sealed record OrderResult
{
public bool Success { get; init; }
public string OrderId { get; init; } = "";
public decimal TotalCharged { get; init; }
public string ErrorMessage { get; init; } = "";
}
These records give callers a simple vocabulary for placing orders. The caller doesn't need to know about payment tokens being passed to a payment gateway, SKU-level inventory calls, or notification channel selection. The facade handles all of that. Understanding how to design clean contracts like this connects directly to the principles behind inversion of control and interface-driven architecture.
Building the Order Processing Facade
Now we build the facade itself. The OrderFacade class coordinates the payment, inventory, and notification subsystems behind a single PlaceOrderAsync method. This is where the facade pattern delivers its value -- callers interact with one class, one method, and one result type:
public interface IOrderFacade
{
Task<OrderResult> PlaceOrderAsync(
OrderRequest request);
Task<OrderResult> RefundOrderAsync(
string customerId,
string orderId,
List<OrderItem> items);
}
public sealed class OrderFacade : IOrderFacade
{
private readonly IPaymentService _payment;
private readonly IInventoryService _inventory;
private readonly INotificationService _notifications;
public OrderFacade(
IPaymentService payment,
IInventoryService inventory,
INotificationService notifications)
{
_payment = payment;
_inventory = inventory;
_notifications = notifications;
}
public async Task<OrderResult> PlaceOrderAsync(
OrderRequest request)
{
// Step 1: Validate inventory for all items
foreach (var item in request.Items)
{
bool inStock = await _inventory
.CheckStockAsync(item.Sku, item.Quantity);
if (!inStock)
{
return new OrderResult
{
Success = false,
ErrorMessage =
$"Item {item.Sku} is out of stock."
};
}
}
// Step 2: Calculate total
decimal total = request.Items.Sum(
i => i.UnitPrice * i.Quantity);
// Step 3: Process payment
var paymentResult = await _payment.ChargeAsync(
request.Payment.CardToken,
total,
"usd");
if (!paymentResult.Success)
{
await _notifications.SendPaymentFailureAsync(
request.CustomerId,
paymentResult.ErrorMessage);
return new OrderResult
{
Success = false,
ErrorMessage = "Payment processing failed."
};
}
// Step 4: Reserve inventory
foreach (var item in request.Items)
{
await _inventory.ReserveAsync(
item.Sku, item.Quantity);
}
// Step 5: Send confirmation
await _notifications.SendOrderConfirmationAsync(
request.CustomerId,
paymentResult.TransactionId,
total);
return new OrderResult
{
Success = true,
OrderId = paymentResult.TransactionId,
TotalCharged = total
};
}
public async Task<OrderResult> RefundOrderAsync(
string customerId,
string orderId,
List<OrderItem> items)
{
decimal refundAmount = items.Sum(
i => i.UnitPrice * i.Quantity);
// Step 1: Process refund
var refundResult = await _payment.RefundAsync(
orderId,
refundAmount);
if (!refundResult.Success)
{
return new OrderResult
{
Success = false,
ErrorMessage = "Refund processing failed."
};
}
// Step 2: Release reserved inventory
foreach (var item in items)
{
await _inventory.ReleaseAsync(
item.Sku, item.Quantity);
}
// Step 3: Notify customer
await _notifications.SendRefundConfirmationAsync(
customerId,
orderId,
refundAmount);
return new OrderResult
{
Success = true,
OrderId = orderId,
TotalCharged = -refundAmount
};
}
}
Look at what the facade pattern does here. The PlaceOrderAsync method encapsulates a five-step workflow that touches three different subsystems. If payment fails, the facade notifies the customer without the caller needing to handle that edge case. If an item is out of stock, the facade returns a clean error result before payment is ever attempted. The RefundOrderAsync method handles the reverse flow -- refunding payment, releasing inventory, and sending a refund confirmation.
The facade doesn't replace the subsystems. They still exist, they still have their own interfaces, and they can still be used directly when needed. The facade pattern adds a simplified entry point for the most common use case. This is what separates a facade from an adapter -- the adapter translates one interface to another, while the facade simplifies access to a group of interfaces.
Using the Facade in Application Code
With the facade in place, the controller or API endpoint becomes trivially simple:
public class OrderController
{
private readonly IOrderFacade _orderFacade;
public OrderController(IOrderFacade orderFacade)
{
_orderFacade = orderFacade;
}
public async Task<OrderResult> PlaceOrder(
string customerId,
List<OrderItem> items,
string cardToken)
{
var request = new OrderRequest(
CustomerId: customerId,
Items: items,
Payment: new PaymentDetails(cardToken));
return await _orderFacade
.PlaceOrderAsync(request);
}
}
Compare this to the original controller at the top of the article. No inventory loops. No payment error handling. No notification calls. The controller does exactly one thing -- it builds a request and delegates to the facade. If you're familiar with how the command pattern encapsulates operations, the facade pattern works similarly by hiding operational complexity behind a clean method signature.
Testing the Facade Pattern Implementation
Unit tests for the facade verify that the orchestration logic works correctly across different scenarios. We mock each subsystem interface to control their behavior and assert that the facade coordinates them properly:
public sealed class OrderFacadeTests
{
private readonly Mock<IPaymentService> _mockPayment;
private readonly Mock<IInventoryService> _mockInventory;
private readonly Mock<INotificationService> _mockNotifications;
private readonly OrderFacade _facade;
public OrderFacadeTests()
{
_mockPayment = new Mock<IPaymentService>();
_mockInventory = new Mock<IInventoryService>();
_mockNotifications =
new Mock<INotificationService>();
_facade = new OrderFacade(
_mockPayment.Object,
_mockInventory.Object,
_mockNotifications.Object);
}
[Fact]
public async Task PlaceOrderAsync_AllItemsInStock_ReturnsSuccess()
{
var items = new List<OrderItem>
{
new("SKU-001", 2, 25.00m),
new("SKU-002", 1, 15.00m)
};
_mockInventory
.Setup(i => i.CheckStockAsync(
It.IsAny<string>(), It.IsAny<int>()))
.ReturnsAsync(true);
_mockPayment
.Setup(p => p.ChargeAsync(
It.IsAny<string>(),
It.IsAny<decimal>(),
It.IsAny<string>()))
.ReturnsAsync(new PaymentResult
{
Success = true,
TransactionId = "txn_abc123"
});
var request = new OrderRequest(
"cust_001", items,
new PaymentDetails("tok_visa"));
var result = await _facade
.PlaceOrderAsync(request);
Assert.True(result.Success);
Assert.Equal("txn_abc123", result.OrderId);
Assert.Equal(65.00m, result.TotalCharged);
_mockNotifications.Verify(
n => n.SendOrderConfirmationAsync(
"cust_001", "txn_abc123", 65.00m),
Times.Once);
}
[Fact]
public async Task PlaceOrderAsync_ItemOutOfStock_ReturnsFailure()
{
var items = new List<OrderItem>
{
new("SKU-999", 1, 50.00m)
};
_mockInventory
.Setup(i => i.CheckStockAsync(
"SKU-999", 1))
.ReturnsAsync(false);
var request = new OrderRequest(
"cust_002", items,
new PaymentDetails("tok_visa"));
var result = await _facade
.PlaceOrderAsync(request);
Assert.False(result.Success);
Assert.Contains("out of stock",
result.ErrorMessage);
_mockPayment.Verify(
p => p.ChargeAsync(
It.IsAny<string>(),
It.IsAny<decimal>(),
It.IsAny<string>()),
Times.Never);
}
[Fact]
public async Task PlaceOrderAsync_PaymentFails_NotifiesCustomer()
{
var items = new List<OrderItem>
{
new("SKU-001", 1, 30.00m)
};
_mockInventory
.Setup(i => i.CheckStockAsync(
It.IsAny<string>(), It.IsAny<int>()))
.ReturnsAsync(true);
_mockPayment
.Setup(p => p.ChargeAsync(
It.IsAny<string>(),
It.IsAny<decimal>(),
It.IsAny<string>()))
.ReturnsAsync(new PaymentResult
{
Success = false,
ErrorMessage = "Card declined"
});
var request = new OrderRequest(
"cust_003", items,
new PaymentDetails("tok_declined"));
var result = await _facade
.PlaceOrderAsync(request);
Assert.False(result.Success);
_mockNotifications.Verify(
n => n.SendPaymentFailureAsync(
"cust_003", "Card declined"),
Times.Once);
_mockInventory.Verify(
i => i.ReserveAsync(
It.IsAny<string>(),
It.IsAny<int>()),
Times.Never);
}
[Fact]
public async Task RefundOrderAsync_ValidOrder_ReleasesInventory()
{
var items = new List<OrderItem>
{
new("SKU-001", 2, 25.00m)
};
_mockPayment
.Setup(p => p.RefundAsync(
"txn_abc123", 50.00m))
.ReturnsAsync(new PaymentResult
{
Success = true,
TransactionId = "txn_abc123"
});
var result = await _facade.RefundOrderAsync(
"cust_001", "txn_abc123", items);
Assert.True(result.Success);
_mockInventory.Verify(
i => i.ReleaseAsync("SKU-001", 2),
Times.Once);
_mockNotifications.Verify(
n => n.SendRefundConfirmationAsync(
"cust_001", "txn_abc123", 50.00m),
Times.Once);
}
}
These tests demonstrate the real power of the facade pattern from a testing perspective. Instead of testing the entire subsystem coordination in a controller, you test the orchestration logic in isolation. Each test verifies a specific scenario: happy path, out-of-stock items, payment failures, and refund processing. The mocks let you control exactly what each subsystem returns, making it easy to verify that the facade coordinates them in the right order with the right data.
Notice that the out-of-stock test verifies payment was never attempted. The payment failure test verifies inventory was never reserved. These assertions confirm the facade's ordering logic is correct -- you wouldn't catch these bugs if the coordination lived scattered across controller methods.
Wiring Everything Up with Dependency Injection
The final step is registering the facade and its subsystems in the DI container. Because the facade depends on interfaces, swapping implementations is straightforward:
using Microsoft.Extensions.DependencyInjection;
public static class OrderServiceRegistration
{
public static IServiceCollection AddOrderProcessing(
this IServiceCollection services)
{
// Register subsystem services
services.AddSingleton<
IPaymentService, PaymentService>();
services.AddSingleton<
IInventoryService, InventoryService>();
services.AddSingleton<
INotificationService, NotificationService>();
// Register the facade
services.AddTransient<
IOrderFacade, OrderFacade>();
return services;
}
}
In your Program.cs, a single call wires everything together:
builder.Services.AddOrderProcessing();
The DI container resolves the facade's constructor dependencies automatically. If you later replace PaymentService with a Stripe integration, you change one registration line. The facade doesn't change. The controller doesn't change. This is the separation of concerns that the facade pattern enables, and it works hand-in-hand with the dependency injection principles that .NET developers rely on.
When the Facade Pattern Works Best
The facade pattern is most effective when you have a group of related subsystems that need to be coordinated for a common operation. E-commerce order processing is a classic case, but the same structure applies to user registration flows (validate, create account, send welcome email, provision resources), report generation pipelines, or any workflow that touches multiple services in a specific sequence.
The key indicator that you need a facade is when you find the same multi-service coordination logic duplicated across controllers, background workers, or API endpoints. That duplication is a signal that the orchestration belongs in a dedicated class. You can think of it similarly to how the composite pattern unifies individual objects and groups under a single interface -- the facade pattern unifies multiple subsystem interactions under a single method call.
A common question is whether the facade pattern violates the single responsibility principle. It doesn't. The facade's single responsibility is orchestration. It doesn't implement payment processing, inventory management, or notification delivery. It coordinates existing implementations. That's one reason, one job, one axis of change.
Facade Pattern vs. Other Structural Patterns
The facade pattern is often confused with the adapter pattern and the strategy pattern. Here's how they differ. The adapter pattern translates one interface into another -- it bridges incompatible types. The facade pattern doesn't translate anything; it simplifies access to a collection of interfaces that already work fine individually. The strategy pattern lets you swap interchangeable algorithms at runtime. The facade pattern doesn't swap anything -- it provides a fixed orchestration workflow.
In practice, you'll often combine these patterns. A facade might use adapters internally to normalize third-party APIs. A facade might use the strategy pattern to select between different payment providers. Patterns work best as composable building blocks, not as isolated solutions. Understanding these distinctions helps you apply the right pattern to the right problem -- and avoid forcing a pattern where a simpler approach would work.
Frequently Asked Questions
Should a facade expose all methods from its subsystems?
No. A facade should expose only the high-level operations that callers actually need. The OrderFacade in this article exposes PlaceOrderAsync and RefundOrderAsync, not individual methods for checking stock, charging cards, or sending emails. If a caller needs direct subsystem access, they can depend on the subsystem interface directly. The facade is a convenience layer for common workflows, not a proxy for every subsystem method.
Can a facade call another facade?
Yes, and it's more common than you'd expect. A large application might have an OrderFacade that delegates to a ShippingFacade during order placement. The key constraint is to avoid circular dependencies. Each facade should have a clear scope, and the dependency graph should flow in one direction. Keeping facade layers thin prevents them from becoming monolithic god classes.
How do I handle errors that span multiple subsystems in a facade?
Design your facade to handle partial failures gracefully. If payment succeeds but inventory reservation fails, the facade should refund the payment before returning an error. The OrderFacade in this article checks inventory before charging payment, which avoids the refund-on-failure scenario entirely. For more complex workflows, consider using the command pattern to implement compensating transactions that undo completed steps.
Does the facade pattern hurt performance?
The facade pattern adds one additional method call per operation -- the overhead is negligible. The facade doesn't add network calls, database queries, or I/O that wasn't already happening. It simply moves the coordination logic into a dedicated class. If anything, a well-designed facade can improve performance by optimizing the order of subsystem calls or short-circuiting early when a prerequisite fails.
When should I NOT use the facade pattern?
Don't use the facade pattern when you have a single subsystem or when the coordination logic is genuinely trivial. If placing an order means calling one service and returning the result, a facade adds indirection without value. Also avoid the facade pattern when callers genuinely need fine-grained control over each subsystem interaction -- forcing them through a facade would limit their flexibility.
How does the facade pattern relate to dependency injection?
The facade pattern and dependency injection complement each other perfectly. The facade depends on subsystem interfaces injected through its constructor, making it easy to test and easy to swap implementations. DI resolves the subsystem dependencies automatically. Without DI, you'd need to manually construct each subsystem and pass it to the facade, which defeats much of the decoupling benefit.
Can I have multiple facades for the same set of subsystems?
Absolutely. You might have an OrderFacade for customer-facing order placement and an AdminOrderFacade for internal operations like bulk refunds or inventory adjustments. Each facade provides a different simplified view of the same subsystems, tailored to its specific audience. This is a perfectly valid and common approach in larger applications.
Wrapping Up This Facade Pattern Real-World Example
This implementation shows the facade pattern doing what it does best -- simplifying complex multi-subsystem coordination behind a clean, testable interface. We started with a controller tangled in payment processing, inventory checks, and notification logic. We ended with an OrderFacade that encapsulates the entire order workflow, a set of focused subsystem services, and a controller that does nothing but delegate.
The facade pattern shines in e-commerce, user onboarding, report generation, and anywhere you coordinate multiple services for a single business operation. When you find the same multi-step orchestration copied across your codebase, wrap it in a facade. Your callers get a simpler API. Your tests get cleaner. Your subsystems stay independent.
Take this order processing example, replace the simulated services with your actual payment gateway, inventory database, and email provider, and you've got a production-ready orchestration layer. The facade pattern keeps your coordination logic centralized, your subsystems decoupled, and your application code focused on what it should be doing -- handling requests and returning results.

