Table of Contents

Facade Pattern Real-World Examples

Production-ready examples demonstrating the Facade pattern in PatternKit.


Example 1: E-Commerce Checkout System

The Problem

An e-commerce checkout involves coordinating multiple services: cart, pricing, tax, inventory, payment, shipping, and notifications. Exposing all these services to the checkout UI creates tight coupling and complexity.

The Solution

A checkout facade that coordinates all services behind simple operation names.

The Code

using PatternKit.Structural.Facade;

public record CheckoutRequest(
    string CartId,
    string CustomerId,
    PaymentInfo Payment,
    ShippingAddress Address);

public record CheckoutResult(
    string Status,
    string? OrderId = null,
    decimal? Total = null,
    string? Error = null);

public class CheckoutFacade
{
    private readonly ICartService _cart;
    private readonly IPricingService _pricing;
    private readonly ITaxService _tax;
    private readonly IInventoryService _inventory;
    private readonly IPaymentService _payment;
    private readonly IShippingService _shipping;
    private readonly INotificationService _notification;
    private readonly ILogger<CheckoutFacade> _logger;

    private readonly Facade<CheckoutRequest, CheckoutResult> _facade;

    public CheckoutFacade(
        ICartService cart,
        IPricingService pricing,
        ITaxService tax,
        IInventoryService inventory,
        IPaymentService payment,
        IShippingService shipping,
        INotificationService notification,
        ILogger<CheckoutFacade> logger)
    {
        _cart = cart;
        _pricing = pricing;
        _tax = tax;
        _inventory = inventory;
        _payment = payment;
        _shipping = shipping;
        _notification = notification;
        _logger = logger;

        _facade = Facade<CheckoutRequest, CheckoutResult>.Create()
            .Operation("checkout", ExecuteCheckout)
            .Operation("validate", ValidateCart)
            .Operation("preview", PreviewOrder)
            .Default((in CheckoutRequest _) =>
                new CheckoutResult("Error", Error: "Unknown operation"))
            .Build();
    }

    public CheckoutResult Execute(string operation, CheckoutRequest request)
    {
        _logger.LogInformation("Checkout operation: {Operation} for customer {CustomerId}",
            operation, request.CustomerId);
        return _facade.Execute(operation, request);
    }

    private CheckoutResult ExecuteCheckout(in CheckoutRequest req)
    {
        try
        {
            // 1. Get cart items
            var items = _cart.GetItems(req.CartId);
            if (!items.Any())
                return new CheckoutResult("Error", Error: "Cart is empty");

            // 2. Calculate pricing
            var pricing = _pricing.Calculate(items);

            // 3. Calculate tax
            var tax = _tax.Calculate(pricing.Subtotal, req.Address);

            // 4. Reserve inventory
            var reservation = _inventory.Reserve(items);
            if (!reservation.Success)
            {
                return new CheckoutResult("Error",
                    Error: $"Items unavailable: {string.Join(", ", reservation.UnavailableItems)}");
            }

            // 5. Process payment
            var total = pricing.Subtotal + pricing.Shipping + tax.Amount;
            var paymentResult = _payment.Process(req.Payment, total);
            if (!paymentResult.Success)
            {
                _inventory.Release(reservation.ReservationId);
                return new CheckoutResult("Error", Error: "Payment failed");
            }

            // 6. Create shipment
            var shipment = _shipping.Create(items, req.Address);

            // 7. Create order
            var orderId = Guid.NewGuid().ToString();

            // 8. Clear cart
            _cart.Clear(req.CartId);

            // 9. Send confirmation
            _notification.SendOrderConfirmation(req.CustomerId, orderId, total);

            _logger.LogInformation("Checkout completed: Order {OrderId}", orderId);

            return new CheckoutResult("Success", orderId, total);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Checkout failed for customer {CustomerId}", req.CustomerId);
            return new CheckoutResult("Error", Error: "Checkout failed. Please try again.");
        }
    }

    private CheckoutResult ValidateCart(in CheckoutRequest req)
    {
        var items = _cart.GetItems(req.CartId);
        if (!items.Any())
            return new CheckoutResult("Invalid", Error: "Cart is empty");

        var availability = _inventory.CheckAvailability(items);
        if (!availability.AllAvailable)
            return new CheckoutResult("Invalid",
                Error: $"Unavailable: {string.Join(", ", availability.UnavailableItems)}");

        return new CheckoutResult("Valid");
    }

    private CheckoutResult PreviewOrder(in CheckoutRequest req)
    {
        var items = _cart.GetItems(req.CartId);
        var pricing = _pricing.Calculate(items);
        var tax = _tax.Calculate(pricing.Subtotal, req.Address);
        var total = pricing.Subtotal + pricing.Shipping + tax.Amount;

        return new CheckoutResult("Preview", Total: total);
    }
}

// Usage
var facade = new CheckoutFacade(cart, pricing, tax, inventory, payment, shipping, notification, logger);

// Validate before checkout
var validation = facade.Execute("validate", request);
if (validation.Status != "Valid")
{
    Console.WriteLine($"Validation failed: {validation.Error}");
    return;
}

// Preview total
var preview = facade.Execute("preview", request);
Console.WriteLine($"Order total: {preview.Total:C}");

// Complete checkout
var result = facade.Execute("checkout", request);
if (result.Status == "Success")
{
    Console.WriteLine($"Order {result.OrderId} placed successfully!");
}

Why This Pattern

The checkout facade hides the complexity of coordinating 7+ services. The UI only needs to know about three operations: validate, preview, and checkout.


Example 2: User Management System (TypedFacade)

The Problem

A user management system requires multiple operations with different parameter types. String-based operations are error-prone and lack IntelliSense.

The Solution

A TypedFacade providing compile-time safety for user operations.

The Code

using PatternKit.Structural.Facade;

// Define the contract
public interface IUserManagement
{
    UserResult CreateUser(CreateUserRequest request);
    UserResult GetUser(int userId);
    UserResult UpdateUser(UpdateUserRequest request);
    bool DeleteUser(int userId);
    IEnumerable<UserResult> SearchUsers(string query);
}

public record CreateUserRequest(string Email, string Name, string Role);
public record UpdateUserRequest(int UserId, string? Name, string? Email, string? Role);
public record UserResult(int Id, string Email, string Name, string Role, bool Active);

public class UserManagementFacade
{
    private readonly IUserRepository _repository;
    private readonly IAuthService _auth;
    private readonly IAuditService _audit;
    private readonly INotificationService _notification;

    public IUserManagement Facade { get; }

    public UserManagementFacade(
        IUserRepository repository,
        IAuthService auth,
        IAuditService audit,
        INotificationService notification)
    {
        _repository = repository;
        _auth = auth;
        _audit = audit;
        _notification = notification;

        Facade = TypedFacade<IUserManagement>.Create()
            .Map(x => x.CreateUser, CreateUser)
            .Map(x => x.GetUser, GetUser)
            .Map(x => x.UpdateUser, UpdateUser)
            .Map(x => x.DeleteUser, DeleteUser)
            .Map(x => x.SearchUsers, SearchUsers)
            .Build();
    }

    private UserResult CreateUser(CreateUserRequest request)
    {
        // Validate email
        if (_repository.EmailExists(request.Email))
            throw new DuplicateEmailException(request.Email);

        // Create user
        var user = _repository.Create(new User
        {
            Email = request.Email,
            Name = request.Name,
            Role = request.Role,
            Active = true
        });

        // Create auth credentials
        var tempPassword = _auth.GenerateTemporaryPassword();
        _auth.CreateCredentials(user.Id, tempPassword);

        // Send welcome email
        _notification.SendWelcome(user.Email, tempPassword);

        // Audit
        _audit.LogUserCreated(user.Id, request);

        return MapToResult(user);
    }

    private UserResult GetUser(int userId)
    {
        var user = _repository.GetById(userId)
            ?? throw new UserNotFoundException(userId);
        return MapToResult(user);
    }

    private UserResult UpdateUser(UpdateUserRequest request)
    {
        var user = _repository.GetById(request.UserId)
            ?? throw new UserNotFoundException(request.UserId);

        if (request.Name != null) user.Name = request.Name;
        if (request.Email != null)
        {
            if (_repository.EmailExists(request.Email) && user.Email != request.Email)
                throw new DuplicateEmailException(request.Email);
            user.Email = request.Email;
        }
        if (request.Role != null) user.Role = request.Role;

        _repository.Update(user);
        _audit.LogUserUpdated(user.Id, request);

        return MapToResult(user);
    }

    private bool DeleteUser(int userId)
    {
        var user = _repository.GetById(userId);
        if (user == null) return false;

        // Soft delete
        user.Active = false;
        _repository.Update(user);

        // Revoke auth
        _auth.RevokeCredentials(userId);

        // Audit
        _audit.LogUserDeleted(userId);

        return true;
    }

    private IEnumerable<UserResult> SearchUsers(string query)
    {
        return _repository.Search(query).Select(MapToResult);
    }

    private static UserResult MapToResult(User user) =>
        new(user.Id, user.Email, user.Name, user.Role, user.Active);
}

// Usage - fully type-safe
var facade = new UserManagementFacade(repo, auth, audit, notification);

// Create user - IntelliSense shows parameter types
var newUser = facade.Facade.CreateUser(new CreateUserRequest(
    Email: "alice@example.com",
    Name: "Alice Smith",
    Role: "Admin"
));

// Get user - compile-time parameter checking
var user = facade.Facade.GetUser(newUser.Id);

// Update user
var updated = facade.Facade.UpdateUser(new UpdateUserRequest(
    UserId: user.Id,
    Name: "Alice Johnson",
    Email: null,
    Role: null
));

// Search users
var admins = facade.Facade.SearchUsers("Admin");

Why This Pattern

TypedFacade provides compile-time safety - typos are caught at build time, not runtime. IntelliSense shows available operations and their parameters.


Example 3: Media Transcoding Service

The Problem

A media transcoding service needs to coordinate multiple subsystems: storage, transcoding engines, CDN, and analytics. Different operations have different complexity levels.

The Solution

A facade that exposes simple operations while handling complex orchestration internally.

The Code

using PatternKit.Structural.Facade;

public record TranscodeRequest(
    string SourceUrl,
    string[] OutputFormats,
    TranscodeOptions Options);

public record TranscodeOptions(
    int? MaxWidth,
    int? MaxHeight,
    string? WatermarkUrl,
    bool GenerateThumbnails);

public record TranscodeResult(
    string Status,
    string JobId,
    Dictionary<string, string>? OutputUrls = null,
    string[]? ThumbnailUrls = null,
    string? Error = null);

public class MediaTranscodingFacade
{
    private readonly IStorageService _storage;
    private readonly ITranscodeEngine _transcoder;
    private readonly ICdnService _cdn;
    private readonly IAnalyticsService _analytics;
    private readonly IQueueService _queue;

    private readonly Facade<TranscodeRequest, TranscodeResult> _facade;

    public MediaTranscodingFacade(
        IStorageService storage,
        ITranscodeEngine transcoder,
        ICdnService cdn,
        IAnalyticsService analytics,
        IQueueService queue)
    {
        _storage = storage;
        _transcoder = transcoder;
        _cdn = cdn;
        _analytics = analytics;
        _queue = queue;

        _facade = Facade<TranscodeRequest, TranscodeResult>.Create()
            .OperationIgnoreCase("transcode", StartTranscode)
            .OperationIgnoreCase("quick", QuickTranscode)
            .OperationIgnoreCase("status", CheckStatus)
            .OperationIgnoreCase("cancel", CancelJob)
            .Build();
    }

    public TranscodeResult Execute(string operation, TranscodeRequest request)
        => _facade.Execute(operation, request);

    private TranscodeResult StartTranscode(in TranscodeRequest req)
    {
        // 1. Download source to temp storage
        var tempPath = _storage.DownloadToTemp(req.SourceUrl);

        // 2. Validate source file
        var mediaInfo = _transcoder.Analyze(tempPath);
        if (!mediaInfo.IsValidMedia)
            return new TranscodeResult("Error", "", Error: "Invalid media file");

        // 3. Create job
        var jobId = Guid.NewGuid().ToString();

        // 4. Queue transcoding tasks for each format
        foreach (var format in req.OutputFormats)
        {
            _queue.Enqueue(new TranscodeTask
            {
                JobId = jobId,
                SourcePath = tempPath,
                OutputFormat = format,
                Options = req.Options
            });
        }

        // 5. Queue thumbnail generation if requested
        if (req.Options.GenerateThumbnails)
        {
            _queue.Enqueue(new ThumbnailTask
            {
                JobId = jobId,
                SourcePath = tempPath,
                Count = 5
            });
        }

        // 6. Track analytics
        _analytics.TrackJobStarted(jobId, req.OutputFormats.Length);

        return new TranscodeResult("Queued", jobId);
    }

    private TranscodeResult QuickTranscode(in TranscodeRequest req)
    {
        // Synchronous transcoding for small files
        var tempPath = _storage.DownloadToTemp(req.SourceUrl);
        var mediaInfo = _transcoder.Analyze(tempPath);

        if (mediaInfo.Duration > TimeSpan.FromMinutes(5))
            return StartTranscode(in req); // Too long, queue instead

        var jobId = Guid.NewGuid().ToString();
        var outputs = new Dictionary<string, string>();

        foreach (var format in req.OutputFormats)
        {
            var outputPath = _transcoder.TranscodeSync(tempPath, format, req.Options);
            var cdnUrl = _cdn.Upload(outputPath);
            outputs[format] = cdnUrl;
        }

        string[]? thumbnails = null;
        if (req.Options.GenerateThumbnails)
        {
            var thumbPaths = _transcoder.GenerateThumbnails(tempPath, 5);
            thumbnails = thumbPaths.Select(p => _cdn.Upload(p)).ToArray();
        }

        _storage.DeleteTemp(tempPath);
        _analytics.TrackJobCompleted(jobId, outputs.Count);

        return new TranscodeResult("Completed", jobId, outputs, thumbnails);
    }

    private TranscodeResult CheckStatus(in TranscodeRequest req)
    {
        // Use SourceUrl as job ID for status checks
        var jobId = req.SourceUrl;
        var status = _queue.GetJobStatus(jobId);

        if (status.IsComplete)
        {
            var outputs = _cdn.GetOutputUrls(jobId);
            var thumbnails = _cdn.GetThumbnailUrls(jobId);
            return new TranscodeResult("Completed", jobId, outputs, thumbnails);
        }

        return new TranscodeResult(status.State, jobId);
    }

    private TranscodeResult CancelJob(in TranscodeRequest req)
    {
        var jobId = req.SourceUrl;
        _queue.CancelJob(jobId);
        _analytics.TrackJobCancelled(jobId);
        return new TranscodeResult("Cancelled", jobId);
    }
}

// Usage
var facade = new MediaTranscodingFacade(storage, transcoder, cdn, analytics, queue);

// Start async transcoding
var job = facade.Execute("transcode", new TranscodeRequest(
    SourceUrl: "https://storage.example.com/video.mp4",
    OutputFormats: new[] { "720p", "1080p", "4k" },
    Options: new TranscodeOptions(
        MaxWidth: 3840,
        MaxHeight: 2160,
        WatermarkUrl: "https://example.com/watermark.png",
        GenerateThumbnails: true
    )
));

Console.WriteLine($"Job {job.JobId} queued");

// Check status later
var status = facade.Execute("status", new TranscodeRequest(
    SourceUrl: job.JobId, // Job ID passed via SourceUrl
    OutputFormats: Array.Empty<string>(),
    Options: new TranscodeOptions(null, null, null, false)
));

if (status.Status == "Completed")
{
    foreach (var (format, url) in status.OutputUrls!)
    {
        Console.WriteLine($"{format}: {url}");
    }
}

Why This Pattern

The facade hides complex coordination between storage, transcoding, CDN, and queue services. Clients only need to know "transcode" to trigger the entire workflow.


Example 4: Financial Reporting System

The Problem

A financial reporting system needs to aggregate data from multiple sources, apply complex calculations, and generate reports in various formats. Direct service access creates maintenance nightmares.

The Solution

A facade that exposes report generation as simple operations while handling data aggregation internally.

The Code

using PatternKit.Structural.Facade;

public record ReportRequest(
    string ReportType,
    DateRange Period,
    string[] Departments,
    string OutputFormat);

public record DateRange(DateTime Start, DateTime End);

public record ReportResult(
    string Status,
    byte[]? Content = null,
    string? ContentType = null,
    string? Filename = null,
    ReportMetadata? Metadata = null,
    string? Error = null);

public record ReportMetadata(
    int TotalRecords,
    decimal TotalAmount,
    Dictionary<string, decimal> ByDepartment);

public class FinancialReportingFacade
{
    private readonly ITransactionService _transactions;
    private readonly IBudgetService _budget;
    private readonly IForecastService _forecast;
    private readonly IExportService _export;
    private readonly ICacheService _cache;

    private readonly Facade<ReportRequest, ReportResult> _facade;

    public FinancialReportingFacade(
        ITransactionService transactions,
        IBudgetService budget,
        IForecastService forecast,
        IExportService export,
        ICacheService cache)
    {
        _transactions = transactions;
        _budget = budget;
        _forecast = forecast;
        _export = export;
        _cache = cache;

        _facade = Facade<ReportRequest, ReportResult>.Create()
            .Operation("expense", GenerateExpenseReport)
            .Operation("revenue", GenerateRevenueReport)
            .Operation("budget-variance", GenerateBudgetVarianceReport)
            .Operation("forecast", GenerateForecastReport)
            .Operation("executive-summary", GenerateExecutiveSummary)
            .Default((in ReportRequest req) =>
                new ReportResult("Error", Error: $"Unknown report type: {req.ReportType}"))
            .Build();
    }

    public ReportResult Generate(ReportRequest request)
    {
        // Check cache first
        var cacheKey = GenerateCacheKey(request);
        if (_cache.TryGet<ReportResult>(cacheKey, out var cached))
            return cached;

        var result = _facade.Execute(request.ReportType, request);

        if (result.Status == "Success")
            _cache.Set(cacheKey, result, TimeSpan.FromHours(1));

        return result;
    }

    private ReportResult GenerateExpenseReport(in ReportRequest req)
    {
        // Aggregate expenses from multiple sources
        var expenses = _transactions.GetExpenses(req.Period, req.Departments);
        var byCategory = expenses.GroupBy(e => e.Category)
            .ToDictionary(g => g.Key, g => g.Sum(e => e.Amount));
        var byDepartment = expenses.GroupBy(e => e.Department)
            .ToDictionary(g => g.Key, g => g.Sum(e => e.Amount));

        var metadata = new ReportMetadata(
            TotalRecords: expenses.Count,
            TotalAmount: expenses.Sum(e => e.Amount),
            ByDepartment: byDepartment
        );

        // Generate report in requested format
        var (content, contentType) = req.OutputFormat switch
        {
            "pdf" => (_export.ToPdf(expenses, "Expense Report"), "application/pdf"),
            "excel" => (_export.ToExcel(expenses), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
            "csv" => (_export.ToCsv(expenses), "text/csv"),
            _ => throw new ArgumentException($"Unsupported format: {req.OutputFormat}")
        };

        return new ReportResult(
            Status: "Success",
            Content: content,
            ContentType: contentType,
            Filename: $"expense-report-{req.Period.Start:yyyy-MM}.{req.OutputFormat}",
            Metadata: metadata
        );
    }

    private ReportResult GenerateRevenueReport(in ReportRequest req)
    {
        var revenue = _transactions.GetRevenue(req.Period, req.Departments);
        var byProduct = revenue.GroupBy(r => r.Product)
            .ToDictionary(g => g.Key, g => g.Sum(r => r.Amount));
        var byDepartment = revenue.GroupBy(r => r.Department)
            .ToDictionary(g => g.Key, g => g.Sum(r => r.Amount));

        var metadata = new ReportMetadata(
            TotalRecords: revenue.Count,
            TotalAmount: revenue.Sum(r => r.Amount),
            ByDepartment: byDepartment
        );

        var (content, contentType) = ExportReport(revenue, "Revenue Report", req.OutputFormat);

        return new ReportResult(
            Status: "Success",
            Content: content,
            ContentType: contentType,
            Filename: $"revenue-report-{req.Period.Start:yyyy-MM}.{req.OutputFormat}",
            Metadata: metadata
        );
    }

    private ReportResult GenerateBudgetVarianceReport(in ReportRequest req)
    {
        // Get actual spend and budget
        var actuals = _transactions.GetExpenses(req.Period, req.Departments);
        var budgets = _budget.GetBudgets(req.Period, req.Departments);

        // Calculate variances
        var variances = budgets.Select(b => new BudgetVariance
        {
            Department = b.Department,
            Category = b.Category,
            Budgeted = b.Amount,
            Actual = actuals.Where(a => a.Department == b.Department && a.Category == b.Category)
                           .Sum(a => a.Amount),
            Variance = b.Amount - actuals.Where(a => a.Department == b.Department && a.Category == b.Category)
                                        .Sum(a => a.Amount)
        }).ToList();

        var overBudget = variances.Where(v => v.Variance < 0);

        var (content, contentType) = ExportReport(variances, "Budget Variance Report", req.OutputFormat);

        return new ReportResult(
            Status: "Success",
            Content: content,
            ContentType: contentType,
            Filename: $"budget-variance-{req.Period.Start:yyyy-MM}.{req.OutputFormat}",
            Metadata: new ReportMetadata(
                TotalRecords: variances.Count,
                TotalAmount: variances.Sum(v => v.Variance),
                ByDepartment: variances.GroupBy(v => v.Department)
                    .ToDictionary(g => g.Key, g => g.Sum(v => v.Variance))
            )
        );
    }

    private ReportResult GenerateForecastReport(in ReportRequest req)
    {
        var historical = _transactions.GetAll(req.Period, req.Departments);
        var forecast = _forecast.Generate(historical, months: 6);

        var (content, contentType) = ExportReport(forecast, "Financial Forecast", req.OutputFormat);

        return new ReportResult(
            Status: "Success",
            Content: content,
            ContentType: contentType,
            Filename: $"forecast-{DateTime.Now:yyyy-MM}.{req.OutputFormat}",
            Metadata: new ReportMetadata(
                TotalRecords: forecast.Count,
                TotalAmount: forecast.Sum(f => f.PredictedAmount),
                ByDepartment: forecast.GroupBy(f => f.Department)
                    .ToDictionary(g => g.Key, g => g.Sum(f => f.PredictedAmount))
            )
        );
    }

    private ReportResult GenerateExecutiveSummary(in ReportRequest req)
    {
        // Aggregate all report types into executive summary
        var expenses = _transactions.GetExpenses(req.Period, req.Departments);
        var revenue = _transactions.GetRevenue(req.Period, req.Departments);
        var budgets = _budget.GetBudgets(req.Period, req.Departments);
        var forecast = _forecast.Generate(
            _transactions.GetAll(req.Period, req.Departments),
            months: 3);

        var summary = new ExecutiveSummary
        {
            Period = req.Period,
            TotalRevenue = revenue.Sum(r => r.Amount),
            TotalExpenses = expenses.Sum(e => e.Amount),
            NetIncome = revenue.Sum(r => r.Amount) - expenses.Sum(e => e.Amount),
            BudgetVariance = budgets.Sum(b => b.Amount) - expenses.Sum(e => e.Amount),
            ForecastedRevenue = forecast.Where(f => f.Type == "Revenue").Sum(f => f.PredictedAmount),
            TopExpenseCategories = expenses.GroupBy(e => e.Category)
                .OrderByDescending(g => g.Sum(e => e.Amount))
                .Take(5)
                .ToDictionary(g => g.Key, g => g.Sum(e => e.Amount))
        };

        var (content, contentType) = ExportReport(new[] { summary }, "Executive Summary", req.OutputFormat);

        return new ReportResult(
            Status: "Success",
            Content: content,
            ContentType: contentType,
            Filename: $"executive-summary-{req.Period.Start:yyyy-MM}.{req.OutputFormat}",
            Metadata: new ReportMetadata(
                TotalRecords: 1,
                TotalAmount: summary.NetIncome,
                ByDepartment: new Dictionary<string, decimal>
                {
                    ["Net Income"] = summary.NetIncome
                }
            )
        );
    }

    private (byte[] content, string contentType) ExportReport<T>(
        IEnumerable<T> data, string title, string format)
    {
        return format switch
        {
            "pdf" => (_export.ToPdf(data, title), "application/pdf"),
            "excel" => (_export.ToExcel(data), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
            "csv" => (_export.ToCsv(data), "text/csv"),
            _ => throw new ArgumentException($"Unsupported format: {format}")
        };
    }

    private string GenerateCacheKey(ReportRequest req) =>
        $"report:{req.ReportType}:{req.Period.Start:yyyyMMdd}:{req.Period.End:yyyyMMdd}:" +
        $"{string.Join(",", req.Departments)}:{req.OutputFormat}";
}

// Usage
var facade = new FinancialReportingFacade(transactions, budget, forecast, export, cache);

// Generate expense report
var expenseReport = facade.Generate(new ReportRequest(
    ReportType: "expense",
    Period: new DateRange(new DateTime(2024, 1, 1), new DateTime(2024, 12, 31)),
    Departments: new[] { "Engineering", "Marketing", "Sales" },
    OutputFormat: "pdf"
));

if (expenseReport.Status == "Success")
{
    File.WriteAllBytes(expenseReport.Filename!, expenseReport.Content!);
    Console.WriteLine($"Generated {expenseReport.Filename}");
    Console.WriteLine($"Total: {expenseReport.Metadata!.TotalAmount:C}");
}

// Generate executive summary
var summary = facade.Generate(new ReportRequest(
    ReportType: "executive-summary",
    Period: new DateRange(new DateTime(2024, 1, 1), new DateTime(2024, 12, 31)),
    Departments: new[] { "All" },
    OutputFormat: "pdf"
));

Why This Pattern

The facade hides complex aggregation logic spanning multiple services. Report generation involves transactions, budgets, forecasts, and export formatting - all coordinated seamlessly behind simple operation names.


See Also