How does this ConcurrentDictionary LazyTaskT code work?
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.
Introduction
The ConcurrentDictionary<TKey, Lazy<Task<T>>> pattern is a way to deduplicate expensive asynchronous work by key. The goal is simple: if multiple callers ask for the same value at the same time, only one asynchronous operation should be created and everyone else should await the same task. ConcurrentDictionary handles thread-safe lookup and insertion, while Lazy ensures the stored task is created only once.
The Usual Shape of the Pattern
A typical implementation looks like this:
This code means:
- look up the key in a thread-safe dictionary
- if the key is missing, insert a
Lazy<Task<TValue>> - when
.Valueis accessed, create the task once - every caller for the same key awaits the same task instance
That is the high-level idea.
Why Not Store Task<T> Directly?
You could store Task<T> directly, but then the factory often runs before you know whether your new value actually won the race to be inserted.
Lazy<Task<T>> delays task creation until the chosen dictionary value is actually used.
That matters because ConcurrentDictionary.GetOrAdd may invoke the value factory multiple times under contention, even though only one value ends up stored.
So Lazy gives you a second layer of protection:
- dictionary chooses one winner
- '
Lazyensures the winning entry creates its task only once'
Without Lazy, duplicate work is much easier to trigger accidentally.
Step-by-Step Race Scenario
Suppose two threads request key "user:42" at nearly the same time.
- both threads call
GetOrAdd - each may construct a
Lazy<Task<T>>candidate - only one candidate is stored in the dictionary
- both threads receive the stored
Lazyinstance - both call
.Value - the
Lazyvalue factory runs only once - both callers await the same
Task<T>
That is the whole trick.
The dictionary resolves the insertion race. The Lazy resolves the initialization race.
Why This Helps
This pattern is useful when the underlying async operation is expensive or should not run in parallel for the same key.
Examples:
- loading user profiles by ID
- fetching and caching configuration by tenant
- downloading one remote resource once and sharing the result
- ensuring only one database warm-up query runs per key
It is especially attractive when duplicate work is wasteful or could produce unnecessary load.
Failure Behavior Needs Thought
A subtle issue is what happens if the stored task fails.
With the basic pattern, the failed task remains cached. That means future callers for the same key will immediately see the same exception instead of retrying.
Sometimes that is correct. Sometimes it is not.
If you want retries after failure, remove the entry when the task faults.
This is often the missing detail in simplified blog examples.
Thread Safety and LazyThreadSafetyMode
The default Lazy<T> constructor uses thread-safe initialization, which is usually what you want here. That means multiple threads can safely call .Value, and only one will run the factory.
If you change the lazy mode carelessly, you can break the guarantee this pattern depends on.
So the default behavior is usually the right choice unless you have a very specific reason to customize it.
Do Not Confuse This with a Full Cache Policy
This pattern deduplicates in-flight work. It does not automatically solve:
- expiration
- eviction policy
- stale data refresh
- memory bounds
- cancellation strategy
Those are separate cache design questions.
So ConcurrentDictionary<TKey, Lazy<Task<T>>> is best thought of as a concurrency pattern, not a full-featured cache system by itself.
Common Pitfalls
The biggest pitfall is assuming GetOrAdd alone prevents duplicate factory work. It does not necessarily do that if the expensive work starts before insertion wins.
Another issue is forgetting that failed tasks may stay cached forever unless you remove them deliberately.
Developers also sometimes use this pattern where plain memoization would have been enough, adding complexity without a concurrency problem to solve.
Finally, do not forget that the task itself may still need cancellation, timeout, or cleanup logic. The dictionary pattern does not provide those automatically.
Summary
- '
ConcurrentDictionaryhandles thread-safe key lookup and insertion.' - '
Lazy<Task<T>>ensures the stored async operation starts only once for that key.' - Together, they let multiple callers await one shared in-flight task.
- This pattern is great for deduplicating expensive keyed async work.
- Think explicitly about failure caching, retries, and cache policy rather than assuming the pattern solves everything by itself.
Related reading
- How does thread pooling works, and how to implement it in an async/await env like NodeJS?
- How does ThreadPoolExecutor.map differ from ThreadPoolExecutor.submit?
- How efficient is locking an unlocked mutex? What is the cost of a mutex?
- How exactly is a coroutine suspended?
- How does TransactionScope roll back transactions?
- How does WCF deserialization instantiate objects without calling a constructor?
- How expensive is the lock statement?
- How expensive is the lock statement?

OOD Fundamentals
Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.