Skip to content

knanda-degreed/csharp-cache-patterns

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

C# Cache Patterns - Comprehensive Examples

This project demonstrates 5 different caching patterns in C# with working, executable examples.

🎯 What You'll Learn

  • Cache-Aside (Lazy Loading) - Most common pattern
  • Write-Through - Keep cache and DB in sync
  • Write-Back (Write-Behind) - Fast writes with async DB updates
  • Write-Around - Avoid cache pollution
  • Read-Through - Clean cache abstraction

🏗️ Project Structure

Caches/
├── Models/
│   ├── Product.cs          # Domain model
│   └── User.cs             # Domain model
├── Database/
│   └── SimulatedDatabase.cs # In-memory DB with latency simulation
├── Patterns/
│   ├── CacheAsidePattern.cs      # Cache-Aside implementation
│   ├── WriteThroughPattern.cs    # Write-Through implementation
│   ├── WriteBackPattern.cs       # Write-Back implementation
│   ├── WriteAroundPattern.cs     # Write-Around implementation
│   └── ReadThroughPattern.cs     # Read-Through implementation
└── Program.cs             # Interactive demo app

🚀 Running the Demo

cd /Users/knanda/source/mini-projects/Caches
dotnet run --project Caches

You'll see an interactive menu:

📋 Select a cache pattern to demo:
  1. Cache-Aside (Lazy Loading)
  2. Write-Through
  3. Write-Back (Write-Behind)
  4. Write-Around
  5. Read-Through
  6. Compare All Patterns
  0. Exit

📚 Pattern Details

1. Cache-Aside (Lazy Loading)

How it works:

  • Application checks cache first
  • On miss: load from DB → cache it → return
  • On write: update DB → invalidate cache

Code Example:

public async Task<Product?> GetProductAsync(int id)
{
    var cacheKey = $"product:{id}";

    // Check cache
    if (_cache.TryGetValue(cacheKey, out Product? cached))
        return cached;

    // Load from DB
    var product = await _db.GetProductAsync(id);

    // Cache it
    _cache.Set(cacheKey, product, _cacheExpiration);
    return product;
}

When to use:

  • ✅ Read-heavy workloads
  • ✅ Most common pattern (used in your Degreed API!)
  • ✅ Resilient to cache failures

2. Write-Through

How it works:

  • Write to cache AND database synchronously
  • Both must succeed for operation to complete
  • Cache is always fresh

Code Example:

public async Task UpdateProductAsync(Product product)
{
    var cacheKey = $"product:{product.Id}";

    // Update cache first
    _cache.Set(cacheKey, product, _cacheExpiration);

    // Then update database
    await _db.UpdateProductAsync(product);
}

When to use:

  • ✅ Strong consistency required
  • ✅ Read-after-write scenarios
  • ❌ Higher write latency (dual write)

3. Write-Back (Write-Behind)

How it works:

  • Write to cache immediately
  • Queue database write for background processing
  • Very fast writes, eventual consistency

Code Example:

public async Task UpdateProductAsync(Product product)
{
    // Update cache (fast)
    _cache.Set($"product:{product.Id}", product);

    // Queue DB write (async)
    await _writeQueue.Writer.WriteAsync(product);

    // Returns immediately!
}

When to use:

  • ✅ Write-heavy workloads
  • ✅ Analytics, logging systems
  • ❌ Risk of data loss if cache crashes

4. Write-Around

How it works:

  • Writes bypass cache, go straight to DB
  • Cache is invalidated on write
  • Next read will cache miss and reload

Code Example:

public async Task UpdateProductAsync(Product product)
{
    // Write to DB only
    await _db.UpdateProductAsync(product);

    // Invalidate cache
    _cache.Remove($"product:{product.Id}");
}

When to use:

  • ✅ Infrequently read data
  • ✅ Bulk operations
  • ✅ Avoid cache pollution

5. Read-Through

How it works:

  • Cache library handles loading automatically
  • Application doesn't need cache-miss logic
  • Clean abstraction over cache-aside

Code Example:

public async Task<Product?> GetProductAsync(int id)
{
    return await _cache.GetOrCreateAsync($"product:{id}", async entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = _cacheExpiration;
        return await _db.GetProductAsync(id);
    });
}

When to use:

  • ✅ Cleaner code
  • ✅ Library-managed caching
  • Similar to Cache-Aside but more abstracted

📊 Comparison Matrix

Pattern Read Latency Write Latency Consistency Complexity
Cache-Aside Medium (lazy) Low (DB only) Eventual Low
Write-Through Low (cached) High (dual) Strong Low
Write-Back Low (cached) Very Low (async) Eventual High
Write-Around High (miss) Low (DB only) Eventual Low
Read-Through Medium (auto) Low (DB only) Eventual Low

🔍 What Makes This Demo Special

  1. No External Dependencies - Uses in-memory simulated database
  2. Latency Simulation - 100ms delays show caching benefits
  3. DB Operation Tracking - See exactly when DB is hit
  4. Interactive Examples - Run each pattern independently
  5. Side-by-Side Comparison - Compare all patterns at once

💡 Key Takeaways

  • Cache-Aside is the most common pattern (used in most .NET APIs)
  • Write-Through for strong consistency
  • Write-Back for maximum write performance (with risk)
  • Write-Around to avoid cache pollution
  • Read-Through for cleaner abstractions

🛠️ Technologies Used

  • .NET 8.0
  • Microsoft.Extensions.Caching.Memory - In-memory cache
  • System.Threading.Channels - Background queue for Write-Back
  • Simulated Database - No SQL Server needed!

📖 Further Reading

🎓 Next Steps

Want to extend this project? Try:

  1. Add distributed caching with Redis
  2. Implement cache stampede protection
  3. Add refresh-ahead pattern
  4. Implement TTL refresh on access
  5. Add metrics and monitoring

No database setup required! Everything runs in-memory with simulated latency.

Run dotnet run --project Caches and explore! 🚀

About

Comprehensive examples of 5 caching patterns in C# - Cache-Aside, Write-Through, Write-Back, Write-Around, and Read-Through

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages