Skip to main content
The Basket service uses Redis as a distributed cache layer to improve performance and scalability of shopping cart operations.

Why Redis?

Redis provides significant benefits for the Basket service:
  • In-Memory Speed: Sub-millisecond response times for cart operations
  • Distributed Cache: Shared across multiple service instances
  • Session Storage: Natural fit for transient shopping cart data
  • Scalability: Handle high-frequency read/write operations
  • Persistence Options: Configure durability based on requirements

Configuration

Connection String

src/Services/Basket/Basket.API/appsettings.json
{
  "ConnectionStrings": {
    "Database": "Server=localhost;Port=5433;Database=BasketDb;User Id=postgres;Password=postgres;Include Error Detail=true",
    "Redis": "localhost:6379"
  },
  "GrpcSettings": {
    "DiscountUrl": "https://localhost:5052"
  }
}

Service Registration

src/Services/Basket/Basket.API/Program.cs
// Data Services - Marten for persistence
builder.Services.AddMarten(opts =>
{
    opts.Connection(builder.Configuration.GetConnectionString("Database")!);
    opts.Schema.For<ShoppingCart>().Identity(x => x.UserName);
}).UseLightweightSessions();

// Repository pattern with caching decorator
builder.Services.AddScoped<IBasketRepository, BasketRepository>();
builder.Services.Decorate<IBasketRepository, CachedBasketRepository>();

// Redis distributed cache
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    // options.InstanceName = "Basket"; // Optional: prefix all keys
});
Key Points:
  • StackExchange.Redis: Microsoft’s recommended Redis client
  • Decorator Pattern: Adds caching transparently over base repository
  • Optional Prefix: Use InstanceName to namespace keys

Cached Repository Implementation

Cache-Aside Pattern

src/Services/Basket/Basket.API/Data/CachedBasketRepository.cs
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;

namespace Basket.API.Data;

public class CachedBasketRepository
    (IBasketRepository repository, IDistributedCache cache) 
    : IBasketRepository
{
    public async Task<ShoppingCart> GetBasket(string userName, CancellationToken cancellationToken = default)
    {
        // Try to get from cache first
        var cachedBasket = await cache.GetStringAsync(userName, cancellationToken);
        if (!string.IsNullOrEmpty(cachedBasket))
            return JsonSerializer.Deserialize<ShoppingCart>(cachedBasket)!;

        // Cache miss - get from database
        var basket = await repository.GetBasket(userName, cancellationToken);
        
        // Store in cache for next time
        await cache.SetStringAsync(userName, JsonSerializer.Serialize(basket), cancellationToken);
        
        return basket;
    }

    public async Task<ShoppingCart> StoreBasket(ShoppingCart basket, CancellationToken cancellationToken = default)
    {
        // Update database
        await repository.StoreBasket(basket, cancellationToken);

        // Update cache
        await cache.SetStringAsync(
            basket.UserName, 
            JsonSerializer.Serialize(basket), 
            cancellationToken);

        return basket;
    }

    public async Task<bool> DeleteBasket(string userName, CancellationToken cancellationToken = default)
    {
        // Delete from database
        await repository.DeleteBasket(userName, cancellationToken);

        // Remove from cache
        await cache.RemoveAsync(userName, cancellationToken);

        return true;
    }
}
Cache Strategy:
  1. Read: Check cache → Cache miss → Read DB → Update cache
  2. Write: Update DB → Update cache
  3. Delete: Delete from DB → Remove from cache

Base Repository (Database)

src/Services/Basket/Basket.API/Data/BasketRepository.cs
public class BasketRepository(IDocumentSession session)
    : IBasketRepository
{
    public async Task<ShoppingCart> GetBasket(string userName, CancellationToken cancellationToken = default)
    {
        var basket = await session.LoadAsync<ShoppingCart>(userName, cancellationToken);
        return basket is null ? throw new BasketNotFoundException(userName) : basket;
    }

    public async Task<ShoppingCart> StoreBasket(ShoppingCart basket, CancellationToken cancellationToken = default)
    {
        session.Store(basket);
        await session.SaveChangesAsync(cancellationToken);
        return basket;
    }

    public async Task<bool> DeleteBasket(string userName, CancellationToken cancellationToken = default)
    {
        session.Delete<ShoppingCart>(userName);
        await session.SaveChangesAsync(cancellationToken);
        return true;
    }
}

Redis Operations

IDistributedCache Interface

The ASP.NET Core IDistributedCache interface provides:
public interface IDistributedCache
{
    byte[] Get(string key);
    Task<byte[]> GetAsync(string key, CancellationToken token = default);
    
    void Set(string key, byte[] value, DistributedCacheEntryOptions options);
    Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default);
    
    void Refresh(string key);
    Task RefreshAsync(string key, CancellationToken token = default);
    
    void Remove(string key);
    Task RemoveAsync(string key, CancellationToken token = default);
}

String Extensions

For convenience, use string extensions:
// Get string value
var value = await cache.GetStringAsync("key");

// Set string value
await cache.SetStringAsync("key", "value");

// Set with expiration
await cache.SetStringAsync("key", "value", new DistributedCacheEntryOptions
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
});

Cache Expiration Strategies

Sliding Expiration

Extends expiration on each access:
var options = new DistributedCacheEntryOptions
{
    SlidingExpiration = TimeSpan.FromMinutes(30)
};

await cache.SetStringAsync(key, value, options);

Absolute Expiration

Expires at specific time:
var options = new DistributedCacheEntryOptions
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
};

await cache.SetStringAsync(key, value, options);

Combined Expiration

var options = new DistributedCacheEntryOptions
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24),
    SlidingExpiration = TimeSpan.FromMinutes(30)
};

await cache.SetStringAsync(key, value, options);

Advanced Usage

Direct StackExchange.Redis

For advanced scenarios, inject IConnectionMultiplexer:
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
    var configuration = builder.Configuration.GetConnectionString("Redis");
    return ConnectionMultiplexer.Connect(configuration);
});
Then use directly:
public class AdvancedCacheService
{
    private readonly IDatabase _redis;

    public AdvancedCacheService(IConnectionMultiplexer multiplexer)
    {
        _redis = multiplexer.GetDatabase();
    }

    public async Task<string> GetAsync(string key)
    {
        return await _redis.StringGetAsync(key);
    }

    public async Task SetAsync(string key, string value, TimeSpan? expiry = null)
    {
        await _redis.StringSetAsync(key, value, expiry);
    }

    // Use Redis data structures
    public async Task AddToListAsync(string key, string value)
    {
        await _redis.ListLeftPushAsync(key, value);
    }

    public async Task<long> IncrementAsync(string key)
    {
        return await _redis.StringIncrementAsync(key);
    }
}

Docker Compose Setup

redis:
  image: redis:alpine
  ports:
    - "6379:6379"
  volumes:
    - redis_data:/data
With persistence:
redis:
  image: redis:alpine
  command: redis-server --appendonly yes
  ports:
    - "6379:6379"
  volumes:
    - redis_data:/data

Health Checks

builder.Services.AddHealthChecks()
    .AddNpgSql(builder.Configuration.GetConnectionString("Database")!)
    .AddRedis(builder.Configuration.GetConnectionString("Redis")!);
Access at /health endpoint.

Monitoring

Redis CLI

# Connect to Redis
redis-cli

# Monitor all commands
MONITOR

# View all keys
KEYS *

# Get value
GET username

# View stats
INFO

# View memory usage
INFO memory

Application Insights

For production monitoring:
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "Basket";
    // Add profiling for Application Insights
    options.ProfilingSession = () => ???;
});

Performance Best Practices

1. Use Binary Serialization

For large objects, use binary serialization:
// MessagePack is faster than JSON
var bytes = MessagePackSerializer.Serialize(basket);
await cache.SetAsync(key, bytes);

2. Batch Operations

var tasks = usernames.Select(async username =>
{
    var basket = await cache.GetStringAsync(username);
    return (username, basket);
});

var results = await Task.WhenAll(tasks);

3. Connection Pooling

StackExchange.Redis automatically pools connections. Reuse IConnectionMultiplexer:
// Good: Singleton connection
services.AddSingleton<IConnectionMultiplexer>(...);

// Bad: Creating new connections
new ConnectionMultiplexer.Connect(config); // DON'T DO THIS

4. Key Naming Convention

// Use consistent key naming
const string KeyPrefix = "basket";
var key = $"{KeyPrefix}:{username}";

// Or with instance name
options.InstanceName = "Basket:"; // All keys prefixed automatically

Troubleshooting

Connection Issues

# Test Redis connection
redis-cli ping
# Expected: PONG

# Check if Redis is running
redis-cli info server

Clear All Cache

redis-cli FLUSHALL

View Cache Contents

# List all keys
redis-cli KEYS "*"

# Get specific key
redis-cli GET username

# Check TTL
redis-cli TTL username

Performance Issues

# Check slow queries
redis-cli SLOWLOG GET 10

# Monitor real-time
redis-cli MONITOR

Production Considerations

High Availability

For production, use Redis Cluster or Azure Cache for Redis:
{
  "ConnectionStrings": {
    "Redis": "your-redis.redis.cache.windows.net:6380,password=key,ssl=True,abortConnect=False"
  }
}

Eviction Policies

Configure Redis eviction:
# In redis.conf
maxmemory 256mb
maxmemory-policy allkeys-lru

Persistence

Choose persistence strategy:
# RDB: Point-in-time snapshots
save 900 1
save 300 10
save 60 10000

# AOF: Append-only file
appendonly yes
appendfsync everysec

PostgreSQL Setup

Configure Marten document storage

StackExchange.Redis

Official Redis client documentation

Build docs developers (and LLMs) love