In-memory cache and IMemoryCache are the .NET names for keeping hot objects in process. If you come from Java (Caffeine, Ehcache, Spring @Cacheable), the mental model is the same — this page maps both ecosystems. On ASP.NET Core, pair IMemoryCache (local) with Redis (IDistributedCache) when multiple instances must share entries.
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.
In-memory cache vs Redis (IMemoryCache vs IDistributedCache)
IMemoryCache is a sticky note on your monitor — fast, invisible to the next desk, gone when the process recycles. Redis is the whiteboard in the hallway every App Service instance can read. Label the note with tenantId or another tenant inherits yesterday's fee schedule.
In-memory cache / in-memory caching on ASP.NET Core means IMemoryCache — objects live in the process heap.
Redis as a distributed cache means IDistributedCache — bytes (usually JSON) live in a shared store.
public sealed class FeeScheduleCache(IMemoryCache memory, IDistributedCache redis)
{
public async Task<FeeScheduleDto?> GetAsync(Guid tenantId, Guid id, CancellationToken ct)
{
var key = $"tenant:{tenantId}:fee:{id}:v1";
if (memory.TryGetValue(key, out FeeScheduleDto? local))
return local;
var json = await redis.GetStringAsync(key, ct);
if (json is not null)
{
var dto = JsonSerializer.Deserialize<FeeScheduleDto>(json)!;
memory.Set(key, dto, TimeSpan.FromMinutes(1));
return dto;
}
return null; // caller loads SQL, then SetAsync
}
}
One API instance, small lookups → IMemoryCache only
Two+ instances, shared reads → Redis (IDistributedCache)
Need L1 speed + L2 sharing → IMemoryCache + Redis
When to use IMemoryCache only: one API instance, small lookups, you can name a size limit.
When to add Redis: two or more instances must share the same keys.
When not to cache at all: per-user clinical records keyed only by URL, checkout prices that must be exact after a promo change, anything you cannot name an invalidation rule for.
IMemoryCache vs IDistributedCache is that scope difference — not a quality ranking. Production Redis patterns: Redis caching ASP.NET Core.
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.