C# & .NET

C# Async/Await and Task in ASP.NET Core: A Practical Guide

Afzal AhmedFaz Ahmed
·23 July 2026·15 min read
C#.NETASP.NET CoreAsync/AwaitTaskThread PoolEntity Framework CoreWeb API
C# async await and Task practical guide for ASP.NET Core developers
C# async await and Task practical guide for ASP.NET Core developers

Why This Matters

C# async and await can make ASP.NET Core applications more scalable, but only when developers understand what Task, threads, the thread pool and asynchronous I/O actually do. Drawing on my experience building and supporting enterprise .NET systems, this guide explains the mental model, common misconceptions, production mistakes and practical patterns every .NET developer should know.

C# Async/Await and Task in ASP.NET Core: A Practical Guide

Why I Wrote This Guide

During code reviews, mentoring sessions and production investigations, I regularly encounter the same misunderstandings around C# async and await.

Developers know the syntax. They know asynchronous method names normally end in Async, and they know they should place await before a call. But many are less certain about what actually happens underneath:

  • Does every Task create a new thread?
  • Does await block the current thread?
  • Does async code always run in parallel?
  • Should database calls be wrapped in Task.Run?
  • Why are .Result and .Wait() dangerous?
  • When should Task.WhenAll be used?
  • Does an ASP.NET Core continuation return to the original thread?
These are not academic questions. Getting the answers wrong can lead to blocked request threads, thread-pool starvation, Entity Framework Core errors, unnecessary concurrency and applications that perform well in development but struggle under production load.

I have worked with .NET applications across financial services, legal platforms, enterprise systems and cloud-hosted APIs. My practical view is that async and await become much easier once we stop thinking of them as special syntax and start understanding the resources involved.

The Distinction I Teach First

The most important distinction is this:

A Task represents an operation. A thread is a resource that can execute instructions.

A Task is not a thread.

A task may require a thread while application code is executing, but it does not necessarily occupy one throughout its lifetime. When genuinely asynchronous I/O is waiting for SQL Server, an external API or Azure Storage, no application thread needs to sit there doing nothing.

That is the reason async and await can improve the scalability of ASP.NET Core applications.

CPU, Cores and Threads in Plain English

The CPU executes instructions. Modern CPUs contain multiple cores, allowing several streams of instructions to execute at approximately the same time.

A thread is an execution path inside a process. When our C# code is actively running, it runs on a thread, and the operating system schedules that thread onto an available CPU core.

An application can contain far more threads than the machine has cores. That does not mean every thread runs simultaneously. The operating system continually switches the available cores between runnable threads.

Misconception: More Threads Always Mean Better Performance

They do not.

Threads have a cost. Each thread consumes memory, requires scheduling and creates context-switching overhead. Once the processor is busy, creating more threads does not create more CPU capacity. In server applications, uncontrolled thread creation can reduce throughput rather than improve it.

What the .NET Thread Pool Does

.NET maintains a collection of reusable worker threads called the thread pool. Instead of creating and destroying an operating-system thread for every small piece of work, the runtime schedules suitable work onto reusable threads.

ASP.NET Core uses thread-pool threads to process requests. If many request threads become blocked, new work may have to wait for a thread to become available.

This is why code such as the following is dangerous:

var customer = customerService.GetByIdAsync(id).Result;

The thread remains occupied while it waits for the task to finish. Under light local testing, this may appear harmless. Under real traffic, hundreds of blocked requests can exhaust the available worker threads.

The result can be thread-pool starvation:

  • Requests become increasingly slow.
  • Timeouts begin appearing.
  • CPU usage may remain surprisingly low.
  • Task continuations are delayed.
  • Application throughput suddenly collapses.
I have learned not to judge asynchronous code only by whether it works. The real question is whether it continues to behave correctly when the system is under load.

What Is a Task in C#?

A Task represents the eventual completion of an operation. It might represent an operation that is waiting to begin, currently executing, waiting for external I/O, completed successfully, failed or cancelled.

Use Task when an asynchronous operation does not return a business value:

public Task SendNotificationAsync(
    Notification notification,
    CancellationToken cancellationToken);

Use Task when the operation produces a result:

public Task<CustomerDto?> GetCustomerAsync(
    int customerId,
    CancellationToken cancellationToken);

Another misconception I see is the assumption that calling a task-returning method automatically places work on the thread pool. That is not true. A task may represent thread-pool work, but it can also represent database or network I/O being monitored by the operating system. It may even already be complete.

What async and await Actually Do

The async keyword enables a method to use await. The compiler transforms that method into a state machine so it can pause and resume without blocking the calling thread.

When execution reaches an await, one of two things happens. If the awaited task has already completed, the method can continue immediately. If it has not completed, the method records where it should resume, returns control to its caller and allows the current thread to perform other work. When the operation completes, the continuation is scheduled to resume.

public async Task<CustomerDto?> GetCustomerAsync(
    int customerId,
    CancellationToken cancellationToken)
{
    var customer = await customerRepository.GetByIdAsync(
        customerId,
        cancellationToken);

    return customer is null
        ? null
        : new CustomerDto(customer.Id, customer.Name);
}

The thread is not sleeping inside await. It is released to do useful work elsewhere.

Misconception: async Creates a New Thread

The async keyword does not create a thread. It enables asynchronous control flow. Whether threads are involved depends on the operation being awaited.

An asynchronous SQL query does not require a dedicated thread to sit and wait for SQL Server. A CPU-intensive calculation, however, must use a thread while executing instructions.

Misconception: await Blocks Until the Operation Finishes

await pauses the logical method, not necessarily the physical thread. This is fundamentally different from .Result, .Wait() or Thread.Sleep, which block the current thread.

How an ASP.NET Core Async Request Flows

A simplified ASP.NET Core request works like this:

  1. A web request arrives.
  2. A thread-pool thread begins executing the request pipeline.
  3. The controller calls an asynchronous service.
  4. The service calls an asynchronous repository.
  5. The repository starts a database operation.
  6. Execution reaches an incomplete await.
  7. The request thread returns to the thread pool.
  8. SQL Server performs the database work.
  9. Completion is reported back to the application.
  10. The continuation is scheduled on an available thread.
  11. ASP.NET Core completes the response.
The thread that resumes the method may be different from the thread that started it. Developers sometimes assume that an ASP.NET Core request owns one thread from beginning to end. It does not.

Async Is Not the Same as Parallel

This is one of the most persistent C# async misconceptions.

Asynchronous programming is mainly about not blocking while waiting. Parallel programming is about performing multiple pieces of CPU work at the same time.

An asynchronous database call may spend most of its lifetime using no application thread. A parallel calculation may use several threads and CPU cores simultaneously.

Async does not automatically make an operation faster. If SQL Server takes 500 milliseconds to answer, await will not make SQL Server answer in 100 milliseconds. The benefit is that your ASP.NET Core application does not waste a thread during those 500 milliseconds.

I/O-Bound Work Versus CPU-Bound Work

Before choosing an async pattern, I ask whether the work is I/O-bound or CPU-bound.

I/O-Bound Operations

These spend most of their time waiting for an external resource: SQL Server queries, HTTP API calls, Azure Blob Storage, file I/O, message brokers and network operations. Use the genuine asynchronous API:

var customer = await dbContext.Customers
    .AsNoTracking()
    .SingleOrDefaultAsync(
        customer => customer.Id == customerId,
        cancellationToken);

CPU-Bound Operations

These spend most of their time executing calculations, such as image processing, encryption, compression and large in-memory transformations.

Task.Run can move CPU-bound work onto a thread-pool thread, but that does not eliminate the CPU cost. In ASP.NET Core, I avoid reaching for Task.Run as a reflex. Heavy processing is often better placed in a worker service, durable job queue or separate processing component.

Mistake: Wrapping Asynchronous I/O in Task.Run

I frequently see code resembling this:

var customer = await Task.Run(
    () => customerRepository.GetByIdAsync(id, cancellationToken));

This adds thread-pool scheduling without making the database operation more asynchronous. Call the asynchronous method directly:

var customer = await customerRepository.GetByIdAsync(
    id,
    cancellationToken);

Async All the Way

Once an operation is asynchronous, I normally keep the complete call chain asynchronous: Controller to Service to Repository to Database.

Avoid blocking partway through that chain. The async-all-the-way principle keeps the execution model consistent and avoids wasting request threads.

Using Task.WhenAll Correctly

Task.WhenAll is valuable when independent asynchronous operations can run concurrently:

var customerTask = customerRepository.GetByIdAsync(customerId, cancellationToken);
var ordersTask = orderRepository.GetForCustomerAsync(customerId, cancellationToken);
var balanceTask = balanceService.GetBalanceAsync(customerId, cancellationToken);

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

If three independent calls each take roughly 300 milliseconds, sequential execution might take around 900 milliseconds. Concurrent waiting could reduce the elapsed time to approximately the duration of the slowest call.

But Task.WhenAll is not a performance decoration to place around every group of calls. Before using it, ask whether the operations are genuinely independent, whether they share non-thread-safe state and whether the downstream service can handle the additional concurrency.

Mistake: Running Dependent Operations Concurrently

An update followed by an email is a simple example. If the email must only be sent after a successful update, the operations belong in sequence:

await customerRepository.UpdateAsync(customer, cancellationToken);
await emailService.SendUpdatedEmailAsync(customer.Email, cancellationToken);

Clarity and correctness matter more than forcing concurrency.

The Entity Framework Core DbContext Trap

A DbContext is not thread-safe. Developers sometimes start multiple EF Core operations on the same scoped context and await them using Task.WhenAll. This can produce the familiar second-operation-started exception.

When queries share the same DbContext, execute them sequentially, redesign the query, or use carefully managed separate contexts only when genuine concurrent database access is justified. Often, one well-designed SQL query is better than several concurrent queries.

Always Propagate CancellationToken

In ASP.NET Core, cancellation should flow through every relevant application layer:

[HttpGet("{id:int}")]
public async Task<ActionResult<CustomerDto>> GetCustomer(
    int id,
    CancellationToken cancellationToken)
{
    var customer = await customerService.GetByIdAsync(
        id,
        cancellationToken);

    return customer is null ? NotFound() : Ok(customer);
}

The token should continue through the service, repository and underlying database or HTTP call. If the client disconnects, the server can stop work that is no longer required.

A common mistake is accepting a token but failing to pass it to the terminal I/O operation. That gives the appearance of cancellation support without actually providing it.

Avoid Fire-and-Forget Work Inside Requests

Another production mistake is starting an asynchronous operation without awaiting it:

emailService.SendEmailAsync(message, cancellationToken);
return Ok();

The request may end before the email operation completes. Scoped services can be disposed, exceptions can be lost, and application shutdown can interrupt the operation.

For important background processing, I use an explicit mechanism such as a hosted background service, bounded channel, Azure Service Bus, RabbitMQ, Hangfire or a durable cloud queue. If the work matters to the business, it should not depend on an HTTP request remaining alive.

Useful Task Helpers

Task.CompletedTask represents a successfully completed task with no result. Task.FromResult wraps an already available value. Task.Delay creates a non-blocking asynchronous delay. Avoid Thread.Sleep in ASP.NET Core request handling because it blocks the current thread.

Task.WhenAny lets code continue when the first task completes. Modern .NET also provides WaitAsync for clear timeout handling:

var result = await externalClient
    .GetResultAsync(cancellationToken)
    .WaitAsync(TimeSpan.FromSeconds(10), cancellationToken);

Exceptions in Asynchronous Code

When an awaited task fails, its exception is rethrown at the await point. I avoid catching exceptions simply to hide them or return an apparently successful response. ASP.NET Core applications benefit from centralised exception handling, consistent problem responses and structured logging.

Expected cancellation is different from a system failure. OperationCanceledException should not automatically be logged as a critical production error when the client intentionally cancelled the request.

My Async Code Review Checklist

When I review C# or ASP.NET Core asynchronous code, I ask:

  • Is the underlying operation genuinely asynchronous?
  • Does async flow through the complete call chain?
  • Are .Result, .Wait() and Thread.Sleep avoided?
  • Is Task.Run genuinely justified?
  • Is CancellationToken passed to the final I/O operation?
  • Are operations inside Task.WhenAll truly independent?
  • Is one DbContext being used concurrently?
  • Could concurrency overload SQL Server or an external API?
  • Is fire-and-forget work handled by a reliable background process?
  • Are exceptions logged and translated consistently?
  • Is cancellation being treated differently from failure?
  • Is the resulting code easier to maintain and support?

Final Thoughts From Experience

The syntax of C# async and await is not the difficult part. The challenge is developing the correct mental model.

A Task is not a thread. Async does not automatically mean parallel. Task.Run does not fix synchronous I/O. Task.WhenAll is not appropriate for dependent operations. An awaited ASP.NET Core method does not need to resume on its original thread.

The practical objective is simple: do not occupy valuable threads while the application is merely waiting for an external system.

When async and await are applied correctly, ASP.NET Core applications can handle more concurrent requests, remain responsive under load and use server resources more efficiently. When they are applied mechanically, the same keywords can hide blocked threads, unnecessary scheduling, uncontrolled concurrency and subtle production failures.

My advice to developers is to pause before adding async and ask three questions:

  • What operation am I waiting for?
  • Does it provide genuine asynchronous support?
  • What resource is occupied while I wait?
If you can answer those clearly, the correct implementation normally becomes much easier. That is the difference between code that merely uses async and a system that is genuinely designed for asynchronous, scalable and production-ready .NET execution.

Async, Await and Task in ASP.NET Core Production Systems

1. Understanding what async changes

Understanding what async changes is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

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

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for understanding what async changes 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 Task as a promise of work

Understanding Task as a promise of work is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled understanding task as a promise of work correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for understanding task as a promise of 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.

3. Following await control flow

Following await control flow is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled following await control flow correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for following await control flow 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. Distinguishing I/O-bound and CPU-bound work

Distinguishing I/O-bound and CPU-bound work is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled distinguishing i/o-bound and cpu-bound work correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for distinguishing i/o-bound and cpu-bound 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.

5. Avoiding fake asynchrony

Avoiding fake asynchrony is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

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

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for avoiding fake asynchrony 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. Avoiding sync-over-async

Avoiding sync-over-async is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled avoiding sync-over-async correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for avoiding sync-over-async 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. Understanding thread-pool starvation

Understanding thread-pool starvation is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled understanding thread-pool starvation correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for understanding thread-pool starvation 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. Writing async all the way

Writing async all the way is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled writing async all the way correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for writing async all the way 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. Naming asynchronous APIs

Naming asynchronous APIs is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled naming asynchronous apis correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for naming asynchronous apis 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. Returning Task and ValueTask

Returning Task and ValueTask is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled returning task and valuetask correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for returning task and valuetask 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 ValueTask selectively

Using ValueTask selectively is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

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

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for using valuetask selectively 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. Propagating cancellation tokens

Propagating cancellation tokens is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled propagating cancellation tokens correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for propagating cancellation tokens 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. Creating cancellation boundaries

Creating cancellation boundaries is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled creating cancellation boundaries correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for creating cancellation boundaries 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. Applying timeouts

Applying timeouts is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled applying timeouts correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for applying timeouts 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. Handling exceptions from awaited tasks

Handling exceptions from awaited tasks is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled handling exceptions from awaited tasks correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for handling exceptions from awaited tasks 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. Observing fire-and-forget failures

Observing fire-and-forget failures is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled observing fire-and-forget failures correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for observing fire-and-forget failures 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 background services

Using background services is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

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

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for using background services 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. Avoiding Task.Run in request handlers

Avoiding Task.Run in request handlers is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled avoiding task.run in request handlers correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for avoiding task.run in request handlers 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. Running independent work concurrently

Running independent work concurrently is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled running independent work concurrently correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for running independent work concurrently 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. Using Task.WhenAll safely

Using Task.WhenAll safely is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled using task.whenall safely correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for using task.whenall safely 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. Limiting concurrency

Limiting concurrency is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

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

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for limiting 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.

22. Streaming with IAsyncEnumerable

Streaming with IAsyncEnumerable is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled streaming with iasyncenumerable correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for streaming with iasyncenumerable 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. Disposing asynchronous resources

Disposing asynchronous resources is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled disposing asynchronous resources correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for disposing asynchronous resources 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. Using await using

Using await using is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

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

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for using await using 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. Calling EF Core asynchronously

Calling EF Core asynchronously is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled calling ef core asynchronously correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for calling ef core asynchronously 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. Avoiding parallel DbContext use

Avoiding parallel DbContext use is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

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

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for avoiding parallel dbcontext use 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. Calling HttpClient asynchronously

Calling HttpClient asynchronously is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled calling httpclient asynchronously correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for calling httpclient asynchronously 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. Applying resilience and cancellation

Applying resilience and cancellation is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled applying resilience and cancellation correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for applying resilience and cancellation 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. Handling client disconnects

Handling client disconnects is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled handling client disconnects correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for handling client disconnects 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 asynchronous code

Testing asynchronous code is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

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

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for testing asynchronous 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.

31. Testing cancellation

Testing cancellation is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

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

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for testing cancellation 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. Diagnosing slow async requests

Diagnosing slow async requests is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled diagnosing slow async requests correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for diagnosing slow async requests 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 thread-pool behaviour

Measuring thread-pool behaviour is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled measuring thread-pool behaviour correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for measuring thread-pool behaviour 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. Protecting downstream dependencies

Protecting downstream dependencies is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled protecting downstream dependencies correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for protecting downstream dependencies 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 async code

Reviewing async code is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. 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.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

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

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for reviewing async 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 an end-to-end async policy

Building an end-to-end async policy is important in an ASP.NET Core application serving many overlapping requests while calling databases and remote services. The goal is responsive, scalable request handling with correct cancellation, error propagation and resource ownership. Design for the engineer diagnosing the system at 02:00. Clear state, structured telemetry and an intentional recovery path are part of the feature.

Mental model

Describe the operation as a timeline. Identify what the caller knows at the beginning, which boundary is crossed, what work may overlap, and which component owns the final decision. Then mark every place where data can be absent, stale, malformed or unauthorised. This turns a vague technical topic into a model that can be reviewed and tested.

A useful model separates intent from mechanism. Intent explains the business or operational outcome. Mechanism explains the library, protocol or runtime behaviour used today. Keeping those separate allows implementation details to evolve without silently changing the contract. It also prevents a familiar API from being mistaken for a complete design.

Implementation approach

Implement the smallest contract that can express success and meaningful failure. Validate untrusted input at the boundary, keep orchestration visible, and move reusable policy close to the knowledge required to enforce it. Prefer explicit dependencies over ambient state. Carry cancellation, identity and correlation information through the call chain where they affect behaviour.

Add safeguards in layers. Compile-time checks catch incorrect composition, runtime validation catches real external values, automated tests prove stable rules, and telemetry explains behaviour that emerges only under production timing or data. No single layer replaces the others. Comments should capture why a surprising constraint exists; the code should make the normal path easy to follow.

Failure modes and trade-offs

Challenge happy-path assumptions. Consider malformed input, empty results, duplicate requests, partial dependency failure, retries, cancellation, concurrent updates, slow responses and process restarts. Decide whether each condition should fail, degrade, retry, compensate or ask a human to intervene. An unspecified failure policy is still a policy—it is simply one chosen accidentally by libraries and timing.

Avoid adding abstraction merely to hide a difficult decision. A wrapper is useful when it owns stable policy, supplies consistent observability or protects callers from a volatile dependency. It is harmful when it renames an API without reducing risk. Measure complexity in the number of concepts a maintainer must hold, not the number of files in the solution.

Verification and operations

Create tests at the cheapest level capable of proving the rule. Include a representative success, a boundary case, a deliberate dependency failure and a cancellation or concurrency scenario when relevant. Integration tests should verify real serialization, configuration and infrastructure behaviour instead of mocking away the reason the test exists.

Expose structured events with identifiers that let an operator follow one operation across boundaries. Choose metrics that reveal demand, latency, failures, saturation and quality rather than reporting activity alone. Define an alert only when someone can take a meaningful action. Record how to disable or reverse the change before deployment.

Review checklist

  • Is the intended outcome stated in language a user or operator can verify?
  • Which input and runtime assumptions are validated rather than asserted?
  • What happens during timeout, cancellation, retry and partial failure?
  • Is ownership of mutable state or external resources unambiguous?
  • Do tests prove behaviour instead of mirroring private implementation?
  • Can logs, metrics and traces distinguish the main failure classes?
  • Is there a safe deployment, compatibility and rollback story?

Mentoring discussion

Junior developer asks: “How can I tell whether I have handled building an end-to-end async policy correctly?”

Explain the design using one realistic example and one hostile example. Trace both from the public contract to the final observable result. If you cannot identify the invariant, the failure representation and the operational evidence, the design needs another pass. Good engineering is not the absence of all risk; it is making important risks explicit, bounded and testable.

Exercise

Choose a feature from an application you know. Produce a one-page design note for building an end-to-end async policy 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 responsive, scalable request handling with correct cancellation, error propagation and resource ownership.

.NET CPU, core, thread, thread pool, Task, async and await explained using a café analogy
.NET CPU, core, thread, thread pool, Task, async and await explained using a café analogy

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 →