DEV Community

Cover image for 🧱 Lesson 6  - Redis Caching for Performance Optimization
Farrukh Rehman
Farrukh Rehman

Posted on

🧱 Lesson 6  - Redis Caching for Performance Optimization

Series: From Code to Cloud: Building a Production-Ready .NET Application
By: Farrukh Rehman - Senior .NET Full Stack Developer / Team Lead
LinkedIn: https://linkedin.com/in/farrukh-rehman
GitHub: https://github.com/farrukh1212cs

Source Code Backend : https://github.com/farrukh1212cs/ECommerce-Backend.git

Source Code Frontend : https://github.com/farrukh1212cs/ECommerce-Frontend.git

🎯 Introduction

In modern e-commerce applications, performance is critical — users expect fast responses, low latency, and instant data access.
One of the most effective ways to achieve this is by introducing caching.

In this lesson, we’ll integrate Redis, an in-memory data store, to cache frequently accessed data (like products, categories, and user sessions) and reduce database load.

Redis helps you:

  • Decrease response times ⚡
  • Reduce the number of database calls 📉
  • Handle high traffic efficiently 🚀

By the end of this lesson, your e-commerce system will have a working Redis integration ready for caching services.

🧩 Step 1: Pull Redis Docker Image

Let’s begin by setting up Redis locally using Docker.
Run the following command in your terminal:

docker pull redis:latest

Once the image is downloaded, start the Redis container:

docker run --name ecommerce-redis -p 6379:6379 -d redis

You can verify that Redis is running using:

docker ps

⚙️ Step 2: Install Redis Package

In your ECommerce.Infrastructure project, install the official Redis client for .NET — StackExchange.Redis.

dotnet add ECommerce.Infrastructure package StackExchange.Redis

🧠 Step 3: Configure Redis in appsettings.json
Add your Redis connection string:

 "DatabaseProvider": "PostgreSQL",
 "ConnectionStrings": {
   "PostgreSQL": "Host=localhost;Port=5432;Database=ECommerceDb;Username=postgres;Password=Admin123!",
   "MySQL": "Server=localhost;Port=3306;Database=ECommerceDb;User=root;Password=Admin123!;",
   "SqlServer": "Server=localhost,1433;Database=ECommerceDb;User Id=sa;Password=Admin123!;TrustServerCertificate=True;",
   "Redis": "localhost:6379"
 }
Enter fullscreen mode Exit fullscreen mode

🧩 Step 4 : Create Interface ICacheService.cs
Path : ECommerce.Application/Services/Interfaces/ICacheService.cs

namespace ECommerce.Application.Services.Interfaces;

public interface ICacheService
{
    Task SetAsync<T>(string key, T value, TimeSpan? expiry = null);
    Task<T?> GetAsync<T>(string key);
    Task RemoveAsync(string key);
}

Enter fullscreen mode Exit fullscreen mode

✅ Purpose:

  • Defines a simple contract for caching.
  • Keeps Redis implementation hidden behind the interface.
  • Allows you to switch to another caching provider (e.g., MemoryCache, DistributedCache) in the future.

⚙️ Step 5 : Implement the Interface in RedisCacheService.cs
Path : ECommerce.Infrastructure/Caching/RedisCacheService.cs

using ECommerce.Application.Services.Interfaces;
using StackExchange.Redis;
using System.Text.Json;

namespace ECommerce.Infrastructure.Caching;

public class RedisCacheService : ICacheService
{
    private readonly IDatabase _database;

    public RedisCacheService(IConnectionMultiplexer redis)
    {
        _database = redis.GetDatabase();
    }

    public async Task SetAsync<T>(string key, T value, TimeSpan? expiry = null)
    {
        var json = JsonSerializer.Serialize(value);
        await _database.StringSetAsync(key, json, expiry);
    }

    public async Task<T?> GetAsync<T>(string key)
    {
        var value = await _database.StringGetAsync(key);
        return value.IsNullOrEmpty ? default : JsonSerializer.Deserialize<T>(value!);
    }

    public async Task RemoveAsync(string key)
    {
        await _database.KeyDeleteAsync(key);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 6 : Updated DependencyInjection.cs

using ECommerce.Application.Services.Interfaces;
using ECommerce.Infrastructure.Caching;
using ECommerce.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;

namespace ECommerce.Infrastructure;

public static class DependencyInjection
{
    public static IServiceCollection AddInfrastructure(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        var provider = configuration["DatabaseProvider"] ?? "MySQL";

        if (string.Equals(provider, "SqlServer", StringComparison.OrdinalIgnoreCase))
        {
            var conn = configuration.GetConnectionString("SqlServer");
            services.AddDbContext<AppDbContext, SqlServerDbContext>(options =>
                options.UseSqlServer(conn));
        }
        else if (string.Equals(provider, "MySQL", StringComparison.OrdinalIgnoreCase))
        {
            var conn = configuration.GetConnectionString("MySQL");
            services.AddDbContext<AppDbContext, MySqlDbContext>(options =>
                options.UseMySql(conn, ServerVersion.AutoDetect(conn)));
        }
        else if (string.Equals(provider, "PostgreSQL", StringComparison.OrdinalIgnoreCase))
        {

            var conn = configuration.GetConnectionString("PostgreSQL");
            services.AddDbContext<AppDbContext, PostgresDbContext>(options =>
                options.UseNpgsql(conn));
        }
        else
        {
            throw new InvalidOperationException($"Unsupported provider: {provider}");
        }

        // ✅ Redis cache setup
        var redisConnection = configuration.GetConnectionString("Redis");
        if (!string.IsNullOrEmpty(redisConnection))
        {
            services.AddSingleton<IConnectionMultiplexer>(sp =>
                ConnectionMultiplexer.Connect(redisConnection));

            services.AddSingleton<ICacheService, RedisCacheService>();
        }

        return services;
    }
}

Enter fullscreen mode Exit fullscreen mode

Step 7 : Updated ProductsController with Redis Cache Support

using ECommerce.Application.DTOs;
using ECommerce.Application.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;

namespace ECommerce.API.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly ICacheService _cacheService;

    public ProductsController(IProductService productService, ICacheService cacheService)
    {
        _productService = productService;
        _cacheService = cacheService;
    }

    [HttpGet]
    public async Task<ActionResult<IEnumerable<ProductDto>>> GetAll()
    {
        const string cacheKey = "products:all";

        // Try get from cache
        var cachedProducts = await _cacheService.GetAsync<IEnumerable<ProductDto>>(cacheKey);
        if (cachedProducts != null)
        {
            return Ok(new
            {
                fromCache = true,
                data = cachedProducts
            });
        }

        // Get from DB if not cached
        var products = await _productService.GetAllAsync();

        // Cache for 60 minutes
        await _cacheService.SetAsync(cacheKey, products, TimeSpan.FromMinutes(60));

        return Ok(new
        {
            fromCache = false,
            data = products
        });
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<ProductDto>> GetById(Guid id)
    {
        var cacheKey = $"product:{id}";

        // Check cache
        var cachedProduct = await _cacheService.GetAsync<ProductDto>(cacheKey);
        if (cachedProduct != null)
        {
            return Ok(new
            {
                fromCache = true,
                data = cachedProduct
            });
        }

        // Fetch from service
        var product = await _productService.GetByIdAsync(id);
        if (product == null)
            return NotFound();

        // Cache for 60 minutes
        await _cacheService.SetAsync(cacheKey, product, TimeSpan.FromMinutes(60));

        return Ok(new
        {
            fromCache = false,
            data = product
        });
    }

    [HttpPost]
    public async Task<ActionResult> Create(ProductDto dto)
    {
        await _productService.AddAsync(dto);

        // Invalidate cache
        await _cacheService.RemoveAsync("products:all");

        return CreatedAtAction(nameof(GetById), new { id = dto.Id }, dto);
    }

    [HttpPut("{id}")]
    public async Task<ActionResult> Update(Guid id, ProductDto dto)
    {
        if (id != dto.Id)
            return BadRequest("Mismatched product ID.");

        await _productService.UpdateAsync(dto);

        // Invalidate caches
        await _cacheService.RemoveAsync("products:all");
        await _cacheService.RemoveAsync($"product:{id}");

        return NoContent();
    }

    [HttpDelete("{id}")]
    public async Task<ActionResult> Delete(Guid id)
    {
        await _productService.DeleteAsync(id);

        // Invalidate caches
        await _cacheService.RemoveAsync("products:all");
        await _cacheService.RemoveAsync($"product:{id}");

        return NoContent();
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Key Notes

Caching scope:

  • GetAll → caches list of products (products:all)
  • GetById → caches single product (product:{id})
  • Invalidation logic:
  • When a product is created, updated, or deleted, relevant keys are removed.
  • Cache TTL (Time to Live):
  • Currently set to 60 minutes

Step 8 : Final Testing

Test on GetAll

Get By Id

Next Lecture Preview
Lecture 7 : Message Queues with RabbitMQ

Implementing event-driven communication, background jobs, and asynchronous processing.

Top comments (0)