🧩

Structural Design Patterns

Design Patterns Intermediate 11 min read 2000 words
Design Patterns

Structural Design Patterns

Structural patterns deal with object composition, creating relationships between objects to form larger structures while keeping them flexible and efficient.


Adapter

Intent: Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.

πŸ“– Theory & When to Use

When to Use

  • You want to use an existing class but its interface doesn’t match what you need
  • You need to create a reusable class that cooperates with unrelated classes
  • You need to use several existing subclasses but it’s impractical to adapt each one

Real-World Analogy

A power adapter: European devices have different plugs than US outlets. An adapter converts the plug interface so European devices can work with US outlets.

Key Participants

  • Target: The interface the client expects
  • Adapter: Adapts the Adaptee to the Target interface
  • Adaptee: The existing interface that needs adapting
  • Client: Collaborates with objects conforming to Target

Two Variants

  • Object Adapter: Uses composition (preferred in C#)
  • Class Adapter: Uses multiple inheritance (not possible in C#)
πŸ“Š UML Diagram
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Client    │──────>β”‚   <<interface>> β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚     ITarget     β”‚
                      β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                      β”‚ + Request()     β”‚
                      β””β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚ implements
                      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                      β”‚     Adapter     │──────>β”‚   Adaptee   β”‚
                      β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                      β”‚ - adaptee       β”‚       β”‚ + SpecificRequest()
                      β”‚ + Request()     β”‚       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’» Short Example (~20 lines)
// Target interface
public interface ITarget
{
    string GetRequest();
}

// Adaptee (existing class with incompatible interface)
public class Adaptee
{
    public string GetSpecificRequest() => "Specific request from Adaptee";
}

// Adapter
public class Adapter : ITarget
{
    private readonly Adaptee _adaptee;
    public Adapter(Adaptee adaptee) => _adaptee = adaptee;
    public string GetRequest() => _adaptee.GetSpecificRequest();
}

// Usage
ITarget target = new Adapter(new Adaptee());
Console.WriteLine(target.GetRequest());
πŸ’» Medium Example (~50 lines)
// Third-party library (Adaptee) - cannot modify
public class ThirdPartyBillingSystem
{
    public void ProcessPayment(string orderId, double amount, string currency)
    {
        Console.WriteLine($"Processing ${amount} {currency} for order {orderId}");
    }
}

// Our expected interface (Target)
public interface IPaymentProcessor
{
    void Pay(PaymentRequest request);
}

public record PaymentRequest(
    string OrderId,
    decimal Amount,
    string CurrencyCode,
    string CustomerEmail
);

// Adapter
public class BillingAdapter : IPaymentProcessor
{
    private readonly ThirdPartyBillingSystem _billingSystem;

    public BillingAdapter(ThirdPartyBillingSystem billingSystem)
    {
        _billingSystem = billingSystem;
    }

    public void Pay(PaymentRequest request)
    {
        // Adapt our interface to the third-party interface
        _billingSystem.ProcessPayment(
            request.OrderId,
            (double)request.Amount,  // decimal to double
            request.CurrencyCode
        );
    }
}

// Client code works with our interface
public class OrderService
{
    private readonly IPaymentProcessor _paymentProcessor;

    public OrderService(IPaymentProcessor paymentProcessor)
    {
        _paymentProcessor = paymentProcessor;
    }

    public void ProcessOrder(string orderId, decimal amount)
    {
        var request = new PaymentRequest(orderId, amount, "USD", "customer@example.com");
        _paymentProcessor.Pay(request);
    }
}

// Usage
var billing = new ThirdPartyBillingSystem();
var adapter = new BillingAdapter(billing);
var orderService = new OrderService(adapter);
orderService.ProcessOrder("ORD-123", 99.99m);
πŸ’» Production-Grade Example
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Text.Json;

// Legacy XML-based weather service (Adaptee)
public class LegacyWeatherService
{
    public string GetWeatherXml(string city)
    {
        // Simulates XML response from legacy service
        return $@"<weather>
            <city>{city}</city>
            <temperature>72</temperature>
            <unit>fahrenheit</unit>
            <conditions>sunny</conditions>
        </weather>";
    }
}

// Modern interface our app expects (Target)
public interface IWeatherService
{
    Task<WeatherData?> GetWeatherAsync(string city, CancellationToken ct = default);
}

public record WeatherData(
    string City,
    double TemperatureCelsius,
    string Conditions,
    DateTime RetrievedAt
);

// Modern external API service (another potential implementation)
public class ModernWeatherApi : IWeatherService
{
    private readonly HttpClient _httpClient;

    public ModernWeatherApi(HttpClient httpClient) => _httpClient = httpClient;

    public async Task<WeatherData?> GetWeatherAsync(string city, CancellationToken ct = default)
    {
        var response = await _httpClient.GetAsync($"/weather/{city}", ct);
        if (!response.IsSuccessStatusCode) return null;

        var json = await response.Content.ReadAsStringAsync(ct);
        return JsonSerializer.Deserialize<WeatherData>(json);
    }
}

// Adapter for legacy service
public class LegacyWeatherAdapter : IWeatherService
{
    private readonly LegacyWeatherService _legacyService;
    private readonly ILogger<LegacyWeatherAdapter> _logger;

    public LegacyWeatherAdapter(LegacyWeatherService legacyService, ILogger<LegacyWeatherAdapter> logger)
    {
        _legacyService = legacyService;
        _logger = logger;
    }

    public Task<WeatherData?> GetWeatherAsync(string city, CancellationToken ct = default)
    {
        try
        {
            var xml = _legacyService.GetWeatherXml(city);
            var data = ParseXml(xml);
            return Task.FromResult<WeatherData?>(data);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to get weather for {City} from legacy service", city);
            return Task.FromResult<WeatherData?>(null);
        }
    }

    private WeatherData ParseXml(string xml)
    {
        // Simple parsing - in production use XDocument or XmlSerializer
        var doc = System.Xml.Linq.XDocument.Parse(xml);
        var weather = doc.Root!;

        var city = weather.Element("city")!.Value;
        var tempF = double.Parse(weather.Element("temperature")!.Value);
        var conditions = weather.Element("conditions")!.Value;

        return new WeatherData(
            City: city,
            TemperatureCelsius: FahrenheitToCelsius(tempF),
            Conditions: conditions,
            RetrievedAt: DateTime.UtcNow
        );
    }

    private static double FahrenheitToCelsius(double fahrenheit)
        => Math.Round((fahrenheit - 32) * 5 / 9, 1);
}

// Caching adapter decorator (combining Adapter + Decorator)
public class CachingWeatherAdapter : IWeatherService
{
    private readonly IWeatherService _inner;
    private readonly IMemoryCache _cache;
    private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(15);

    public CachingWeatherAdapter(IWeatherService inner, IMemoryCache cache)
    {
        _inner = inner;
        _cache = cache;
    }

    public async Task<WeatherData?> GetWeatherAsync(string city, CancellationToken ct = default)
    {
        var cacheKey = $"weather:{city.ToLowerInvariant()}";

        if (_cache.TryGetValue(cacheKey, out WeatherData? cached))
            return cached;

        var data = await _inner.GetWeatherAsync(city, ct);

        if (data != null)
        {
            _cache.Set(cacheKey, data, _cacheDuration);
        }

        return data;
    }
}

// DI Registration with fallback strategy
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddWeatherServices(
        this IServiceCollection services,
        bool useLegacyService = false)
    {
        services.AddMemoryCache();

        if (useLegacyService)
        {
            services.AddSingleton<LegacyWeatherService>();
            services.AddSingleton<IWeatherService>(sp =>
            {
                var legacy = sp.GetRequiredService<LegacyWeatherService>();
                var logger = sp.GetRequiredService<ILogger<LegacyWeatherAdapter>>();
                var cache = sp.GetRequiredService<IMemoryCache>();

                var adapter = new LegacyWeatherAdapter(legacy, logger);
                return new CachingWeatherAdapter(adapter, cache);
            });
        }
        else
        {
            services.AddHttpClient<IWeatherService, ModernWeatherApi>(client =>
            {
                client.BaseAddress = new Uri("https://api.weather.example.com");
            });
        }

        return services;
    }
}

// Usage in application
public class WeatherController
{
    private readonly IWeatherService _weatherService;

    public WeatherController(IWeatherService weatherService) => _weatherService = weatherService;

    public async Task<string> GetWeather(string city)
    {
        var weather = await _weatherService.GetWeatherAsync(city);
        return weather != null
            ? $"{weather.City}: {weather.TemperatureCelsius}Β°C, {weather.Conditions}"
            : "Weather data unavailable";
    }
}
❓ Interview Q&A

Q1: What’s the difference between Adapter and Facade? A1: Adapter converts one interface to another expected by the client. Facade provides a simplified interface to a complex subsystem. Adapter works with single classes; Facade works with subsystems.

Q2: When would you use Adapter vs creating a new implementation? A2: Use Adapter when: the adaptee is third-party code you can’t modify, the adaptee is tested/stable and rewriting is risky, or you need to integrate multiple incompatible interfaces.

Q3: Can Adapter be used with async methods? A3: Yes. The adapter can wrap synchronous adaptee methods in Task.FromResult() or call them with Task.Run() to match async target interfaces.


Bridge

Intent: Decouple an abstraction from its implementation so that the two can vary independently.

πŸ“– Theory & When to Use

When to Use

  • You want to avoid permanent binding between abstraction and implementation
  • Both abstraction and implementation should be extensible via subclassing
  • Changes in implementation should not affect clients
  • You have a proliferation of classes from combining orthogonal dimensions

Real-World Analogy

Remote controls (abstraction) and devices (implementation): A universal remote can control any TV brand. The remote’s interface doesn’t change based on the TV’s internal implementation.

Key Participants

  • Abstraction: Defines the abstraction’s interface, maintains reference to Implementor
  • RefinedAbstraction: Extends the interface defined by Abstraction
  • Implementor: Interface for implementation classes
  • ConcreteImplementor: Implements the Implementor interface
πŸ“Š UML Diagram
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Abstraction   │────────>β”‚   <<interface>>     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€         β”‚    IImplementor     β”‚
β”‚ # implementor   β”‚         β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ + Operation()   β”‚         β”‚ + OperationImpl()   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚                             β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ RefinedAbstraction        β”‚ ConcreteImplementorAβ”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€         β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ + Operation()   β”‚         β”‚ + OperationImpl()   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’» Short Example (~20 lines)
// Implementor
public interface IRenderer
{
    void RenderCircle(float x, float y, float radius);
}

public class VectorRenderer : IRenderer
{
    public void RenderCircle(float x, float y, float radius) =>
        Console.WriteLine($"Drawing circle at ({x},{y}) with radius {radius} as vectors");
}

// Abstraction
public class Circle
{
    private readonly IRenderer _renderer;
    public float X { get; set; }
    public float Y { get; set; }
    public float Radius { get; set; }

    public Circle(IRenderer renderer) => _renderer = renderer;
    public void Draw() => _renderer.RenderCircle(X, Y, Radius);
}

// Usage
var circle = new Circle(new VectorRenderer()) { X = 5, Y = 10, Radius = 3 };
circle.Draw();
πŸ’» Medium Example (~50 lines)
// Implementor interface
public interface IMessageSender
{
    void Send(string recipient, string content);
}

// Concrete implementors
public class EmailSender : IMessageSender
{
    public void Send(string recipient, string content) =>
        Console.WriteLine($"Email to {recipient}: {content}");
}

public class SmsSender : IMessageSender
{
    public void Send(string recipient, string content) =>
        Console.WriteLine($"SMS to {recipient}: {content}");
}

public class PushNotificationSender : IMessageSender
{
    public void Send(string recipient, string content) =>
        Console.WriteLine($"Push to {recipient}: {content}");
}

// Abstraction
public abstract class Message
{
    protected readonly IMessageSender _sender;

    protected Message(IMessageSender sender) => _sender = sender;

    public abstract void Send(string recipient);
}

// Refined abstractions
public class TextMessage : Message
{
    public string Text { get; set; } = "";

    public TextMessage(IMessageSender sender) : base(sender) { }

    public override void Send(string recipient) =>
        _sender.Send(recipient, Text);
}

public class UrgentMessage : Message
{
    public string Text { get; set; } = "";

    public UrgentMessage(IMessageSender sender) : base(sender) { }

    public override void Send(string recipient) =>
        _sender.Send(recipient, $"[URGENT] {Text}");
}

// Usage - combine any message type with any sender
var urgentEmail = new UrgentMessage(new EmailSender()) { Text = "Server down!" };
urgentEmail.Send("admin@example.com");

var textSms = new TextMessage(new SmsSender()) { Text = "Meeting at 3pm" };
textSms.Send("+1234567890");
πŸ’» Production-Grade Example
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

// Implementor interface - data persistence strategies
public interface IDataStore
{
    Task<T?> GetAsync<T>(string key, CancellationToken ct = default) where T : class;
    Task SetAsync<T>(string key, T value, TimeSpan? expiry = null, CancellationToken ct = default) where T : class;
    Task DeleteAsync(string key, CancellationToken ct = default);
    Task<bool> ExistsAsync(string key, CancellationToken ct = default);
}

// Concrete implementors
public class InMemoryDataStore : IDataStore
{
    private readonly Dictionary<string, (object Value, DateTime? Expiry)> _store = new();
    private readonly object _lock = new();

    public Task<T?> GetAsync<T>(string key, CancellationToken ct = default) where T : class
    {
        lock (_lock)
        {
            if (_store.TryGetValue(key, out var entry))
            {
                if (entry.Expiry == null || entry.Expiry > DateTime.UtcNow)
                    return Task.FromResult(entry.Value as T);
                _store.Remove(key);
            }
            return Task.FromResult<T?>(null);
        }
    }

    public Task SetAsync<T>(string key, T value, TimeSpan? expiry = null, CancellationToken ct = default) where T : class
    {
        lock (_lock)
        {
            _store[key] = (value, expiry.HasValue ? DateTime.UtcNow + expiry : null);
        }
        return Task.CompletedTask;
    }

    public Task DeleteAsync(string key, CancellationToken ct = default)
    {
        lock (_lock) { _store.Remove(key); }
        return Task.CompletedTask;
    }

    public Task<bool> ExistsAsync(string key, CancellationToken ct = default)
    {
        lock (_lock)
        {
            return Task.FromResult(_store.ContainsKey(key) &&
                (_store[key].Expiry == null || _store[key].Expiry > DateTime.UtcNow));
        }
    }
}

public class RedisDataStore : IDataStore
{
    private readonly ILogger<RedisDataStore> _logger;
    private readonly string _connectionString;

    public RedisDataStore(string connectionString, ILogger<RedisDataStore> logger)
    {
        _connectionString = connectionString;
        _logger = logger;
    }

    public async Task<T?> GetAsync<T>(string key, CancellationToken ct = default) where T : class
    {
        _logger.LogDebug("Redis GET: {Key}", key);
        await Task.Delay(5, ct); // Simulate network call
        return null; // Simplified
    }

    public async Task SetAsync<T>(string key, T value, TimeSpan? expiry = null, CancellationToken ct = default) where T : class
    {
        _logger.LogDebug("Redis SET: {Key}", key);
        await Task.Delay(5, ct);
    }

    public async Task DeleteAsync(string key, CancellationToken ct = default)
    {
        _logger.LogDebug("Redis DEL: {Key}", key);
        await Task.Delay(5, ct);
    }

    public async Task<bool> ExistsAsync(string key, CancellationToken ct = default)
    {
        await Task.Delay(5, ct);
        return false;
    }
}

// Abstraction - cache strategies
public abstract class Cache
{
    protected readonly IDataStore DataStore;
    protected readonly ILogger Logger;
    protected readonly string Prefix;

    protected Cache(IDataStore dataStore, ILogger logger, string prefix)
    {
        DataStore = dataStore;
        Logger = logger;
        Prefix = prefix;
    }

    protected string BuildKey(string key) => $"{Prefix}:{key}";

    public abstract Task<T?> GetOrSetAsync<T>(
        string key,
        Func<Task<T>> factory,
        CancellationToken ct = default) where T : class;
}

// Refined abstraction - standard cache
public class StandardCache : Cache
{
    private readonly TimeSpan _defaultExpiry;

    public StandardCache(IDataStore dataStore, ILogger<StandardCache> logger, TimeSpan? defaultExpiry = null)
        : base(dataStore, logger, "cache")
    {
        _defaultExpiry = defaultExpiry ?? TimeSpan.FromMinutes(5);
    }

    public override async Task<T?> GetOrSetAsync<T>(
        string key,
        Func<Task<T>> factory,
        CancellationToken ct = default) where T : class
    {
        var cacheKey = BuildKey(key);

        var cached = await DataStore.GetAsync<T>(cacheKey, ct);
        if (cached != null)
        {
            Logger.LogDebug("Cache hit: {Key}", key);
            return cached;
        }

        Logger.LogDebug("Cache miss: {Key}", key);
        var value = await factory();

        if (value != null)
        {
            await DataStore.SetAsync(cacheKey, value, _defaultExpiry, ct);
        }

        return value;
    }
}

// Refined abstraction - sliding expiry cache
public class SlidingCache : Cache
{
    private readonly TimeSpan _slidingExpiry;

    public SlidingCache(IDataStore dataStore, ILogger<SlidingCache> logger, TimeSpan slidingExpiry)
        : base(dataStore, logger, "sliding")
    {
        _slidingExpiry = slidingExpiry;
    }

    public override async Task<T?> GetOrSetAsync<T>(
        string key,
        Func<Task<T>> factory,
        CancellationToken ct = default) where T : class
    {
        var cacheKey = BuildKey(key);

        var cached = await DataStore.GetAsync<T>(cacheKey, ct);
        if (cached != null)
        {
            // Refresh expiry on access
            await DataStore.SetAsync(cacheKey, cached, _slidingExpiry, ct);
            Logger.LogDebug("Sliding cache hit, refreshed: {Key}", key);
            return cached;
        }

        var value = await factory();
        if (value != null)
        {
            await DataStore.SetAsync(cacheKey, value, _slidingExpiry, ct);
        }

        return value;
    }
}

// Refined abstraction - write-through cache
public class WriteThroughCache<TEntity> : Cache where TEntity : class
{
    private readonly Func<string, Task<TEntity?>> _loadFromDb;
    private readonly Func<TEntity, Task> _saveToDb;

    public WriteThroughCache(
        IDataStore dataStore,
        ILogger<WriteThroughCache<TEntity>> logger,
        Func<string, Task<TEntity?>> loadFromDb,
        Func<TEntity, Task> saveToDb)
        : base(dataStore, logger, "wt")
    {
        _loadFromDb = loadFromDb;
        _saveToDb = saveToDb;
    }

    public override async Task<T?> GetOrSetAsync<T>(
        string key,
        Func<Task<T>> factory,
        CancellationToken ct = default) where T : class
    {
        var cacheKey = BuildKey(key);

        var cached = await DataStore.GetAsync<T>(cacheKey, ct);
        if (cached != null) return cached;

        var value = await factory();
        if (value != null)
        {
            await DataStore.SetAsync(cacheKey, value, null, ct);
        }
        return value;
    }

    public async Task SetAsync(string key, TEntity entity, CancellationToken ct = default)
    {
        // Write through to both DB and cache
        await _saveToDb(entity);
        await DataStore.SetAsync(BuildKey(key), entity, null, ct);
        Logger.LogDebug("Write-through: {Key}", key);
    }
}

// DI Registration - compose cache strategy with storage implementation
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddCaching(this IServiceCollection services, bool useRedis = false)
    {
        if (useRedis)
        {
            services.AddSingleton<IDataStore>(sp =>
                new RedisDataStore("localhost:6379", sp.GetRequiredService<ILogger<RedisDataStore>>()));
        }
        else
        {
            services.AddSingleton<IDataStore, InMemoryDataStore>();
        }

        services.AddSingleton<StandardCache>();
        services.AddSingleton(sp => new SlidingCache(
            sp.GetRequiredService<IDataStore>(),
            sp.GetRequiredService<ILogger<SlidingCache>>(),
            TimeSpan.FromMinutes(10)));

        return services;
    }
}
❓ Interview Q&A

Q1: What problem does Bridge solve that inheritance can’t? A1: Bridge avoids class explosion from combining multiple dimensions. With 3 message types and 3 senders, inheritance creates 9 classes. Bridge creates 6 (3+3) that can be combined at runtime.

Q2: How is Bridge different from Strategy? A2: Bridge separates abstraction from implementation as a structural concern. Strategy encapsulates interchangeable algorithms as a behavioral concern. Bridge is about structure; Strategy is about varying behavior.

Q3: When should you prefer Bridge over inheritance? A3: When you have two orthogonal dimensions of variation, when you need runtime flexibility in combining abstractions with implementations, or when changes in implementation shouldn’t require recompilation of clients.


Composite

Intent: Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions uniformly.

πŸ“– Theory & When to Use

When to Use

  • You need to represent part-whole hierarchies
  • Clients should treat individual objects and compositions uniformly
  • You want to ignore the difference between compositions and individual objects

Real-World Analogy

File system: folders can contain files or other folders. You can perform operations (copy, delete, get size) on both files and folders the same way.

Key Participants

  • Component: Interface for all objects in the composition
  • Leaf: Represents leaf objects with no children
  • Composite: Defines behavior for components having children
πŸ“Š UML Diagram
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   <<interface>>     β”‚
β”‚     IComponent      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ + Operation()       β”‚
β”‚ + Add(IComponent)   β”‚
β”‚ + Remove(IComponent)β”‚
β”‚ + GetChild(int)     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
     β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”
     β”‚           β”‚
β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Leaf   β”‚ β”‚  Composite  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚+ Operation() β”‚- children   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚+ Operation()β”‚
            β”‚+ Add()      β”‚
            β”‚+ Remove()   β”‚
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’» Short Example (~20 lines)
public interface IComponent
{
    string Name { get; }
    decimal GetPrice();
}

public class Product : IComponent
{
    public string Name { get; }
    public decimal Price { get; }
    public Product(string name, decimal price) => (Name, Price) = (name, price);
    public decimal GetPrice() => Price;
}

public class Box : IComponent
{
    public string Name { get; }
    private readonly List<IComponent> _items = new();
    public Box(string name) => Name = name;
    public void Add(IComponent item) => _items.Add(item);
    public decimal GetPrice() => _items.Sum(i => i.GetPrice());
}

// Usage
var box = new Box("Gift Box");
box.Add(new Product("Book", 15.99m));
box.Add(new Product("Pen", 2.50m));
Console.WriteLine($"Total: ${box.GetPrice()}"); // Total: $18.49
πŸ’» Medium Example (~50 lines)
// Component
public interface IFileSystemItem
{
    string Name { get; }
    long GetSize();
    void Display(string indent = "");
}

// Leaf
public class File : IFileSystemItem
{
    public string Name { get; }
    public long Size { get; }

    public File(string name, long size) => (Name, Size) = (name, size);

    public long GetSize() => Size;

    public void Display(string indent = "") =>
        Console.WriteLine($"{indent}πŸ“„ {Name} ({Size} bytes)");
}

// Composite
public class Directory : IFileSystemItem
{
    public string Name { get; }
    private readonly List<IFileSystemItem> _items = new();

    public Directory(string name) => Name = name;

    public void Add(IFileSystemItem item) => _items.Add(item);
    public void Remove(IFileSystemItem item) => _items.Remove(item);

    public long GetSize() => _items.Sum(item => item.GetSize());

    public void Display(string indent = "")
    {
        Console.WriteLine($"{indent}πŸ“ {Name}/ ({GetSize()} bytes)");
        foreach (var item in _items)
        {
            item.Display(indent + "  ");
        }
    }
}

// Usage
var root = new Directory("root");
var docs = new Directory("documents");
docs.Add(new File("resume.pdf", 102400));
docs.Add(new File("cover.docx", 25600));

var pics = new Directory("pictures");
pics.Add(new File("photo.jpg", 2048000));

root.Add(docs);
root.Add(pics);
root.Add(new File("readme.txt", 1024));

root.Display();
// πŸ“ root/ (2177024 bytes)
//   πŸ“ documents/ (128000 bytes)
//     πŸ“„ resume.pdf (102400 bytes)
//     πŸ“„ cover.docx (25600 bytes)
//   πŸ“ pictures/ (2048000 bytes)
//     πŸ“„ photo.jpg (2048000 bytes)
//   πŸ“„ readme.txt (1024 bytes)
πŸ’» Production-Grade Example
using System.Text.Json.Serialization;

// Component interface with visitor support
public interface IMenuComponent
{
    string Id { get; }
    string Name { get; }
    decimal Price { get; }
    bool IsAvailable { get; set; }

    void Accept(IMenuVisitor visitor);
    IEnumerable<IMenuComponent> GetItems();
}

// Visitor interface for operations
public interface IMenuVisitor
{
    void Visit(MenuItem item);
    void Visit(MenuCategory category);
    void Visit(ComboMeal combo);
}

// Leaf - individual menu item
public class MenuItem : IMenuComponent
{
    public string Id { get; }
    public string Name { get; }
    public decimal Price { get; }
    public bool IsAvailable { get; set; } = true;
    public string Description { get; init; } = "";
    public List<string> Allergens { get; init; } = new();
    public int CalorieCount { get; init; }

    public MenuItem(string id, string name, decimal price)
    {
        Id = id;
        Name = name;
        Price = price;
    }

    public void Accept(IMenuVisitor visitor) => visitor.Visit(this);
    public IEnumerable<IMenuComponent> GetItems() => Enumerable.Empty<IMenuComponent>();
}

// Composite - category containing items or subcategories
public class MenuCategory : IMenuComponent
{
    public string Id { get; }
    public string Name { get; }
    public bool IsAvailable { get; set; } = true;
    public string IconUrl { get; init; } = "";

    private readonly List<IMenuComponent> _items = new();

    public MenuCategory(string id, string name)
    {
        Id = id;
        Name = name;
    }

    public decimal Price => 0; // Categories don't have a direct price

    public void Add(IMenuComponent component) => _items.Add(component);
    public void Remove(IMenuComponent component) => _items.Remove(component);

    public void Accept(IMenuVisitor visitor)
    {
        visitor.Visit(this);
        foreach (var item in _items.Where(i => i.IsAvailable))
        {
            item.Accept(visitor);
        }
    }

    public IEnumerable<IMenuComponent> GetItems() => _items.Where(i => i.IsAvailable);
}

// Composite - combo meal (items with discount)
public class ComboMeal : IMenuComponent
{
    public string Id { get; }
    public string Name { get; }
    public bool IsAvailable { get; set; } = true;
    public decimal DiscountPercentage { get; init; } = 10;

    private readonly List<MenuItem> _items = new();

    public ComboMeal(string id, string name)
    {
        Id = id;
        Name = name;
    }

    public decimal Price
    {
        get
        {
            var totalPrice = _items.Where(i => i.IsAvailable).Sum(i => i.Price);
            return totalPrice * (1 - DiscountPercentage / 100);
        }
    }

    public void Add(MenuItem item) => _items.Add(item);

    public void Accept(IMenuVisitor visitor) => visitor.Visit(this);
    public IEnumerable<IMenuComponent> GetItems() => _items.Where(i => i.IsAvailable);
}

// Concrete visitors
public class PriceCalculatorVisitor : IMenuVisitor
{
    public decimal TotalPrice { get; private set; }

    public void Visit(MenuItem item)
    {
        if (item.IsAvailable)
            TotalPrice += item.Price;
    }

    public void Visit(MenuCategory category) { } // Categories don't add to price directly

    public void Visit(ComboMeal combo)
    {
        if (combo.IsAvailable)
            TotalPrice += combo.Price;
    }
}

public class AllergenFinderVisitor : IMenuVisitor
{
    private readonly string _allergen;
    public List<MenuItem> ItemsWithAllergen { get; } = new();

    public AllergenFinderVisitor(string allergen) => _allergen = allergen;

    public void Visit(MenuItem item)
    {
        if (item.Allergens.Contains(_allergen, StringComparer.OrdinalIgnoreCase))
            ItemsWithAllergen.Add(item);
    }

    public void Visit(MenuCategory category) { }
    public void Visit(ComboMeal combo) { }
}

public class MenuPrinterVisitor : IMenuVisitor
{
    private int _depth = 0;
    private readonly List<string> _lines = new();

    public string GetOutput() => string.Join(Environment.NewLine, _lines);

    public void Visit(MenuItem item)
    {
        var indent = new string(' ', _depth * 2);
        var status = item.IsAvailable ? "" : " [UNAVAILABLE]";
        _lines.Add($"{indent}- {item.Name}: ${item.Price:F2}{status}");
    }

    public void Visit(MenuCategory category)
    {
        var indent = new string(' ', _depth * 2);
        _lines.Add($"{indent}πŸ“‚ {category.Name}");
        _depth++;
    }

    public void Visit(ComboMeal combo)
    {
        var indent = new string(' ', _depth * 2);
        var items = string.Join(" + ", combo.GetItems().Select(i => i.Name));
        _lines.Add($"{indent}🍱 {combo.Name} (${combo.Price:F2}): {items}");
    }
}

// Menu service
public class MenuService
{
    private readonly MenuCategory _rootMenu;

    public MenuService()
    {
        _rootMenu = BuildMenu();
    }

    private MenuCategory BuildMenu()
    {
        var menu = new MenuCategory("root", "Full Menu");

        // Burgers category
        var burgers = new MenuCategory("burgers", "Burgers");
        burgers.Add(new MenuItem("b1", "Classic Burger", 8.99m)
        {
            Description = "Beef patty with lettuce, tomato, onion",
            Allergens = new List<string> { "Gluten", "Dairy" },
            CalorieCount = 650
        });
        burgers.Add(new MenuItem("b2", "Cheese Burger", 9.99m)
        {
            Allergens = new List<string> { "Gluten", "Dairy" },
            CalorieCount = 750
        });

        // Sides category
        var sides = new MenuCategory("sides", "Sides");
        sides.Add(new MenuItem("s1", "French Fries", 3.99m) { CalorieCount = 400 });
        sides.Add(new MenuItem("s2", "Onion Rings", 4.99m)
        {
            Allergens = new List<string> { "Gluten" },
            CalorieCount = 450
        });

        // Drinks category
        var drinks = new MenuCategory("drinks", "Drinks");
        drinks.Add(new MenuItem("d1", "Soda", 2.49m) { CalorieCount = 150 });
        drinks.Add(new MenuItem("d2", "Milkshake", 4.99m)
        {
            Allergens = new List<string> { "Dairy" },
            CalorieCount = 500
        });

        // Combo meal
        var combo = new ComboMeal("c1", "Burger Combo") { DiscountPercentage = 15 };
        combo.Add(new MenuItem("b1", "Classic Burger", 8.99m));
        combo.Add(new MenuItem("s1", "French Fries", 3.99m));
        combo.Add(new MenuItem("d1", "Soda", 2.49m));

        menu.Add(burgers);
        menu.Add(sides);
        menu.Add(drinks);
        menu.Add(combo);

        return menu;
    }

    public string PrintMenu()
    {
        var printer = new MenuPrinterVisitor();
        _rootMenu.Accept(printer);
        return printer.GetOutput();
    }

    public List<MenuItem> FindItemsWithAllergen(string allergen)
    {
        var finder = new AllergenFinderVisitor(allergen);
        _rootMenu.Accept(finder);
        return finder.ItemsWithAllergen;
    }

    public IMenuComponent? FindById(string id)
    {
        return FindByIdRecursive(_rootMenu, id);
    }

    private IMenuComponent? FindByIdRecursive(IMenuComponent component, string id)
    {
        if (component.Id == id) return component;

        foreach (var child in component.GetItems())
        {
            var found = FindByIdRecursive(child, id);
            if (found != null) return found;
        }

        return null;
    }
}
❓ Interview Q&A

Q1: What’s the main benefit of Composite pattern? A1: It lets clients treat individual objects and compositions uniformly. You can call the same methods on a leaf node or a tree of nodes without knowing which it is.

Q2: When should you NOT use Composite? A2: When leaf and composite behaviors differ significantly, when the tree structure is flat (no real hierarchy), or when type-specific operations are common (better to use explicit types).

Q3: How do you handle operations that only make sense for composites (like Add/Remove)? A3: Options: 1) Put them in base interface with default implementations, 2) Use separate interfaces, 3) Return null/throw for leaves. The choice depends on whether you prioritize uniformity or type safety.


Decorator

Intent: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

πŸ“– Theory & When to Use

When to Use

  • You need to add responsibilities to objects without affecting others
  • You want to add and remove responsibilities dynamically
  • Subclassing would result in too many classes
  • You need to wrap objects multiple times with different behaviors

Real-World Analogy

Coffee ordering: Start with basic coffee, then add decorators (milk, sugar, cream). Each addition wraps the previous, adding cost and description.

Key Participants

  • Component: Interface for objects that can have responsibilities added
  • ConcreteComponent: The object to which responsibilities can be added
  • Decorator: Maintains a reference to Component and conforms to its interface
  • ConcreteDecorator: Adds responsibilities to the component
πŸ“Š UML Diagram
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   <<interface>>    β”‚
β”‚     IComponent     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ + Operation()      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
    β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚                       β”‚
β”Œβ”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Concrete   β”‚    β”‚     Decorator     β”‚
β”‚ Component  β”‚    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”‚ - component       β”‚
β”‚+ Operation()    β”‚ + Operation()     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚ ConcreteDecorator β”‚
                  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                  β”‚ + Operation()     β”‚
                  β”‚ + AddedBehavior() β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’» Short Example (~20 lines)
public interface ICoffee
{
    string GetDescription();
    decimal GetCost();
}

public class SimpleCoffee : ICoffee
{
    public string GetDescription() => "Coffee";
    public decimal GetCost() => 2.00m;
}

public class MilkDecorator : ICoffee
{
    private readonly ICoffee _coffee;
    public MilkDecorator(ICoffee coffee) => _coffee = coffee;
    public string GetDescription() => _coffee.GetDescription() + ", Milk";
    public decimal GetCost() => _coffee.GetCost() + 0.50m;
}

// Usage
ICoffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
Console.WriteLine($"{coffee.GetDescription()}: ${coffee.GetCost()}"); // Coffee, Milk: $2.50
πŸ’» Medium Example (~50 lines)
// Component
public interface IDataSource
{
    void WriteData(string data);
    string ReadData();
}

// Concrete component
public class FileDataSource : IDataSource
{
    private readonly string _filename;
    private string _data = "";

    public FileDataSource(string filename) => _filename = filename;

    public void WriteData(string data)
    {
        _data = data;
        Console.WriteLine($"Writing to {_filename}: {data}");
    }

    public string ReadData()
    {
        Console.WriteLine($"Reading from {_filename}");
        return _data;
    }
}

// Base decorator
public abstract class DataSourceDecorator : IDataSource
{
    protected readonly IDataSource _wrappee;
    protected DataSourceDecorator(IDataSource source) => _wrappee = source;

    public virtual void WriteData(string data) => _wrappee.WriteData(data);
    public virtual string ReadData() => _wrappee.ReadData();
}

// Concrete decorators
public class EncryptionDecorator : DataSourceDecorator
{
    public EncryptionDecorator(IDataSource source) : base(source) { }

    public override void WriteData(string data)
    {
        var encrypted = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(data));
        Console.WriteLine($"Encrypting data...");
        base.WriteData(encrypted);
    }

    public override string ReadData()
    {
        var data = base.ReadData();
        Console.WriteLine($"Decrypting data...");
        return System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(data));
    }
}

public class CompressionDecorator : DataSourceDecorator
{
    public CompressionDecorator(IDataSource source) : base(source) { }

    public override void WriteData(string data)
    {
        Console.WriteLine($"Compressing data...");
        base.WriteData($"[compressed]{data}[/compressed]");
    }

    public override string ReadData()
    {
        var data = base.ReadData();
        Console.WriteLine($"Decompressing data...");
        return data.Replace("[compressed]", "").Replace("[/compressed]", "");
    }
}

// Usage - stack decorators
IDataSource source = new FileDataSource("data.txt");
source = new EncryptionDecorator(source);
source = new CompressionDecorator(source);

source.WriteData("Secret message");
var result = source.ReadData();
πŸ’» Production-Grade Example
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Diagnostics;

// Component interface
public interface IOrderProcessor
{
    Task<OrderResult> ProcessAsync(Order order, CancellationToken ct = default);
}

public record Order(string Id, string CustomerId, List<OrderItem> Items, decimal Total);
public record OrderItem(string ProductId, int Quantity, decimal UnitPrice);
public record OrderResult(bool Success, string OrderId, string? Message = null, string? TransactionId = null);

// Concrete component
public class OrderProcessor : IOrderProcessor
{
    private readonly ILogger<OrderProcessor> _logger;

    public OrderProcessor(ILogger<OrderProcessor> logger) => _logger = logger;

    public async Task<OrderResult> ProcessAsync(Order order, CancellationToken ct = default)
    {
        _logger.LogInformation("Processing order {OrderId}", order.Id);
        await Task.Delay(100, ct); // Simulate processing
        return new OrderResult(true, order.Id, TransactionId: Guid.NewGuid().ToString("N")[..8]);
    }
}

// Base decorator
public abstract class OrderProcessorDecorator : IOrderProcessor
{
    protected readonly IOrderProcessor Inner;
    protected OrderProcessorDecorator(IOrderProcessor inner) => Inner = inner;
    public virtual Task<OrderResult> ProcessAsync(Order order, CancellationToken ct = default)
        => Inner.ProcessAsync(order, ct);
}

// Logging decorator
public class LoggingOrderProcessor : OrderProcessorDecorator
{
    private readonly ILogger<LoggingOrderProcessor> _logger;

    public LoggingOrderProcessor(IOrderProcessor inner, ILogger<LoggingOrderProcessor> logger)
        : base(inner) => _logger = logger;

    public override async Task<OrderResult> ProcessAsync(Order order, CancellationToken ct = default)
    {
        var sw = Stopwatch.StartNew();
        _logger.LogInformation("Starting order processing: {OrderId}, Items: {ItemCount}, Total: {Total}",
            order.Id, order.Items.Count, order.Total);

        try
        {
            var result = await base.ProcessAsync(order, ct);
            sw.Stop();

            if (result.Success)
                _logger.LogInformation("Order {OrderId} processed successfully in {ElapsedMs}ms",
                    order.Id, sw.ElapsedMilliseconds);
            else
                _logger.LogWarning("Order {OrderId} failed: {Message}", order.Id, result.Message);

            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Order {OrderId} threw exception after {ElapsedMs}ms",
                order.Id, sw.ElapsedMilliseconds);
            throw;
        }
    }
}

// Validation decorator
public class ValidationOrderProcessor : OrderProcessorDecorator
{
    public ValidationOrderProcessor(IOrderProcessor inner) : base(inner) { }

    public override async Task<OrderResult> ProcessAsync(Order order, CancellationToken ct = default)
    {
        var validationErrors = ValidateOrder(order);
        if (validationErrors.Any())
        {
            return new OrderResult(false, order.Id,
                Message: $"Validation failed: {string.Join(", ", validationErrors)}");
        }

        return await base.ProcessAsync(order, ct);
    }

    private static List<string> ValidateOrder(Order order)
    {
        var errors = new List<string>();

        if (string.IsNullOrWhiteSpace(order.Id))
            errors.Add("Order ID is required");

        if (string.IsNullOrWhiteSpace(order.CustomerId))
            errors.Add("Customer ID is required");

        if (!order.Items.Any())
            errors.Add("Order must have at least one item");

        if (order.Total <= 0)
            errors.Add("Order total must be positive");

        foreach (var item in order.Items)
        {
            if (item.Quantity <= 0)
                errors.Add($"Invalid quantity for product {item.ProductId}");
        }

        return errors;
    }
}

// Retry decorator
public class RetryOrderProcessor : OrderProcessorDecorator
{
    private readonly int _maxRetries;
    private readonly TimeSpan _delay;
    private readonly ILogger<RetryOrderProcessor> _logger;

    public RetryOrderProcessor(
        IOrderProcessor inner,
        ILogger<RetryOrderProcessor> logger,
        int maxRetries = 3,
        TimeSpan? delay = null)
        : base(inner)
    {
        _logger = logger;
        _maxRetries = maxRetries;
        _delay = delay ?? TimeSpan.FromSeconds(1);
    }

    public override async Task<OrderResult> ProcessAsync(Order order, CancellationToken ct = default)
    {
        var attempt = 0;
        while (true)
        {
            try
            {
                attempt++;
                return await base.ProcessAsync(order, ct);
            }
            catch (Exception ex) when (attempt < _maxRetries && !ct.IsCancellationRequested)
            {
                _logger.LogWarning(ex, "Order {OrderId} attempt {Attempt} failed, retrying in {Delay}ms",
                    order.Id, attempt, _delay.TotalMilliseconds);
                await Task.Delay(_delay, ct);
            }
        }
    }
}

// Caching decorator (for idempotency)
public class IdempotentOrderProcessor : OrderProcessorDecorator
{
    private readonly IMemoryCache _cache;
    private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(5);

    public IdempotentOrderProcessor(IOrderProcessor inner, IMemoryCache cache) : base(inner)
    {
        _cache = cache;
    }

    public override async Task<OrderResult> ProcessAsync(Order order, CancellationToken ct = default)
    {
        var cacheKey = $"order:{order.Id}";

        if (_cache.TryGetValue(cacheKey, out OrderResult? cachedResult))
        {
            return cachedResult! with { Message = "Duplicate order - returning cached result" };
        }

        var result = await base.ProcessAsync(order, ct);

        if (result.Success)
        {
            _cache.Set(cacheKey, result, _cacheDuration);
        }

        return result;
    }
}

// Metrics decorator
public class MetricsOrderProcessor : OrderProcessorDecorator
{
    private static int _totalOrders;
    private static int _successfulOrders;
    private static int _failedOrders;
    private static long _totalProcessingTimeMs;

    public MetricsOrderProcessor(IOrderProcessor inner) : base(inner) { }

    public override async Task<OrderResult> ProcessAsync(Order order, CancellationToken ct = default)
    {
        Interlocked.Increment(ref _totalOrders);
        var sw = Stopwatch.StartNew();

        var result = await base.ProcessAsync(order, ct);

        sw.Stop();
        Interlocked.Add(ref _totalProcessingTimeMs, sw.ElapsedMilliseconds);

        if (result.Success)
            Interlocked.Increment(ref _successfulOrders);
        else
            Interlocked.Increment(ref _failedOrders);

        return result;
    }

    public static OrderMetrics GetMetrics() => new(
        TotalOrders: _totalOrders,
        SuccessfulOrders: _successfulOrders,
        FailedOrders: _failedOrders,
        AverageProcessingTimeMs: _totalOrders > 0 ? _totalProcessingTimeMs / _totalOrders : 0
    );
}

public record OrderMetrics(int TotalOrders, int SuccessfulOrders, int FailedOrders, long AverageProcessingTimeMs);

// DI Registration with decoration chain
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddOrderProcessing(this IServiceCollection services)
    {
        services.AddMemoryCache();

        // Register base processor
        services.AddScoped<OrderProcessor>();

        // Build decoration chain
        services.AddScoped<IOrderProcessor>(sp =>
        {
            IOrderProcessor processor = sp.GetRequiredService<OrderProcessor>();

            // Apply decorators in order (innermost to outermost)
            processor = new ValidationOrderProcessor(processor);
            processor = new IdempotentOrderProcessor(processor, sp.GetRequiredService<IMemoryCache>());
            processor = new RetryOrderProcessor(processor, sp.GetRequiredService<ILogger<RetryOrderProcessor>>());
            processor = new MetricsOrderProcessor(processor);
            processor = new LoggingOrderProcessor(processor, sp.GetRequiredService<ILogger<LoggingOrderProcessor>>());

            return processor;
        });

        return services;
    }
}
❓ Interview Q&A

Q1: How is Decorator different from inheritance? A1: Inheritance is static and adds behavior to all instances. Decorator adds behavior dynamically to individual objects. You can stack multiple decorators and add/remove them at runtime.

Q2: What’s a common use case for Decorator in .NET? A2: Stream classes (BufferedStream, GZipStream, CryptoStream wrapping FileStream), HTTP handlers in ASP.NET Core, and logging/caching wrappers around services.

Q3: Can a decorator change the interface of the component? A3: No, decorators should conform to the same interface. If you need to change the interface, that’s closer to Adapter pattern. Decorators add behavior transparently.

Q4: How do you handle decorator ordering? A4: Order matters! Usually: validation first, then retry/caching, then logging/metrics on the outside. In DI, build the chain explicitly or use libraries like Scrutor that support decoration.


Facade

Intent: Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.

πŸ“– Theory & When to Use

When to Use

  • You want to provide a simple interface to a complex subsystem
  • You want to layer your subsystems
  • There are many dependencies between clients and implementation classes

Real-World Analogy

A hotel concierge: instead of dealing with housekeeping, restaurant, transportation, and activities separately, you go to the concierge who handles everything for you.

Key Participants

  • Facade: Knows which subsystem classes are responsible for a request; delegates client requests to appropriate subsystem objects
  • Subsystem classes: Implement subsystem functionality; handle work assigned by the Facade; have no knowledge of the Facade
πŸ“Š UML Diagram
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Client    β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚             Facade                   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ + SimpleOperation()                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β”‚ uses
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β–Ό           β–Ό           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”
β”‚ SubA  β”‚  β”‚ SubB  β”‚  β”‚ SubC  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’» Short Example (~20 lines)
// Complex subsystem classes
public class CPU { public void Freeze() => Console.WriteLine("CPU frozen"); public void Execute() => Console.WriteLine("CPU executing"); }
public class Memory { public void Load(long address) => Console.WriteLine($"Memory loaded at {address}"); }
public class HardDrive { public byte[] Read(long lba, int size) { Console.WriteLine($"Reading {size} bytes"); return new byte[size]; } }

// Facade
public class ComputerFacade
{
    private readonly CPU _cpu = new();
    private readonly Memory _memory = new();
    private readonly HardDrive _hardDrive = new();

    public void Start()
    {
        _cpu.Freeze();
        _memory.Load(0x00);
        _hardDrive.Read(0, 1024);
        _cpu.Execute();
    }
}

// Usage - client only sees simple interface
var computer = new ComputerFacade();
computer.Start();
πŸ’» Medium Example (~50 lines)
// Subsystem classes
public class Inventory
{
    public bool CheckStock(string productId, int quantity) =>
        quantity <= 100; // Simplified
    public void Reserve(string productId, int quantity) =>
        Console.WriteLine($"Reserved {quantity} of {productId}");
}

public class Payment
{
    public bool ProcessPayment(string customerId, decimal amount)
    {
        Console.WriteLine($"Processing ${amount} for customer {customerId}");
        return true;
    }
    public string GetTransactionId() => Guid.NewGuid().ToString("N")[..8];
}

public class Shipping
{
    public string CreateShipment(string orderId, string address)
    {
        Console.WriteLine($"Creating shipment for {orderId} to {address}");
        return $"SHIP-{orderId}";
    }
}

public class Notification
{
    public void SendOrderConfirmation(string email, string orderId) =>
        Console.WriteLine($"Sent confirmation for {orderId} to {email}");
}

// Facade
public class OrderFacade
{
    private readonly Inventory _inventory = new();
    private readonly Payment _payment = new();
    private readonly Shipping _shipping = new();
    private readonly Notification _notification = new();

    public OrderResult PlaceOrder(string customerId, string productId, int quantity,
                                   decimal amount, string address, string email)
    {
        // Check inventory
        if (!_inventory.CheckStock(productId, quantity))
            return new OrderResult(false, null, "Out of stock");

        // Process payment
        if (!_payment.ProcessPayment(customerId, amount))
            return new OrderResult(false, null, "Payment failed");

        // Reserve inventory
        _inventory.Reserve(productId, quantity);

        // Create shipment
        var orderId = Guid.NewGuid().ToString("N")[..8];
        var trackingId = _shipping.CreateShipment(orderId, address);

        // Send notification
        _notification.SendOrderConfirmation(email, orderId);

        return new OrderResult(true, orderId, $"Tracking: {trackingId}");
    }
}

public record OrderResult(bool Success, string? OrderId, string? Message);

// Usage
var facade = new OrderFacade();
var result = facade.PlaceOrder("C123", "PROD-456", 2, 99.99m, "123 Main St", "customer@email.com");
πŸ’» Production-Grade Example
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

// Subsystem interfaces
public interface IUserRepository
{
    Task<User?> GetByIdAsync(string id, CancellationToken ct = default);
    Task<User?> GetByEmailAsync(string email, CancellationToken ct = default);
    Task CreateAsync(User user, CancellationToken ct = default);
    Task UpdateAsync(User user, CancellationToken ct = default);
}

public interface IPasswordHasher
{
    string Hash(string password);
    bool Verify(string password, string hash);
}

public interface ITokenService
{
    string GenerateAccessToken(User user);
    string GenerateRefreshToken();
    ClaimsPrincipal? ValidateToken(string token);
}

public interface IEmailService
{
    Task SendVerificationEmailAsync(string email, string token, CancellationToken ct = default);
    Task SendPasswordResetEmailAsync(string email, string token, CancellationToken ct = default);
    Task SendWelcomeEmailAsync(string email, string name, CancellationToken ct = default);
}

public interface IAuditService
{
    Task LogAsync(string action, string userId, object? details = null, CancellationToken ct = default);
}

// Domain model
public class User
{
    public string Id { get; set; } = "";
    public string Email { get; set; } = "";
    public string Name { get; set; } = "";
    public string PasswordHash { get; set; } = "";
    public bool EmailVerified { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? LastLoginAt { get; set; }
    public string? RefreshToken { get; set; }
    public DateTime? RefreshTokenExpiresAt { get; set; }
}

// Result types
public record AuthResult(bool Success, string? AccessToken = null, string? RefreshToken = null, string? Error = null);
public record RegistrationResult(bool Success, string? UserId = null, string? Error = null);

// Facade - simplifies authentication operations
public class AuthenticationFacade
{
    private readonly IUserRepository _userRepository;
    private readonly IPasswordHasher _passwordHasher;
    private readonly ITokenService _tokenService;
    private readonly IEmailService _emailService;
    private readonly IAuditService _auditService;
    private readonly ILogger<AuthenticationFacade> _logger;

    public AuthenticationFacade(
        IUserRepository userRepository,
        IPasswordHasher passwordHasher,
        ITokenService tokenService,
        IEmailService emailService,
        IAuditService auditService,
        ILogger<AuthenticationFacade> logger)
    {
        _userRepository = userRepository;
        _passwordHasher = passwordHasher;
        _tokenService = tokenService;
        _emailService = emailService;
        _auditService = auditService;
        _logger = logger;
    }

    public async Task<RegistrationResult> RegisterAsync(
        string email, string password, string name, CancellationToken ct = default)
    {
        try
        {
            // Check if user exists
            var existingUser = await _userRepository.GetByEmailAsync(email, ct);
            if (existingUser != null)
            {
                return new RegistrationResult(false, Error: "Email already registered");
            }

            // Create user
            var user = new User
            {
                Id = Guid.NewGuid().ToString(),
                Email = email,
                Name = name,
                PasswordHash = _passwordHasher.Hash(password),
                CreatedAt = DateTime.UtcNow
            };

            await _userRepository.CreateAsync(user, ct);

            // Generate verification token and send email
            var verificationToken = _tokenService.GenerateAccessToken(user);
            await _emailService.SendVerificationEmailAsync(email, verificationToken, ct);

            // Audit
            await _auditService.LogAsync("user_registered", user.Id, new { email }, ct);

            _logger.LogInformation("User registered: {Email}", email);
            return new RegistrationResult(true, user.Id);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Registration failed for {Email}", email);
            return new RegistrationResult(false, Error: "Registration failed");
        }
    }

    public async Task<AuthResult> LoginAsync(
        string email, string password, CancellationToken ct = default)
    {
        try
        {
            var user = await _userRepository.GetByEmailAsync(email, ct);
            if (user == null || !_passwordHasher.Verify(password, user.PasswordHash))
            {
                await _auditService.LogAsync("login_failed", email, new { reason = "invalid_credentials" }, ct);
                return new AuthResult(false, Error: "Invalid email or password");
            }

            if (!user.EmailVerified)
            {
                return new AuthResult(false, Error: "Email not verified");
            }

            // Generate tokens
            var accessToken = _tokenService.GenerateAccessToken(user);
            var refreshToken = _tokenService.GenerateRefreshToken();

            // Update user
            user.LastLoginAt = DateTime.UtcNow;
            user.RefreshToken = refreshToken;
            user.RefreshTokenExpiresAt = DateTime.UtcNow.AddDays(7);
            await _userRepository.UpdateAsync(user, ct);

            // Audit
            await _auditService.LogAsync("user_login", user.Id, ct: ct);

            _logger.LogInformation("User logged in: {Email}", email);
            return new AuthResult(true, accessToken, refreshToken);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Login failed for {Email}", email);
            return new AuthResult(false, Error: "Login failed");
        }
    }

    public async Task<AuthResult> RefreshTokenAsync(
        string refreshToken, CancellationToken ct = default)
    {
        try
        {
            // In production, query by refresh token
            // This is simplified
            return new AuthResult(false, Error: "Invalid refresh token");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Token refresh failed");
            return new AuthResult(false, Error: "Token refresh failed");
        }
    }

    public async Task<bool> ForgotPasswordAsync(
        string email, CancellationToken ct = default)
    {
        var user = await _userRepository.GetByEmailAsync(email, ct);
        if (user == null)
        {
            // Don't reveal whether email exists
            return true;
        }

        var resetToken = _tokenService.GenerateAccessToken(user);
        await _emailService.SendPasswordResetEmailAsync(email, resetToken, ct);
        await _auditService.LogAsync("password_reset_requested", user.Id, ct: ct);

        return true;
    }

    public async Task LogoutAsync(string userId, CancellationToken ct = default)
    {
        var user = await _userRepository.GetByIdAsync(userId, ct);
        if (user != null)
        {
            user.RefreshToken = null;
            user.RefreshTokenExpiresAt = null;
            await _userRepository.UpdateAsync(user, ct);
            await _auditService.LogAsync("user_logout", userId, ct: ct);
        }
    }
}

// DI Registration
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddAuthentication(this IServiceCollection services)
    {
        // Register subsystems
        services.AddScoped<IUserRepository, UserRepository>();
        services.AddSingleton<IPasswordHasher, BCryptPasswordHasher>();
        services.AddSingleton<ITokenService, JwtTokenService>();
        services.AddScoped<IEmailService, SmtpEmailService>();
        services.AddScoped<IAuditService, DatabaseAuditService>();

        // Register facade
        services.AddScoped<AuthenticationFacade>();

        return services;
    }
}

// Controller using facade
public class AuthController
{
    private readonly AuthenticationFacade _auth;

    public AuthController(AuthenticationFacade auth) => _auth = auth;

    public async Task<IActionResult> Register(RegisterRequest request)
    {
        var result = await _auth.RegisterAsync(request.Email, request.Password, request.Name);
        return result.Success
            ? Ok(new { UserId = result.UserId })
            : BadRequest(new { result.Error });
    }

    public async Task<IActionResult> Login(LoginRequest request)
    {
        var result = await _auth.LoginAsync(request.Email, request.Password);
        return result.Success
            ? Ok(new { result.AccessToken, result.RefreshToken })
            : Unauthorized(new { result.Error });
    }
}
❓ Interview Q&A

Q1: What’s the difference between Facade and Adapter? A1: Adapter makes one interface compatible with another (same functionality, different interface). Facade simplifies a complex subsystem into a single interface (reduced functionality, simpler interface).

Q2: Does Facade violate Single Responsibility Principle? A2: It can if it does too much. A well-designed Facade coordinates calls to subsystems but doesn’t implement business logic itself. It’s about simplification, not combination.

Q3: When should you use Facade vs directly using subsystems? A3: Use Facade for common operations that clients need frequently. Advanced clients can still access subsystems directly for specialized needs. Facade doesn’t hide subsystemsβ€”it provides a convenient alternative.


Flyweight

Intent: Use sharing to support large numbers of fine-grained objects efficiently.

πŸ“– Theory & When to Use

When to Use

  • An application uses a large number of objects
  • Storage costs are high due to object quantity
  • Most object state can be made extrinsic
  • Many groups of objects can be replaced by few shared objects
  • The application doesn’t depend on object identity

Key Concepts

  • Intrinsic state: Stored in the flyweight, shared, context-independent
  • Extrinsic state: Stored or computed by client, passed to flyweight methods

Real-World Analogy

Font characters in a word processor: instead of storing full formatting for each character, share the character shape and store position/color externally.

Key Participants

  • Flyweight: Interface through which flyweights receive and act on extrinsic state
  • ConcreteFlyweight: Stores intrinsic state, shareable
  • FlyweightFactory: Creates and manages flyweight objects, ensures sharing
πŸ“Š UML Diagram
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ FlyweightFactory  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ - flyweights: Map β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ + GetFlyweight()  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚ creates/returns
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   <<interface>>   β”‚
β”‚     IFlyweight    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ + Operation(      β”‚
β”‚    extrinsicState)β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ConcreteFlyweight β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ - intrinsicState  β”‚ (shared)
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ + Operation()     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’» Short Example (~20 lines)
// Flyweight - stores intrinsic state
public class CharacterStyle
{
    public string FontFamily { get; }
    public int FontSize { get; }
    public CharacterStyle(string fontFamily, int fontSize) => (FontFamily, FontSize) = (fontFamily, fontSize);
}

// Factory - ensures sharing
public class CharacterStyleFactory
{
    private readonly Dictionary<string, CharacterStyle> _styles = new();

    public CharacterStyle GetStyle(string fontFamily, int fontSize)
    {
        var key = $"{fontFamily}_{fontSize}";
        if (!_styles.ContainsKey(key))
            _styles[key] = new CharacterStyle(fontFamily, fontSize);
        return _styles[key];
    }

    public int StyleCount => _styles.Count;
}

// Usage
var factory = new CharacterStyleFactory();
var style1 = factory.GetStyle("Arial", 12);
var style2 = factory.GetStyle("Arial", 12); // Same instance
Console.WriteLine(ReferenceEquals(style1, style2)); // True
πŸ’» Medium Example (~50 lines)
// Flyweight - tree type (intrinsic state)
public class TreeType
{
    public string Name { get; }
    public string Color { get; }
    public string Texture { get; } // Could be large texture data

    public TreeType(string name, string color, string texture)
    {
        Name = name;
        Color = color;
        Texture = texture;
    }

    public void Draw(int x, int y)
    {
        Console.WriteLine($"Drawing {Name} tree ({Color}) at ({x}, {y})");
    }
}

// Flyweight factory
public class TreeTypeFactory
{
    private readonly Dictionary<string, TreeType> _treeTypes = new();

    public TreeType GetTreeType(string name, string color, string texture)
    {
        var key = $"{name}_{color}_{texture}";
        if (!_treeTypes.TryGetValue(key, out var treeType))
        {
            treeType = new TreeType(name, color, texture);
            _treeTypes[key] = treeType;
            Console.WriteLine($"Created new TreeType: {name}");
        }
        return treeType;
    }
}

// Context - tree instance (extrinsic state)
public class Tree
{
    public int X { get; }
    public int Y { get; }
    public TreeType Type { get; }

    public Tree(int x, int y, TreeType type) => (X, Y, Type) = (x, y, type);

    public void Draw() => Type.Draw(X, Y);
}

// Forest using flyweights
public class Forest
{
    private readonly List<Tree> _trees = new();
    private readonly TreeTypeFactory _factory = new();

    public void PlantTree(int x, int y, string name, string color, string texture)
    {
        var type = _factory.GetTreeType(name, color, texture);
        _trees.Add(new Tree(x, y, type));
    }

    public void Draw()
    {
        foreach (var tree in _trees)
            tree.Draw();
    }
}

// Usage - 1000 trees but only ~5 TreeType objects
var forest = new Forest();
var random = new Random();
for (int i = 0; i < 1000; i++)
{
    forest.PlantTree(
        random.Next(100), random.Next(100),
        random.Next(2) == 0 ? "Oak" : "Pine",
        random.Next(2) == 0 ? "Green" : "DarkGreen",
        "StandardTexture"
    );
}
forest.Draw();
πŸ’» Production-Grade Example
using System.Collections.Concurrent;
using Microsoft.Extensions.Caching.Memory;

// Flyweight - compiled regex patterns (expensive to create)
public class RegexPattern
{
    public string Pattern { get; }
    public Regex CompiledRegex { get; }
    public DateTime CreatedAt { get; }

    public RegexPattern(string pattern, RegexOptions options = RegexOptions.Compiled)
    {
        Pattern = pattern;
        CompiledRegex = new Regex(pattern, options | RegexOptions.Compiled);
        CreatedAt = DateTime.UtcNow;
    }

    public bool IsMatch(string input) => CompiledRegex.IsMatch(input);
    public Match Match(string input) => CompiledRegex.Match(input);
    public MatchCollection Matches(string input) => CompiledRegex.Matches(input);
}

// Flyweight factory with bounded cache
public class RegexPatternFactory
{
    private readonly ConcurrentDictionary<string, Lazy<RegexPattern>> _patterns = new();
    private readonly int _maxPatterns;
    private readonly ILogger<RegexPatternFactory>? _logger;

    public RegexPatternFactory(int maxPatterns = 1000, ILogger<RegexPatternFactory>? logger = null)
    {
        _maxPatterns = maxPatterns;
        _logger = logger;
    }

    public RegexPattern GetPattern(string pattern, RegexOptions options = RegexOptions.None)
    {
        var key = $"{pattern}_{(int)options}";

        var lazyPattern = _patterns.GetOrAdd(key, _ =>
        {
            _logger?.LogDebug("Creating new compiled regex: {Pattern}", pattern);
            return new Lazy<RegexPattern>(() => new RegexPattern(pattern, options));
        });

        // Evict oldest if over limit
        if (_patterns.Count > _maxPatterns)
        {
            EvictOldest();
        }

        return lazyPattern.Value;
    }

    private void EvictOldest()
    {
        var oldest = _patterns
            .Where(kvp => kvp.Value.IsValueCreated)
            .OrderBy(kvp => kvp.Value.Value.CreatedAt)
            .Take(_patterns.Count - _maxPatterns + 100)
            .ToList();

        foreach (var item in oldest)
        {
            _patterns.TryRemove(item.Key, out _);
            _logger?.LogDebug("Evicted regex pattern: {Key}", item.Key);
        }
    }

    public int CachedPatternCount => _patterns.Count;
}

// Flyweight for string interning (production scenario)
public class StringPool
{
    private readonly ConcurrentDictionary<string, string> _pool = new();
    private readonly int _maxLength;

    public StringPool(int maxStringLength = 100)
    {
        _maxLength = maxStringLength;
    }

    public string Intern(string value)
    {
        if (string.IsNullOrEmpty(value) || value.Length > _maxLength)
            return value;

        return _pool.GetOrAdd(value, v => v);
    }

    public int PoolSize => _pool.Count;
    public long EstimatedMemorySaved => _pool.Sum(kvp => kvp.Key.Length * sizeof(char));
}

// Flyweight for immutable configuration objects
public record DatabaseConnectionConfig(
    string Host,
    int Port,
    string Database,
    int MaxPoolSize,
    TimeSpan CommandTimeout
);

public class ConnectionConfigFactory
{
    private readonly ConcurrentDictionary<string, DatabaseConnectionConfig> _configs = new();

    public DatabaseConnectionConfig GetConfig(
        string host, int port, string database,
        int maxPoolSize = 100, TimeSpan? commandTimeout = null)
    {
        var key = $"{host}:{port}/{database}";

        return _configs.GetOrAdd(key, _ => new DatabaseConnectionConfig(
            host, port, database, maxPoolSize, commandTimeout ?? TimeSpan.FromSeconds(30)
        ));
    }

    // For connection strings - even more sharing potential
    public DatabaseConnectionConfig GetConfigFromConnectionString(string connectionString)
    {
        return _configs.GetOrAdd(connectionString, cs =>
        {
            // Parse connection string (simplified)
            var parts = cs.Split(';')
                .Select(p => p.Split('='))
                .Where(p => p.Length == 2)
                .ToDictionary(p => p[0].Trim(), p => p[1].Trim(), StringComparer.OrdinalIgnoreCase);

            return new DatabaseConnectionConfig(
                Host: parts.GetValueOrDefault("Server", "localhost"),
                Port: int.Parse(parts.GetValueOrDefault("Port", "5432")),
                Database: parts.GetValueOrDefault("Database", ""),
                MaxPoolSize: int.Parse(parts.GetValueOrDefault("MaxPoolSize", "100")),
                CommandTimeout: TimeSpan.FromSeconds(int.Parse(parts.GetValueOrDefault("Timeout", "30")))
            );
        });
    }
}

// Game sprite flyweight example
public class Sprite
{
    public string Name { get; }
    public byte[] ImageData { get; } // Large data - shared
    public int Width { get; }
    public int Height { get; }

    public Sprite(string name, byte[] imageData, int width, int height)
    {
        Name = name;
        ImageData = imageData;
        Width = width;
        Height = height;
    }

    public void Draw(int x, int y, float rotation, float scale)
    {
        // Drawing uses extrinsic state (position, rotation, scale)
        // but shares intrinsic state (image data)
        Console.WriteLine($"Drawing {Name} at ({x},{y}) rot:{rotation} scale:{scale}");
    }
}

public class SpriteFactory
{
    private readonly ConcurrentDictionary<string, Sprite> _sprites = new();

    public Sprite GetSprite(string name)
    {
        return _sprites.GetOrAdd(name, n =>
        {
            // Load from file system or embedded resources
            var imageData = LoadImageData(n);
            return new Sprite(n, imageData, 64, 64);
        });
    }

    private byte[] LoadImageData(string name)
    {
        // Simulate loading large image data
        return new byte[64 * 64 * 4]; // RGBA
    }

    public long TotalMemoryUsed => _sprites.Values.Sum(s => s.ImageData.Length);
}

// Game entity using flyweight sprite
public class GameEntity
{
    public int X { get; set; }
    public int Y { get; set; }
    public float Rotation { get; set; }
    public float Scale { get; set; } = 1.0f;
    public Sprite Sprite { get; } // Shared

    public GameEntity(Sprite sprite, int x, int y)
    {
        Sprite = sprite;
        X = x;
        Y = y;
    }

    public void Draw() => Sprite.Draw(X, Y, Rotation, Scale);
}

// DI Registration
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddFlyweightServices(this IServiceCollection services)
    {
        services.AddSingleton<RegexPatternFactory>();
        services.AddSingleton<StringPool>();
        services.AddSingleton<ConnectionConfigFactory>();
        services.AddSingleton<SpriteFactory>();

        return services;
    }
}
❓ Interview Q&A

Q1: What’s the key insight behind Flyweight pattern? A1: Separate intrinsic state (shared, immutable, context-independent) from extrinsic state (unique per instance, passed as parameters). Share objects with same intrinsic state.

Q2: What .NET features implement Flyweight? A2: String.Intern(), Type objects, boxed small integers (-128 to 127), Nullable<T> caching, and Enum.GetName() caching are built-in flyweight implementations.

Q3: What’s the trade-off with Flyweight? A3: Memory savings vs. runtime overhead. Looking up flyweights and managing extrinsic state has CPU cost. Worth it only when you have many instances with shared state.


Proxy

Intent: Provide a surrogate or placeholder for another object to control access to it.

πŸ“– Theory & When to Use

When to Use

  • Virtual Proxy: Lazy initialization of expensive objects
  • Protection Proxy: Control access based on permissions
  • Remote Proxy: Local representative of remote object
  • Logging Proxy: Add logging before/after operations
  • Caching Proxy: Cache results of expensive operations

Real-World Analogy

A credit card is a proxy for cash. It has the same interface (payment) but adds access control (credit limit), logging (statements), and deferred settlement.

Key Participants

  • Subject: Interface for RealSubject and Proxy
  • RealSubject: The real object the proxy represents
  • Proxy: Maintains reference to RealSubject, controls access
πŸ“Š UML Diagram
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   <<interface>>     β”‚
β”‚      ISubject       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ + Request()         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
     β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”
     β”‚           β”‚
β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Real    β”‚ β”‚    Proxy     β”‚
β”‚ Subject β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ - realSubject│────> RealSubject
β”‚+Request()β”‚ β”‚ + Request() β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’» Short Example (~20 lines)
public interface IImage
{
    void Display();
}

public class RealImage : IImage
{
    private readonly string _filename;
    public RealImage(string filename)
    {
        _filename = filename;
        Console.WriteLine($"Loading image from disk: {filename}");
    }
    public void Display() => Console.WriteLine($"Displaying {_filename}");
}

// Virtual Proxy - lazy loading
public class ProxyImage : IImage
{
    private readonly string _filename;
    private RealImage? _realImage;

    public ProxyImage(string filename) => _filename = filename;

    public void Display()
    {
        _realImage ??= new RealImage(_filename);
        _realImage.Display();
    }
}

// Usage - image loaded only when Display() called
IImage image = new ProxyImage("photo.jpg"); // No loading yet
image.Display(); // Now it loads
image.Display(); // Uses cached instance
πŸ’» Medium Example (~50 lines)
// Subject interface
public interface IBankAccount
{
    decimal GetBalance();
    void Withdraw(decimal amount);
    void Deposit(decimal amount);
}

// Real subject
public class BankAccount : IBankAccount
{
    private decimal _balance;

    public BankAccount(decimal initialBalance) => _balance = initialBalance;

    public decimal GetBalance() => _balance;

    public void Withdraw(decimal amount)
    {
        if (amount > _balance) throw new InvalidOperationException("Insufficient funds");
        _balance -= amount;
    }

    public void Deposit(decimal amount) => _balance += amount;
}

// Protection proxy
public class SecureBankAccountProxy : IBankAccount
{
    private readonly BankAccount _account;
    private readonly string _userId;
    private readonly IAuthorizationService _authService;

    public SecureBankAccountProxy(BankAccount account, string userId, IAuthorizationService authService)
    {
        _account = account;
        _userId = userId;
        _authService = authService;
    }

    public decimal GetBalance()
    {
        if (!_authService.CanRead(_userId)) throw new UnauthorizedAccessException();
        return _account.GetBalance();
    }

    public void Withdraw(decimal amount)
    {
        if (!_authService.CanWithdraw(_userId, amount)) throw new UnauthorizedAccessException();
        _account.Withdraw(amount);
        Console.WriteLine($"Audit: {_userId} withdrew {amount:C}");
    }

    public void Deposit(decimal amount)
    {
        if (!_authService.CanDeposit(_userId)) throw new UnauthorizedAccessException();
        _account.Deposit(amount);
        Console.WriteLine($"Audit: {_userId} deposited {amount:C}");
    }
}

public interface IAuthorizationService
{
    bool CanRead(string userId);
    bool CanWithdraw(string userId, decimal amount);
    bool CanDeposit(string userId);
}

// Usage
var account = new BankAccount(1000);
var proxy = new SecureBankAccountProxy(account, "user123", new SimpleAuthService());
proxy.Deposit(500);
Console.WriteLine($"Balance: {proxy.GetBalance():C}");
πŸ’» Production-Grade Example
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System.Diagnostics;

// Subject interface
public interface IProductRepository
{
    Task<Product?> GetByIdAsync(string id, CancellationToken ct = default);
    Task<IEnumerable<Product>> GetAllAsync(CancellationToken ct = default);
    Task<IEnumerable<Product>> SearchAsync(string query, CancellationToken ct = default);
    Task CreateAsync(Product product, CancellationToken ct = default);
    Task UpdateAsync(Product product, CancellationToken ct = default);
    Task DeleteAsync(string id, CancellationToken ct = default);
}

public record Product(string Id, string Name, decimal Price, string Category);

// Real subject - actual database repository
public class ProductRepository : IProductRepository
{
    private readonly IDbConnection _connection;

    public ProductRepository(IDbConnection connection) => _connection = connection;

    public async Task<Product?> GetByIdAsync(string id, CancellationToken ct = default)
    {
        await Task.Delay(50, ct); // Simulate DB call
        return new Product(id, "Sample Product", 99.99m, "Electronics");
    }

    public async Task<IEnumerable<Product>> GetAllAsync(CancellationToken ct = default)
    {
        await Task.Delay(100, ct);
        return new[] { new Product("1", "Product 1", 10m, "Cat1") };
    }

    public async Task<IEnumerable<Product>> SearchAsync(string query, CancellationToken ct = default)
    {
        await Task.Delay(75, ct);
        return Enumerable.Empty<Product>();
    }

    public async Task CreateAsync(Product product, CancellationToken ct = default)
    {
        await Task.Delay(50, ct);
    }

    public async Task UpdateAsync(Product product, CancellationToken ct = default)
    {
        await Task.Delay(50, ct);
    }

    public async Task DeleteAsync(string id, CancellationToken ct = default)
    {
        await Task.Delay(50, ct);
    }
}

// Caching proxy
public class CachingProductRepositoryProxy : IProductRepository
{
    private readonly IProductRepository _repository;
    private readonly IMemoryCache _cache;
    private readonly ILogger<CachingProductRepositoryProxy> _logger;
    private readonly TimeSpan _defaultExpiration = TimeSpan.FromMinutes(5);

    public CachingProductRepositoryProxy(
        IProductRepository repository,
        IMemoryCache cache,
        ILogger<CachingProductRepositoryProxy> logger)
    {
        _repository = repository;
        _cache = cache;
        _logger = logger;
    }

    public async Task<Product?> GetByIdAsync(string id, CancellationToken ct = default)
    {
        var cacheKey = $"product:{id}";

        if (_cache.TryGetValue(cacheKey, out Product? cached))
        {
            _logger.LogDebug("Cache hit for product {Id}", id);
            return cached;
        }

        _logger.LogDebug("Cache miss for product {Id}", id);
        var product = await _repository.GetByIdAsync(id, ct);

        if (product != null)
        {
            _cache.Set(cacheKey, product, _defaultExpiration);
        }

        return product;
    }

    public async Task<IEnumerable<Product>> GetAllAsync(CancellationToken ct = default)
    {
        var cacheKey = "products:all";

        if (_cache.TryGetValue(cacheKey, out IEnumerable<Product>? cached))
        {
            return cached!;
        }

        var products = await _repository.GetAllAsync(ct);
        _cache.Set(cacheKey, products, TimeSpan.FromMinutes(1));
        return products;
    }

    public Task<IEnumerable<Product>> SearchAsync(string query, CancellationToken ct = default)
    {
        // Don't cache searches - too many variations
        return _repository.SearchAsync(query, ct);
    }

    public async Task CreateAsync(Product product, CancellationToken ct = default)
    {
        await _repository.CreateAsync(product, ct);
        InvalidateCache();
    }

    public async Task UpdateAsync(Product product, CancellationToken ct = default)
    {
        await _repository.UpdateAsync(product, ct);
        _cache.Remove($"product:{product.Id}");
        InvalidateCache();
    }

    public async Task DeleteAsync(string id, CancellationToken ct = default)
    {
        await _repository.DeleteAsync(id, ct);
        _cache.Remove($"product:{id}");
        InvalidateCache();
    }

    private void InvalidateCache()
    {
        _cache.Remove("products:all");
        _logger.LogDebug("Cache invalidated");
    }
}

// Logging/metrics proxy
public class LoggingProductRepositoryProxy : IProductRepository
{
    private readonly IProductRepository _repository;
    private readonly ILogger<LoggingProductRepositoryProxy> _logger;

    public LoggingProductRepositoryProxy(
        IProductRepository repository,
        ILogger<LoggingProductRepositoryProxy> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public async Task<Product?> GetByIdAsync(string id, CancellationToken ct = default)
    {
        return await ExecuteWithLogging(
            () => _repository.GetByIdAsync(id, ct),
            nameof(GetByIdAsync),
            new { id }
        );
    }

    public async Task<IEnumerable<Product>> GetAllAsync(CancellationToken ct = default)
    {
        return await ExecuteWithLogging(
            () => _repository.GetAllAsync(ct),
            nameof(GetAllAsync)
        );
    }

    public async Task<IEnumerable<Product>> SearchAsync(string query, CancellationToken ct = default)
    {
        return await ExecuteWithLogging(
            () => _repository.SearchAsync(query, ct),
            nameof(SearchAsync),
            new { query }
        );
    }

    public async Task CreateAsync(Product product, CancellationToken ct = default)
    {
        await ExecuteWithLogging(
            async () => { await _repository.CreateAsync(product, ct); return true; },
            nameof(CreateAsync),
            new { product.Id, product.Name }
        );
    }

    public async Task UpdateAsync(Product product, CancellationToken ct = default)
    {
        await ExecuteWithLogging(
            async () => { await _repository.UpdateAsync(product, ct); return true; },
            nameof(UpdateAsync),
            new { product.Id }
        );
    }

    public async Task DeleteAsync(string id, CancellationToken ct = default)
    {
        await ExecuteWithLogging(
            async () => { await _repository.DeleteAsync(id, ct); return true; },
            nameof(DeleteAsync),
            new { id }
        );
    }

    private async Task<T> ExecuteWithLogging<T>(Func<Task<T>> operation, string methodName, object? parameters = null)
    {
        var sw = Stopwatch.StartNew();
        try
        {
            _logger.LogDebug("Executing {Method} with {Parameters}", methodName, parameters);
            var result = await operation();
            sw.Stop();
            _logger.LogDebug("{Method} completed in {ElapsedMs}ms", methodName, sw.ElapsedMilliseconds);
            return result;
        }
        catch (Exception ex)
        {
            sw.Stop();
            _logger.LogError(ex, "{Method} failed after {ElapsedMs}ms", methodName, sw.ElapsedMilliseconds);
            throw;
        }
    }
}

// Circuit breaker proxy
public class CircuitBreakerProductRepositoryProxy : IProductRepository
{
    private readonly IProductRepository _repository;
    private readonly ILogger<CircuitBreakerProductRepositoryProxy> _logger;

    private int _failureCount;
    private DateTime _lastFailure = DateTime.MinValue;
    private readonly int _threshold = 5;
    private readonly TimeSpan _resetTimeout = TimeSpan.FromSeconds(30);

    public CircuitBreakerProductRepositoryProxy(
        IProductRepository repository,
        ILogger<CircuitBreakerProductRepositoryProxy> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    private bool IsOpen => _failureCount >= _threshold &&
                           DateTime.UtcNow - _lastFailure < _resetTimeout;

    public async Task<Product?> GetByIdAsync(string id, CancellationToken ct = default)
    {
        return await ExecuteWithCircuitBreaker(() => _repository.GetByIdAsync(id, ct));
    }

    public async Task<IEnumerable<Product>> GetAllAsync(CancellationToken ct = default)
    {
        return await ExecuteWithCircuitBreaker(() => _repository.GetAllAsync(ct));
    }

    public async Task<IEnumerable<Product>> SearchAsync(string query, CancellationToken ct = default)
    {
        return await ExecuteWithCircuitBreaker(() => _repository.SearchAsync(query, ct));
    }

    public Task CreateAsync(Product product, CancellationToken ct = default) =>
        ExecuteWithCircuitBreaker(async () => { await _repository.CreateAsync(product, ct); return true; });

    public Task UpdateAsync(Product product, CancellationToken ct = default) =>
        ExecuteWithCircuitBreaker(async () => { await _repository.UpdateAsync(product, ct); return true; });

    public Task DeleteAsync(string id, CancellationToken ct = default) =>
        ExecuteWithCircuitBreaker(async () => { await _repository.DeleteAsync(id, ct); return true; });

    private async Task<T> ExecuteWithCircuitBreaker<T>(Func<Task<T>> operation)
    {
        if (IsOpen)
        {
            _logger.LogWarning("Circuit breaker is open, rejecting request");
            throw new CircuitBreakerOpenException();
        }

        try
        {
            var result = await operation();
            Interlocked.Exchange(ref _failureCount, 0); // Reset on success
            return result;
        }
        catch (Exception ex) when (ex is not CircuitBreakerOpenException)
        {
            _lastFailure = DateTime.UtcNow;
            var failures = Interlocked.Increment(ref _failureCount);
            _logger.LogWarning("Operation failed, failure count: {Count}", failures);
            throw;
        }
    }
}

public class CircuitBreakerOpenException : Exception
{
    public CircuitBreakerOpenException() : base("Circuit breaker is open") { }
}

// DI Registration with proxy chain
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddProductRepository(this IServiceCollection services)
    {
        services.AddMemoryCache();

        // Real repository
        services.AddScoped<ProductRepository>();

        // Build proxy chain
        services.AddScoped<IProductRepository>(sp =>
        {
            var repository = sp.GetRequiredService<ProductRepository>();
            var cache = sp.GetRequiredService<IMemoryCache>();
            var loggerFactory = sp.GetRequiredService<ILoggerFactory>();

            // Inner to outer: Repository -> CircuitBreaker -> Caching -> Logging
            IProductRepository proxy = new CircuitBreakerProductRepositoryProxy(
                repository,
                loggerFactory.CreateLogger<CircuitBreakerProductRepositoryProxy>());

            proxy = new CachingProductRepositoryProxy(
                proxy,
                cache,
                loggerFactory.CreateLogger<CachingProductRepositoryProxy>());

            proxy = new LoggingProductRepositoryProxy(
                proxy,
                loggerFactory.CreateLogger<LoggingProductRepositoryProxy>());

            return proxy;
        });

        return services;
    }
}
❓ Interview Q&A

Q1: What’s the difference between Proxy and Decorator? A1: Both wrap objects but serve different purposes. Decorator adds behavior dynamically. Proxy controls access (lazy loading, security, caching). Proxy typically manages lifecycle; Decorator doesn’t.

Q2: How does Proxy differ from Adapter? A2: Adapter changes the interface to make incompatible classes work together. Proxy keeps the same interface but adds control/functionality. Adapter is about compatibility; Proxy is about control.

Q3: When would you choose Virtual Proxy over eager loading? A3: When objects are expensive to create, when many objects exist but few are used, or when initialization order matters. Trade-off is complexity vs. memory/startup time.

Q4: How do you implement Proxy in .NET? A4: Manual implementation (shown above), or use frameworks: Castle DynamicProxy, DispatchProxy (built-in), or source generators. For simple cases, manual is clearer.


Quick Reference

Pattern Use Case Key Benefit
Adapter Integrate incompatible interfaces Reuse existing classes
Bridge Separate abstraction from implementation Independent variation
Composite Tree structures, part-whole hierarchies Uniform treatment
Decorator Add responsibilities dynamically Flexible extension
Facade Simplify complex subsystem Easy-to-use interface
Flyweight Share many fine-grained objects Memory efficiency
Proxy Control access to object Lazy loading, security, caching

See Also

  • Creational Patterns - Factory, Abstract Factory, Builder, Prototype, Singleton
  • Behavioral Patterns - Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor

πŸ“š Related Articles