C# & .NET

C# Async, Await, Race Conditions and Locks: A Senior Developer's Practical Guide

Afzal AhmedFaz Ahmed
·24 July 2026·15 min read
C#ASP.NET Coreasync/awaitTaskSemaphoreSlimInterlockedThread SafetyConcurrency

Why This Matters

My practical revision guide to async and await, Tasks, thread-pool behaviour, race conditions, locks, SemaphoreSlim, Interlocked and safe ASP.NET Core concurrency.

C# Async, Await, Race Conditions and Locks: A Senior Developer’s Practical Guide

A Question Every Developer Should Ask

"Faz, I use async and await every day, but I still do not feel I understand what is happening underneath."

That is one of the best questions a junior developer can ask. Async programming is easy to copy and surprisingly hard to understand properly. You can write code that compiles, calls await, and still block threads, lose exceptions, overload downstream APIs or create race conditions.

The goal is not to memorise every threading API in .NET. The goal is to understand what kind of problem you have, then choose the smallest safe tool.

Is Async the Same as Multithreading?

No. They are related, but they are not the same thing.

Async is mainly about non-blocking waiting. Multithreading is about work executing on multiple threads.

Think of a waiter in a restaurant. A blocking waiter takes an order and then stands beside the kitchen for twenty minutes. Nobody else gets served. An async waiter gives the order to the kitchen, serves other tables and returns when the food is ready.

The waiter did not become five waiters. They simply stopped wasting time waiting.

public async Task<string> GetDataAsync()
{
    return await httpClient.GetStringAsync(
        "https://example.com");
}

This does not mean, "Create a new thread." It means start the HTTP operation, do not block a thread while the network responds, then resume the method when the response is ready.

This is why async is valuable for database calls, HTTP calls, file access, message queues, cloud storage and other I/O operations.

Threads, the Thread Pool and Tasks

A thread is an execution path managed by the operating system and .NET runtime. In ASP.NET Core, incoming requests use reusable thread-pool threads. If a request blocks a thread with .Result, .Wait() or expensive synchronous work, that thread cannot help process another request.

A Task represents work that may complete later. A Task represents work that will eventually produce a value.

Task SaveAsync()
{
    // Completes later; no returned value.
}

Task<Customer> GetCustomerAsync()
{
    // Completes later and returns a Customer.
}

When you write var customer = await GetCustomerAsync();, you are saying: pause this method until the operation completes, but do not block the thread while it waits.

The async keyword allows a method to use await. The compiler splits the method into continuation points.

public async Task ProcessAsync()
{
    Console.WriteLine("Before");

    await Task.Delay(1000);

    Console.WriteLine("After");
}

The practical flow is: write Before, start the delay, pause the method, return control to the caller, then resume when the delay finishes.

Use Await, Not .Result or .Wait()

This is a warning sign:

var customer = GetCustomerAsync(id).Result;

.Result blocks the current thread until the task completes. In ASP.NET Core, this wastes thread-pool threads. In older application types with a synchronization context, it can also cause deadlocks.

The normal rule is simple: use await, not .Result or .Wait().

Async does not make a database query intrinsically faster. A query that takes 200 milliseconds still takes 200 milliseconds. Async improves throughput because the application does not waste a thread while waiting.

Do Not Forget to Await Important Work

This code starts an email task but does not wait for it:

public async Task ProcessOrderAsync()
{
    SendEmailAsync();

    await SaveOrderAsync();
}

This is fire-and-forget behaviour. If the email fails, the exception may be difficult to observe. If the application stops, it may never finish.

The straightforward version is:

public async Task ProcessOrderAsync()
{
    await SaveOrderAsync();
    await SendEmailAsync();
}

If operations are genuinely independent, start both and wait for both:

public async Task ProcessOrderAsync()
{
    Task saveTask = SaveOrderAsync();
    Task emailTask = SendEmailAsync();

    await Task.WhenAll(saveTask, emailTask);
}

Task.WhenAll does not start tasks. The method calls create or start the operations; WhenAll waits for them to finish.

Concurrency Is Not Always Parallelism

This runs sequentially because each await waits before the next operation starts:

var customer = await GetCustomerAsync(id);
var orders = await GetOrdersAsync(id);
var invoices = await GetInvoicesAsync(id);

This starts independent operations together:

Task<Customer> customerTask = GetCustomerAsync(id);
Task<List<Order>> ordersTask = GetOrdersAsync(id);
Task<List<Invoice>> invoicesTask = GetInvoicesAsync(id);

await Task.WhenAll(customerTask, ordersTask, invoicesTask);

Customer customer = await customerTask;
List<Order> orders = await ordersTask;
List<Invoice> invoices = await invoicesTask;

The mentoring distinction is:

Async      → do not block while waiting
Concurrency → manage multiple operations in progress
Parallelism → execute multiple pieces of work at the same time

They overlap, but they are not interchangeable.

Task.Run: Use It for CPU Work, Not I/O

Task.Run schedules work on a thread-pool thread. It is useful for CPU-bound work such as compression, image processing, large calculations and transformations.

public async Task<int> CalculateReportAsync(
    IReadOnlyCollection<int> numbers)
{
    return await Task.Run(() =>
        numbers.Sum(ExpensiveCalculation));
}

This is not a good way to make I/O asynchronous:

public async Task<Customer?> GetCustomerAsync(int id)
{
    return await Task.Run(() =>
        _db.Customers.Find(id));
}

The database call is still synchronous. You simply moved blocked work to another thread. Prefer the genuine async API:

public async Task<Customer?> GetCustomerAsync(
    int id,
    CancellationToken cancellationToken)
{
    return await _db.Customers.FindAsync(
        [id],
        cancellationToken);
}

In ASP.NET Core, think carefully before running heavy CPU work in the web process. A background worker, queue, Azure Function or separate service may be a better design.

What Is a Race Condition?

A race condition occurs when multiple operations access shared mutable state, at least one changes it, and the final result depends on timing.

private int _counter;

public async Task IncrementAsync()
{
    var current = _counter;

    await Task.Delay(10);

    _counter = current + 1;
}

If two callers run this concurrently, both can read the same value before either writes back.

Counter is 10

Operation A reads 10
Operation B reads 10

Operation A writes 11
Operation B writes 11

You expected 12. You received 11.

The real danger is shared mutable state: a singleton-service field, in-memory cache entry, static counter, shared list, stock quantity or account balance. Whenever more than one operation can reach changing data, ask whether they can arrive at the same time.

Use Lock for Short, Synchronous Critical Sections

A critical section is the smallest piece of code that reads or changes shared mutable state.

public sealed class CounterService
{
    private int _counter;
    private readonly Lock _counterLock = new();

    public void Increment()
    {
        lock (_counterLock)
        {
            _counter++;
        }
    }
}

Only one thread can enter the lock at a time. In modern .NET, a dedicated System.Threading.Lock is a strong choice for synchronous locking. For older target frameworks, use a private object.

private readonly object _counterLock = new();

Never lock on this, a public object or a string. Keep the locked section small. Do not make database calls, HTTP calls, slow calculations or email operations while holding a normal lock.

Use SemaphoreSlim When the Critical Section Awaits

You cannot await inside a normal lock. If protected work must await, use SemaphoreSlim.

public sealed class OrderNumberGenerator
{
    private int _lastOrderNumber = 1000;
    private readonly SemaphoreSlim _gate = new(1, 1);

    public async Task<int> GetNextOrderNumberAsync(
        CancellationToken cancellationToken)
    {
        await _gate.WaitAsync(cancellationToken);

        try
        {
            await SaveAuditEntryAsync(cancellationToken);

            _lastOrderNumber++;

            return _lastOrderNumber;
        }
        finally
        {
            _gate.Release();
        }
    }
}

The pattern is always wait, try, finally and release. If an exception skips Release, later callers can wait forever.

Use Interlocked for Simple Atomic Updates

A simple counter does not need a full lock.

public sealed class RequestStatsService
{
    private int _totalRequests;

    public void Increment()
    {
        Interlocked.Increment(ref _totalRequests);
    }

    public int GetTotalRequests()
    {
        return Volatile.Read(ref _totalRequests);
    }
}

Use Interlocked for small atomic operations such as incrementing, decrementing, adding numeric totals and swapping references. Use a lock when multiple related steps must remain consistent.

public sealed class BankAccount
{
    private decimal _balance;
    private readonly Lock _balanceLock = new();

    public bool Withdraw(decimal amount)
    {
        lock (_balanceLock)
        {
            if (_balance < amount)
            {
                return false;
            }

            _balance -= amount;
            return true;
        }
    }
}

Checking the balance and subtracting the amount is one business rule, so it must be protected together.

ASP.NET Core Service Lifetimes Matter

A singleton exists for the life of the application:

builder.Services.AddSingleton<MyMutableService>();

Mutable fields in a singleton may be reached by many requests at once. Race conditions are therefore especially likely there.

Transient → a new instance when requested
Scoped    → one instance per web request
Singleton → one shared instance for the whole application

Avoid shared mutable state where possible. Put durable business data in a database or cache. Use scoped services for request-specific state. Synchronize shared in-memory data only when it truly belongs in memory.

Concurrent Collections and Channels

List and Dictionary are not safe for concurrent writes. Use a concurrent collection when the access pattern fits.

private readonly ConcurrentDictionary<string, int> _scores = new();

public void AddRun(string player)
{
    _scores.AddOrUpdate(
        player,
        addValue: 1,
        updateValueFactory: (_, score) => score + 1);
}

For producer-consumer workflows, Channel is often clearer than a network of locks. A request produces work, a channel stores it, and a background worker consumes it. This fits notifications, imports, audit records and report generation.

Cancellation and Bounded Concurrency

Async methods that may wait or run for a meaningful time should usually accept a CancellationToken.

[HttpGet]
public async Task<IActionResult> Get(
    CancellationToken cancellationToken)
{
    var customers = await _db.Customers
        .ToListAsync(cancellationToken);

    return Ok(customers);
}

Do not start thousands of operations merely because Task.WhenAll makes it easy.

using var gate = new SemaphoreSlim(10);

var tasks = customers.Select(async customer =>
{
    await gate.WaitAsync(cancellationToken);

    try
    {
        await SendEmailAsync(customer, cancellationToken);
    }
    finally
    {
        gate.Release();
    }
});

await Task.WhenAll(tasks);

This limits concurrency to ten operations. The correct limit depends on your application, database, email provider and downstream services.

In-Memory Locks Do Not Protect Multiple Servers

A lock or SemaphoreSlim protects one application process only. If your API runs in three containers, each container has its own lock. They do not coordinate with each other.

For shared business data across instances, the real protection often belongs in durable storage. Use database transactions, unique constraints, optimistic concurrency tokens, conditional updates and idempotency keys.

A payment should not be processed twice merely because two requests reached two different containers at almost the same time. That rule belongs in transaction design, not only in a local lock.

The Practical Decision Guide

One simple numeric update
  → Interlocked

Short synchronous critical section
  → lock

Critical section that must await
  → SemaphoreSlim

Concurrent collection operations
  → System.Collections.Concurrent types

Producer-consumer workflow
  → Channel<T> or a durable queue

Shared data across application instances
  → transactions, constraints and optimistic concurrency

Avoidable shared state
  → redesign to remove it

Core Mentoring Advice

The first instinct after discovering a race condition is often, "Where should I add a lock?" Sometimes that is correct. More often, the better question is, "Why is this mutable state shared, and where should the truth of this business operation live?"

Async is about non-blocking waiting. Parallelism is about doing work at the same time. Thread safety is about protecting shared state. They solve different problems.

The best fix is rarely adding locks everywhere. It is minimising shared mutable state, making ownership clear and choosing the narrowest mechanism that protects the real business rule. That is how C# and ASP.NET Core systems behave reliably under load—not just on a development machine.


Applying Concurrency, Race Conditions and Locks in Modern C#

1. Distinguishing concurrency and parallelism

Distinguishing concurrency and parallelism is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Start by writing down the observable behaviour before selecting an API or framework feature. A concrete scenario gives the team a shared basis for discussing correctness.

Junior developer asks: “How can I tell whether I have handled distinguishing concurrency and parallelism correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for distinguishing concurrency and parallelism containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

2. Understanding interleaving

Understanding interleaving is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Treat this as a system boundary with explicit inputs, outputs, ownership and failure semantics. Hidden assumptions become production incidents when load or partial failure exposes them.

Junior developer asks: “How can I tell whether I have handled understanding interleaving correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for understanding interleaving containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

3. Recognising shared mutable state

Recognising shared mutable state is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Build a thin end-to-end example and measure it. Small experiments reveal scheduling, configuration and integration behaviour that cannot be settled by intuition alone.

Junior developer asks: “How can I tell whether I have handled recognising shared mutable state correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for recognising shared mutable state containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

4. Finding check-then-act races

Finding check-then-act races is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Junior developer asks: “How can I tell whether I have handled finding check-then-act races correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for finding check-then-act races containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

5. Understanding atomic operations

Understanding atomic operations is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Start by writing down the observable behaviour before selecting an API or framework feature. A concrete scenario gives the team a shared basis for discussing correctness.

Junior developer asks: “How can I tell whether I have handled understanding atomic operations correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for understanding atomic operations containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

6. Using Interlocked

Using Interlocked is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Treat this as a system boundary with explicit inputs, outputs, ownership and failure semantics. Hidden assumptions become production incidents when load or partial failure exposes them.

Junior developer asks: “How can I tell whether I have handled using interlocked correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for using interlocked containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

7. Using the lock statement

Using the lock statement is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Build a thin end-to-end example and measure it. Small experiments reveal scheduling, configuration and integration behaviour that cannot be settled by intuition alone.

Junior developer asks: “How can I tell whether I have handled using the lock statement correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for using the lock statement containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

8. Choosing a lock object

Choosing a lock object is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Junior developer asks: “How can I tell whether I have handled choosing a lock object correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for choosing a lock object containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

9. Keeping critical sections small

Keeping critical sections small is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Start by writing down the observable behaviour before selecting an API or framework feature. A concrete scenario gives the team a shared basis for discussing correctness.

Junior developer asks: “How can I tell whether I have handled keeping critical sections small correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for keeping critical sections small containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

10. Avoiding blocking inside locks

Avoiding blocking inside locks is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Treat this as a system boundary with explicit inputs, outputs, ownership and failure semantics. Hidden assumptions become production incidents when load or partial failure exposes them.

Junior developer asks: “How can I tell whether I have handled avoiding blocking inside locks correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for avoiding blocking inside locks containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

11. Using SemaphoreSlim

Using SemaphoreSlim is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Build a thin end-to-end example and measure it. Small experiments reveal scheduling, configuration and integration behaviour that cannot be settled by intuition alone.

Junior developer asks: “How can I tell whether I have handled using semaphoreslim correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for using semaphoreslim containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

12. Applying async-compatible coordination

Applying async-compatible coordination is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Junior developer asks: “How can I tell whether I have handled applying async-compatible coordination correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for applying async-compatible coordination containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

13. Understanding Monitor

Understanding Monitor is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Start by writing down the observable behaviour before selecting an API or framework feature. A concrete scenario gives the team a shared basis for discussing correctness.

Junior developer asks: “How can I tell whether I have handled understanding monitor correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for understanding monitor containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

14. Using ReaderWriterLockSlim carefully

Using ReaderWriterLockSlim carefully is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Treat this as a system boundary with explicit inputs, outputs, ownership and failure semantics. Hidden assumptions become production incidents when load or partial failure exposes them.

Junior developer asks: “How can I tell whether I have handled using readerwriterlockslim carefully correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for using readerwriterlockslim carefully containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

15. Working with concurrent collections

Working with concurrent collections is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Build a thin end-to-end example and measure it. Small experiments reveal scheduling, configuration and integration behaviour that cannot be settled by intuition alone.

Junior developer asks: “How can I tell whether I have handled working with concurrent collections correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for working with concurrent collections containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

16. Designing immutable state

Designing immutable state is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Junior developer asks: “How can I tell whether I have handled designing immutable state correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for designing immutable state containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

17. Using channels for ownership transfer

Using channels for ownership transfer is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Start by writing down the observable behaviour before selecting an API or framework feature. A concrete scenario gives the team a shared basis for discussing correctness.

Junior developer asks: “How can I tell whether I have handled using channels for ownership transfer correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for using channels for ownership transfer containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

18. Applying producer-consumer patterns

Applying producer-consumer patterns is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Treat this as a system boundary with explicit inputs, outputs, ownership and failure semantics. Hidden assumptions become production incidents when load or partial failure exposes them.

Junior developer asks: “How can I tell whether I have handled applying producer-consumer patterns correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for applying producer-consumer patterns containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

19. Understanding memory visibility

Understanding memory visibility is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Build a thin end-to-end example and measure it. Small experiments reveal scheduling, configuration and integration behaviour that cannot be settled by intuition alone.

Junior developer asks: “How can I tell whether I have handled understanding memory visibility correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for understanding memory visibility containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

20. Avoiding deadlocks

Avoiding deadlocks is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Junior developer asks: “How can I tell whether I have handled avoiding deadlocks correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for avoiding deadlocks containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

21. Recognising lock ordering problems

Recognising lock ordering problems is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Start by writing down the observable behaviour before selecting an API or framework feature. A concrete scenario gives the team a shared basis for discussing correctness.

Junior developer asks: “How can I tell whether I have handled recognising lock ordering problems correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for recognising lock ordering problems containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

22. Handling cancellation during coordination

Handling cancellation during coordination is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Treat this as a system boundary with explicit inputs, outputs, ownership and failure semantics. Hidden assumptions become production incidents when load or partial failure exposes them.

Junior developer asks: “How can I tell whether I have handled handling cancellation during coordination correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for handling cancellation during coordination containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

23. Handling exceptions in concurrent work

Handling exceptions in concurrent work is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Build a thin end-to-end example and measure it. Small experiments reveal scheduling, configuration and integration behaviour that cannot be settled by intuition alone.

Junior developer asks: “How can I tell whether I have handled handling exceptions in concurrent work correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for handling exceptions in concurrent work containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

24. Coordinating application startup

Coordinating application startup is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Junior developer asks: “How can I tell whether I have handled coordinating application startup correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for coordinating application startup containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

25. Protecting in-memory caches

Protecting in-memory caches is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Start by writing down the observable behaviour before selecting an API or framework feature. A concrete scenario gives the team a shared basis for discussing correctness.

Junior developer asks: “How can I tell whether I have handled protecting in-memory caches correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for protecting in-memory caches containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

26. Handling optimistic database concurrency

Handling optimistic database concurrency is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Treat this as a system boundary with explicit inputs, outputs, ownership and failure semantics. Hidden assumptions become production incidents when load or partial failure exposes them.

Junior developer asks: “How can I tell whether I have handled handling optimistic database concurrency correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for handling optimistic database concurrency containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

27. Using row versions

Using row versions is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Build a thin end-to-end example and measure it. Small experiments reveal scheduling, configuration and integration behaviour that cannot be settled by intuition alone.

Junior developer asks: “How can I tell whether I have handled using row versions correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for using row versions containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

28. Designing idempotent operations

Designing idempotent operations is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Junior developer asks: “How can I tell whether I have handled designing idempotent operations correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for designing idempotent operations containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

29. Preventing duplicate background jobs

Preventing duplicate background jobs is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Start by writing down the observable behaviour before selecting an API or framework feature. A concrete scenario gives the team a shared basis for discussing correctness.

Junior developer asks: “How can I tell whether I have handled preventing duplicate background jobs correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for preventing duplicate background jobs containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

30. Testing race conditions repeatedly

Testing race conditions repeatedly is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Treat this as a system boundary with explicit inputs, outputs, ownership and failure semantics. Hidden assumptions become production incidents when load or partial failure exposes them.

Junior developer asks: “How can I tell whether I have handled testing race conditions repeatedly correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for testing race conditions repeatedly containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

31. Using deterministic coordination in tests

Using deterministic coordination in tests is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Build a thin end-to-end example and measure it. Small experiments reveal scheduling, configuration and integration behaviour that cannot be settled by intuition alone.

Junior developer asks: “How can I tell whether I have handled using deterministic coordination in tests correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for using deterministic coordination in tests containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

32. Reading thread dumps and traces

Reading thread dumps and traces is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Junior developer asks: “How can I tell whether I have handled reading thread dumps and traces correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for reading thread dumps and traces containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

33. Measuring contention

Measuring contention is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Start by writing down the observable behaviour before selecting an API or framework feature. A concrete scenario gives the team a shared basis for discussing correctness.

Junior developer asks: “How can I tell whether I have handled measuring contention correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for measuring contention containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

34. Avoiding premature parallelism

Avoiding premature parallelism is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Treat this as a system boundary with explicit inputs, outputs, ownership and failure semantics. Hidden assumptions become production incidents when load or partial failure exposes them.

Junior developer asks: “How can I tell whether I have handled avoiding premature parallelism correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for avoiding premature parallelism containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

35. Reviewing concurrent code

Reviewing concurrent code is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Build a thin end-to-end example and measure it. Small experiments reveal scheduling, configuration and integration behaviour that cannot be settled by intuition alone.

Junior developer asks: “How can I tell whether I have handled reviewing concurrent code correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for reviewing concurrent code containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

36. Building a concurrency safety case

Building a concurrency safety case is important in a concurrent .NET application in which multiple requests, tasks or workers can touch shared state. The goal is correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Junior developer asks: “How can I tell whether I have handled building a concurrency safety case correctly?”

Exercise

Choose a feature from an application you know. Produce a one-page design note for building a concurrency safety case containing the scenario, contract, assumptions, failure matrix, test cases, telemetry and rollback procedure. Review it with a teammate who did not write the code. Questions they ask are evidence about missing context and should improve both the design and its documentation.

Final Perspective

These practices form one engineering system: model the behaviour, make boundaries honest, keep ownership clear, verify failure as deliberately as success, and operate the result with evidence. Use the chapters as prompts for design and review rather than as isolated rules. The objective remains correct behaviour under real scheduling, cancellation and failure rather than correctness that exists only in a single-threaded test.

Use this journal entry for recall practice

Compare your explanation with the questions and working answers in my Practice Room.

Practise async and concurrency interview questions →
Afzal Ahmed

Faz Ahmed

Senior Full Stack Engineer & Technical Lead

A hands-on engineer with 15+ years in commercial software. I publish what I am studying, revising and testing so visitors can see both established experience and learning still in progress.

How would you approach this problem? I'd love to hear your thoughts or continue the discussion.

Connect on LinkedIn →