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