ConcurrentDictionary
Lazy<Task\`\`\`\`<T>\`\`\`\`>
C#
asynchronous programming
multithreading

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.

Browse interview questions

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:

csharp
1using System;
2using System.Collections.Concurrent;
3using System.Threading.Tasks;
4
5public class CacheByKey<TKey, TValue>
6{
7    private readonly ConcurrentDictionary<TKey, Lazy<Task<TValue>>> _map = new();
8
9    public Task<TValue> GetOrAddAsync(TKey key, Func<TKey, Task<TValue>> factory)
10    {
11        var lazy = _map.GetOrAdd(
12            key,
13            k => new Lazy<Task<TValue>>(() => factory(k))
14        );
15
16        return lazy.Value;
17    }
18}

This code means:

  • look up the key in a thread-safe dictionary
  • if the key is missing, insert a Lazy<Task<TValue>>
  • when .Value is 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
  • 'Lazy ensures 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.

  1. both threads call GetOrAdd
  2. each may construct a Lazy<Task<T>> candidate
  3. only one candidate is stored in the dictionary
  4. both threads receive the stored Lazy instance
  5. both call .Value
  6. the Lazy value factory runs only once
  7. 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.

csharp
1public Task<TValue> GetOrAddAsync(TKey key, Func<TKey, Task<TValue>> factory)
2{
3    var lazy = _map.GetOrAdd(
4        key,
5        k => new Lazy<Task<TValue>>(() => factory(k))
6    );
7
8    return Observe(key, lazy);
9}
10
11private async Task<TValue> Observe(TKey key, Lazy<Task<TValue>> lazy)
12{
13    try
14    {
15        return await lazy.Value;
16    }
17    catch
18    {
19        _map.TryRemove(key, out _);
20        throw;
21    }
22}

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

  • 'ConcurrentDictionary handles 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
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track 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.

Browse interview questions

All Rights Reserved.