How to create and call async version of caching GetOrSet method?
System Design practice on Codemia
Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.
Introduction
An async GetOrSet cache method should do more than just wrap a synchronous cache lookup in Task.Run. The real goal is to avoid blocking while computing the missing value and to prevent several callers from doing the same expensive async work at once. A good design therefore combines asynchronous factories with stampede protection.
Define the Async Contract Clearly
A typical async cache API takes:
- a cache key
- an asynchronous factory that produces the value on a miss
- optional expiration settings
The factory should return Task<T>, not T, so callers can await genuine asynchronous I O such as database or HTTP work.
Example with ConcurrentDictionary and Lazy<Task<T>>
One practical pattern is to use a ConcurrentDictionary that stores lazy tasks per key.
Usage:
The important property is that concurrent callers for the same key share the same in-flight task instead of starting duplicate work.
Handle Failures Carefully
If the factory throws, you often do not want to keep a failed task cached forever. One approach is to remove the entry on failure.
Without this, one transient error can poison the cache entry.
Calling the Async Cache Method
Callers should simply await it.
Do not call .Result or .Wait() from synchronous code unless you have a very controlled environment. That defeats the async design and can cause thread-blocking problems.
If You Already Use IMemoryCache
In ASP.NET Core, IMemoryCache can be combined with async factories, but you still need to think about duplicate concurrent misses. A simple TryGetValue plus async set is not enough if many requests can race on the same key.
That is why stampede protection matters more than the surface signature of the method. The hard problem is not storing the value. It is coordinating concurrent cache misses safely.
Decide What Happens on Expiration and Refresh
A practical cache design also needs a policy for expiration and stale refresh. Async access changes the call shape, but it does not remove the need to choose when values expire, whether stale values may be served briefly, and how expensive refresh work is coordinated. Those decisions often matter more than the method signature itself.
Common Pitfalls
- Wrapping synchronous cache code in
Task.Runand calling it async. - Allowing several concurrent misses to invoke the expensive factory at the same time.
- Caching a failed task permanently after one exception.
- Calling the async cache method synchronously with
.Resultor.Wait(). - Forgetting expiration and invalidation just because the cache access became asynchronous.
Summary
- An async
GetOrSetmethod should accept an async factory and returnTask<T>. - The main design challenge is preventing duplicate in-flight work for the same key.
- '
ConcurrentDictionaryplusLazy<Task<T>>is a practical stampede-protection pattern.' - Failed tasks usually should be removed so retries can succeed later.
- The async method should be awaited end to end rather than forced back into synchronous calling code.
Related reading
- how to deal with replication lag in microservices
- How to decide Kafka Cluster size
- How to deploy Kafka Stream applications on Kubernetes?
- How to derive a sequence number in paxos
- How to create async function using NAPI that return Promises
- How to create dispatch queue in Swift 3
- How To Design a Distributed Logging System in Kubernetes?
- How to design a distributed write-heavy data store

System Design Fundamentals
Build a strong foundation in designing scalable, reliable distributed systems.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
System Design practice on Codemia
Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.