5 min readBy Muhammad Shahid
Caching System in .NET: IMemoryCache, Redis, and Object Cache
Caching system explained for .NET and Java developers — object cache, in-memory vs distributed cache, IMemoryCache vs Redis on ASP.NET Core, compared to Java caching patterns.
Part of Caching
Quick answers
- What is a caching system?
- A caching system stores copies of frequently used data in fast storage (RAM, Redis) so reads avoid slower sources like SQL or HTTP. It includes the cache store, keys, TTL, eviction, invalidation on writes, and hit/miss metrics.
- What is a Java object cache?
- In Java, an object cache holds deserialized objects in memory — e.g. Caffeine, Ehcache, or a ConcurrentHashMap with TTL. In .NET the equivalent is IMemoryCache or IDistributedCache storing serialized or in-memory objects per key.
- What is the Java caching system vs .NET?
- Same concepts: in-process object cache (Caffeine / IMemoryCache) vs distributed cache (Redis for both). .NET uses IDistributedCache abstraction; Java often uses Spring Cache or Redisson. Both need tenant-aware keys and invalidation on writes.
- When use IMemoryCache vs Redis in ASP.NET Core?
- IMemoryCache for single-instance, fast, local object cache with size limits. Redis (IDistributedCache) when multiple API instances must share cache entries or you need a centralized TTL store.
Search java caching system or java object cache and you get Caffeine, Ehcache, and Spring @Cacheable. On ASP.NET Core, the same ideas apply with IMemoryCache and Redis — this page maps both ecosystems so the concepts transfer.
Start with: what is a cache miss. Redis production patterns: Redis caching ASP.NET Core.
What is a caching system?
Definition: A caching system is infrastructure that stores reusable data closer to the consumer than the authoritative source, with rules for lookup, expiry, eviction, and invalidation.
Client → API → [ CACHE ] → SQL / HTTP / disk
↑
hit = fast
miss = load origin (see cache miss guide)
Layers in a typical ASP.NET Core product:
| Layer | Technology | Scope |
|---|---|---|
| Browser / CDN | HTTP cache headers | Public static assets |
| In-process object cache | IMemoryCache | One API instance |
| Distributed object cache | Redis + IDistributedCache | All API instances |
| Database | SQL Server buffer pool | Server-side (not your app code) |
Java object cache vs .NET object cache
Java object cache usually means keeping Java objects in heap memory keyed by id — avoid repeated DB round-trips for the same entity graph.
.NET equivalent:
// In-process object cache — ASP.NET Core
public class FeeScheduleService
{
private readonly IMemoryCache _cache;
private readonly AppDbContext _db;
public async Task<FeeScheduleDto?> GetAsync(Guid tenantId, Guid scheduleId, CancellationToken ct)
{
var key = $"tenant:{tenantId}:feeschedule:{scheduleId}:v1";
if (_cache.TryGetValue(key, out FeeScheduleDto? cached))
return cached; // object cache HIT
var row = await _db.FeeSchedules
.AsNoTracking()
.FirstOrDefaultAsync(f => f.TenantId == tenantId && f.Id == scheduleId, ct);
if (row is null) return null;
var dto = Map(row);
_cache.Set(key, dto, TimeSpan.FromMinutes(10));
return dto;
}
}
Register cache:
builder.Services.AddMemoryCache();
| Concept | Java | .NET / ASP.NET Core |
|---|---|---|
| In-process object cache | Caffeine, Ehcache, ConcurrentHashMap | IMemoryCache |
| Annotation-driven cache | @Cacheable (Spring) | Manual or third-party (FusionCache, LazyCache) |
| Distributed cache | Redis + Redisson / Spring Data Redis | IDistributedCache + StackExchange.Redis |
| Serialize for Redis | JSON, Kryo | JSON, MessagePack |
| Stampede control | Caffeine refreshAfterWrite | Lock / single-flight (Redis post) |
Java caching system (mapped to .NET)
A full Java caching system in enterprise apps often includes:
- Local L1 — Caffeine per JVM
- Shared L2 — Redis cluster
- Cache-aside — app reads cache, on miss loads DB, writes cache
- Write-through / write-behind — less common on CRUD APIs; know the terms
.NET ASP.NET Core mirror:
// L1 + L2 pattern (simplified)
public async Task<CatalogDto> GetCatalogAsync(Guid tenantId, CancellationToken ct)
{
var key = $"tenant:{tenantId}:catalog:v1";
if (_memory.TryGetValue(key, out CatalogDto? local))
return local!;
var json = await _distributed.GetStringAsync(key, ct);
if (json is not null)
{
var fromRedis = JsonSerializer.Deserialize<CatalogDto>(json)!;
_memory.Set(key, fromRedis, TimeSpan.FromMinutes(1));
return fromRedis;
}
var fresh = await LoadFromSqlAsync(tenantId, ct);
var payload = JsonSerializer.Serialize(fresh);
await _distributed.SetStringAsync(key, payload, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15),
}, ct);
_memory.Set(key, fresh, TimeSpan.FromMinutes(1));
return fresh;
}
L1 (IMemoryCache) cuts Redis round-trips. L2 (Redis) shares state across App Service instances.
IMemoryCache — when to use object cache in-process
Good for:
- Reference data with moderate churn on single instance or per-instance optimization
- Parsed config, feature flags, small lookup tables
- Short TTL memoization of expensive read-only computation
Avoid:
- Storing unbounded graphs — set
SizeLimitand entry size - Multi-tenant data without
tenantIdin the key (ConcurrentDictionary pitfalls) - Assuming L1 is shared across servers — it is not
builder.Services.AddMemoryCache(options =>
{
options.SizeLimit = 10_000;
});
Redis — distributed caching system
When you scale to multiple Kestrel workers or App Service instances, in-process object cache diverges — each node has its own copy. Redis becomes the shared caching system.
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration["Redis:Connection"];
options.InstanceName = "myapp:";
});
Full patterns: Redis caching ASP.NET Core.
Connection failures: error establishing a Redis connection.
Cache-aside (the pattern both stacks use)
1. GET key from cache
2. if HIT → return
3. if MISS → query SQL
4. SET key with TTL
5. return
This is the default for catalog lists, fee schedules, and dashboard aggregates when stale reads are acceptable for a few minutes.
Invalidation — caching system completeness
A caching system without invalidation is a stale data machine:
| Write event | Action |
|---|---|
| Update entity | Remove entity key + list keys |
| Bulk import | Bump key version v1 → v2 |
| Permission change | Invalidate user/role caches |
If you cannot name the invalidation rule, do not cache the read yet.
Choosing your caching system stack
One API instance, small data → IMemoryCache only
Multiple instances, shared reads → Redis (IDistributedCache)
Need L1 speed + L2 sharing → IMemoryCache + Redis
Real-time per-user clinical data → often no cache; tune SQL
If an interviewer asks
"Java object cache vs Redis?"
Object cache = in-process heap (fast, local). Redis = network shared store (consistent across nodes).
"IMemoryCache vs ConcurrentDictionary?"
IMemoryCache has TTL and size limits. ConcurrentDictionary is a thread-safe map — no eviction unless you build it.
"Cache miss implications?"
What is a cache miss — miss runs full SQL; stampede if uncontrolled.
Hub: caching.