🧩

Modern Patterns Cheatsheet

Design Patterns Intermediate 3 min read 400 words

Modern .NET Patterns - Quick Reference Cheatsheet

Principal Engineer Interview Quick Reference - CQRS, Rate Limiting, Resilience, Health Checks, Distributed Caching


1. CQRS (Command Query Responsibility Segregation)

Core Concept

Commands β†’ Write Model (full entity) β†’ Write Database
Queries  β†’ Read Model (DTOs/projections) β†’ Read Database (optional)

MediatR Setup

// Command
public record CreateOrderCommand(string CustomerId, List<OrderItem> Items)
    : IRequest<Guid>;

// Handler
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
{
    public async Task<Guid> Handle(CreateOrderCommand cmd, CancellationToken ct)
    {
        var order = new Order(cmd.CustomerId, cmd.Items);
        await _writeDb.Orders.AddAsync(order, ct);
        await _writeDb.SaveChangesAsync(ct);
        return order.Id;
    }
}

// Query
public record GetOrderQuery(Guid Id) : IRequest<OrderDto>;

// Usage
var orderId = await _mediator.Send(new CreateOrderCommand(...));
var order = await _mediator.Send(new GetOrderQuery(orderId));

When to Use CQRS

Use CQRS Don’t Use CQRS
Complex domain logic Simple CRUD apps
Different read/write scaling Small team/project
Event Sourcing needed Low traffic
High read:write ratio Tight deadlines

2. Rate Limiting (.NET 7+)

Built-in Limiters

Limiter Use Case Key Config
Fixed Window Simple quota per period PermitLimit, Window
Sliding Window Smoother distribution PermitLimit, Window, SegmentsPerWindow
Token Bucket Burst-friendly APIs TokenLimit, ReplenishmentPeriod, TokensPerPeriod
Concurrency Limit parallel requests PermitLimit, QueueLimit

Quick Setup

// Program.cs
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("api", opt =>
    {
        opt.PermitLimit = 100;
        opt.Window = TimeSpan.FromMinutes(1);
        opt.QueueLimit = 10;
        opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
    });

    options.RejectionStatusCode = 429;
});

app.UseRateLimiter();

// Apply to endpoint
app.MapGet("/api/data", () => "OK")
   .RequireRateLimiting("api");

Per-User Rate Limiting

options.AddPolicy("per-user", context =>
    RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: context.User.Identity?.Name ?? context.Connection.RemoteIpAddress?.ToString(),
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 10,
            Window = TimeSpan.FromSeconds(10)
        }));

3. Resilience Patterns (Polly v8)

Pipeline Builder Pattern

var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddRetry(new RetryStrategyOptions<HttpResponseMessage>
    {
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromMilliseconds(500),
        BackoffType = DelayBackoffType.Exponential,
        UseJitter = true,
        ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
            .Handle<HttpRequestException>()
            .HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError)
    })
    .AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
    {
        FailureRatio = 0.5,
        SamplingDuration = TimeSpan.FromSeconds(30),
        MinimumThroughput = 10,
        BreakDuration = TimeSpan.FromSeconds(30)
    })
    .AddTimeout(TimeSpan.FromSeconds(10))
    .Build();

var result = await pipeline.ExecuteAsync(async ct =>
    await httpClient.GetAsync(url, ct));

HttpClientFactory Integration

builder.Services.AddHttpClient<IMyService, MyService>()
    .AddStandardResilienceHandler(); // Built-in resilience

// Or custom
builder.Services.AddHttpClient<IMyService, MyService>()
    .AddResilienceHandler("custom", builder =>
    {
        builder.AddRetry(retryOptions);
        builder.AddCircuitBreaker(cbOptions);
    });

Circuit Breaker States

CLOSED β†’ (failures exceed threshold) β†’ OPEN
OPEN β†’ (break duration expires) β†’ HALF-OPEN
HALF-OPEN β†’ (test request succeeds) β†’ CLOSED
HALF-OPEN β†’ (test request fails) β†’ OPEN

4. Health Checks

Quick Setup

// Registration
builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy())
    .AddSqlServer(connectionString, name: "database")
    .AddRedis(redisConnection, name: "redis")
    .AddUrlGroup(new Uri("https://api.external.com"), name: "external-api");

// Endpoints
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false // No dependency checks
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});

Custom Health Check

public class DatabaseHealthCheck : IHealthCheck
{
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, CancellationToken ct = default)
    {
        try
        {
            await _db.Database.CanConnectAsync(ct);
            return HealthCheckResult.Healthy();
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("DB connection failed", ex);
        }
    }
}

Kubernetes Probes Mapping

Probe Endpoint Purpose
livenessProbe /health/live Is process alive?
readinessProbe /health/ready Can accept traffic?
startupProbe /health/startup Has started successfully?

5. Distributed Caching

IDistributedCache Setup

// Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
    options.InstanceName = "MyApp:";
});

// Usage
await _cache.SetStringAsync("key", "value", new DistributedCacheEntryOptions
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),
    SlidingExpiration = TimeSpan.FromMinutes(1)
});

var value = await _cache.GetStringAsync("key");

Cache-Aside Pattern

public async Task<T?> GetOrSetAsync<T>(string key, Func<Task<T>> factory, TimeSpan ttl)
{
    var cached = await _cache.GetStringAsync(key);
    if (cached != null)
        return JsonSerializer.Deserialize<T>(cached);

    var value = await factory();
    if (value != null)
    {
        await _cache.SetStringAsync(key, JsonSerializer.Serialize(value),
            new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = ttl });
    }
    return value;
}

Cache Stampede Prevention

private readonly SemaphoreSlim _lock = new(1, 1);

public async Task<T?> GetOrSetWithLockAsync<T>(string key, Func<Task<T>> factory, TimeSpan ttl)
{
    var cached = await _cache.GetStringAsync(key);
    if (cached != null) return JsonSerializer.Deserialize<T>(cached);

    await _lock.WaitAsync();
    try
    {
        // Double-check after acquiring lock
        cached = await _cache.GetStringAsync(key);
        if (cached != null) return JsonSerializer.Deserialize<T>(cached);

        var value = await factory();
        await _cache.SetStringAsync(key, JsonSerializer.Serialize(value),
            new() { AbsoluteExpirationRelativeToNow = ttl });
        return value;
    }
    finally { _lock.Release(); }
}

Multi-Tier Caching (L1 + L2)

public async Task<T?> GetAsync<T>(string key)
{
    // L1: Memory (fast)
    if (_memory.TryGetValue(key, out T? value))
        return value;

    // L2: Redis (distributed)
    var cached = await _redis.GetStringAsync(key);
    if (cached != null)
    {
        value = JsonSerializer.Deserialize<T>(cached);
        _memory.Set(key, value, TimeSpan.FromSeconds(30)); // Short L1 TTL
        return value;
    }

    return default;
}

Quick Decision Trees

Rate Limiter Selection

Need burst support? β†’ Token Bucket
Simple quota per period? β†’ Fixed Window
Smooth distribution needed? β†’ Sliding Window
Limit parallel operations? β†’ Concurrency Limiter

Caching Strategy Selection

Single server? β†’ IMemoryCache
Multiple servers? β†’ IDistributedCache (Redis)
High traffic + multiple servers? β†’ Multi-tier (Memory + Redis)
Complex invalidation? β†’ Consider cache tags/pub-sub

Resilience Strategy Selection

Transient failures? β†’ Retry with exponential backoff
Dependency unreliable? β†’ Circuit Breaker
Need fast failure? β†’ Timeout
All of above? β†’ Combined pipeline

Interview Quick Answers

Q: When would you use CQRS?

When read and write models have different requirements, high read:write ratios, or when scaling reads and writes independently is beneficial.

Q: Difference between rate limiter types?

Fixed Window: simple quota per period (can have burst at boundaries). Sliding Window: smoother distribution. Token Bucket: allows controlled bursts. Concurrency: limits parallel requests.

Q: How to prevent cache stampede?

Use locking (SemaphoreSlim) to ensure only one request fetches data while others wait, then double-check cache after acquiring lock.

Q: Circuit breaker states?

Closed (normal), Open (failing fast), Half-Open (testing recovery). Transitions based on failure ratios and break duration.

Q: Health check types in Kubernetes?

Liveness (restart if fails), Readiness (stop traffic if fails), Startup (allow slow initialization).


Common Gotchas

  1. CQRS: Don’t use for simple CRUD - adds unnecessary complexity
  2. Rate Limiting: Fixed window can allow 2x burst at window boundaries
  3. Circuit Breaker: Set appropriate sampling duration to avoid false positives
  4. Caching: Always handle cache misses gracefully; cache can be cleared anytime
  5. Health Checks: Don’t include slow checks in liveness probes - can cause unnecessary restarts

Last updated: December 2024 | .NET 8

πŸ“š Related Articles