Dinesh/ Blog
application engineering

MS Stack Ch 22 — Pro skills (the bits no one writes down)

Code review craft, on-call composure, incident writing, technical proposals, mentoring, learning velocity, knowing when to rewrite. The chapter that turns a strong engineer into a senior one.

31 min readChecking device speech…
Lesson preparation & details

Level: advanced

By the end, you should be able to

  • Debug by discriminating hypotheses and interpreting traces without losing evidence
  • Measure allocation and performance before selecting an optimization
  • Write reviewable PRs, ADRs, incident notes and incremental rewrite decisions

Bring with you

Sources checked: . A source check does not change the publication or update date.

Editorial review: · What review means

In this article · 31 sections

Review and execution boundary

Reference debugging/diagnostics practices for .NET and browser applications; synthetic scenarios below are not incidents observed in a deployed service, company promotion rules or measured performance results.

The 9 October 2026 body review freshly read the complete current chapter and reused the hash-verified a72 original audit/recovery, explicitly not a fresh whole-original read. Saved official/maintainer quotations support the contracts below. Local bounded fixtures model hypothesis discrimination, reachability and operation counts; they are not .NET GC execution, a profiler run, a real incident or a cloud database evaluation. No build, deployment, credentials, cloud writes or site approval occurred.

Chapter 22 of From Novice to Fluent on the Modern Microsoft Web Stack — a 22-chapter self-study plan.

Why this chapter

The previous twenty-one chapters were mechanics — the runtime, the framework, the pipeline, the cloud. This chapter develops practices relevant to greater engineering responsibility. Promotion and certification criteria differ by organization; these habits are neither a universal rubric nor a guarantee of becoming senior.

The skill set is portable across stacks and companies. Reading a large codebase you did not write. Debugging with intent rather than print statements. Writing a pull request that someone enjoys reviewing. Documenting a decision so the team in 2028 understands why a 2026 you chose what you chose. Each is a craft; each takes deliberate practice; none has a tutorial that finishes the job for you.

The shipping-grade version is "I get my code in, I respond to reviews, I unblock myself". The senior version is "I land changes that compose with what's there, I shape the team's reviews to be productive, I leave the codebase better-understood for the next person". The gap is taste, calibration, and a small set of habits.

You finish this chapter when these habits are on autopilot: opening a new repo and finding the composition root first, framing a bug as a hypothesis before writing the fix, breaking a 600-line change into PRs reviewers can hold in their heads, writing an ADR before merging the choice it captures.

Reading code — Composition roots, follow the wiring outward, build the map fast.

Debugging — Hypothesis-driven, narrow the suspect set, the failure mode tells you where to look.

Stack traces — Async state machines, missing frames, what to keep and what to skip.

Performance — Measure first, big-O on hot paths, allocation awareness, the cost of premature opt.

PR craft — Small changes, clear intent, the review you would want to receive.

Decisions on paper — ADRs, design docs, the conversation the doc has with future-you.

Concepts and depth

Reading large codebases

You will spend more of your career reading code than writing it. The skill is not "read every file"; it is "find the right file fast and build a mental map from it outward".

Look for the composition roots — places where hosts, dependencies and entry points are wired together. A multi-host repo can have several; a library may have no application entry point. In a typical ASP.NET Core minimal-host app, start at Program.cs: services are registered, middleware is added, endpoints are mapped, the host runs. In a client-rendered React app, main.tsx or App.tsx may wire providers/router; framework-driven apps can use route/layout entry points instead. In a CLI, find the Main method or actual argument-parser dispatch (Node exposes arguments as process.argv, not a universal program.argv). Trace outward and draft a map of modules, data flow and external dependencies; thirty minutes is a planning timebox, not a completeness guarantee.

The next move is read a single endpoint or screen end to end. Pick the simplest one. Follow the request from controller to service to repository, or from component to hook to API call. You learn the codebase's conventions — naming, layering, error handling, logging — by repetition; one end-to-end trace teaches more than browsing twenty files.

Read the tests. Tests are executable examples of exercised expectations, not automatically the complete or correct contract. The integration test for an endpoint shows you the expected request and response shapes, the auth requirements, the database state. The unit test for a service shows you the contracts of its collaborators. When a part of the codebase confuses you, find its test and read it; the test is the simplest form of the code's intent.

Ignore the noise. A codebase has accumulated cruft — abandoned experiments, dead branches, the old config layer that was supposed to be deleted three years ago. New readers waste time treating cruft as if it were load-bearing. Ask the team or check git log -- file.cs to find out whether a file has been touched in the last year. Old untouched code can be the most stable and critical part of the system. Use references, runtime traces, ownership and tests alongside history before classifying anything as dead.

Good enough to ship

• Locate likely entry/composition roots early and record unresolved wiring rather than promise a discovery time • Trace one endpoint or screen end to end • Read tests as executable evidence of intended contracts; compare with requirements and actual behavior

Expert tier

• Write the team's "first day" onboarding map for the repo • Combine references, runtime evidence, ownership and tests with git history before classifying code as dead • Internalise the conventions enough to write code that looks native

Debugging with intent

The default debugging mode is "add print statements until the bug reveals itself". This works for small bugs in code you wrote yesterday. It does not work for production incidents, race conditions, or any bug whose root cause is two layers below the symptom.

The intentional mode is hypothesis-driven. State, in one sentence, what you think is happening: "I believe the cache is returning a stale value because the invalidation event arrives after the next read". Now design an experiment that distinguishes its predictions from alternatives: add a log at the invalidation point, replay the failing scenario, observe whether the invalidation precedes the read. The result may support, refute or leave the hypothesis unresolved. A fitting observation is not unique proof of a cause; test an alternative explanation and preserve timing/telemetry assumptions.

The technique that scales is narrow the suspect set. You have a bug; the change that caused it must live somewhere in the codebase or its inputs. Eliminate large regions: does it reproduce with stub inputs? (No → investigate what changed, including timing and data shape.) Does it reproduce with the DB stubbed? (No → database behavior or the removed latency/concurrency may matter.) Does it reproduce on the previous build with matched inputs/configuration? (No → a changed build is a candidate, but intermittent behavior and environmental differences still need controls.) Each controlled comparison narrows or revises the hypothesis set; neither a halving nor a one-hour diagnosis is guaranteed.

Read the actual error. Stack traces, exception messages, log lines — they say more than developers usually read. A NullReferenceException on line 47 of OrderService.cs is not "something is null"; it is "the specific reference at line 47 was null when this method ran with these inputs". Trace back: which variable, where was it assigned, what could make it null at that point.

Reproduce reliably before fixing. A reproducer gives strong regression evidence. During an incident, mitigate safely even if reproduction is incomplete, retain observations and distinguish mitigation from a confirmed root-cause fix. Spend the time on a reliable reproducer — a unit test, a script, a saved request — before changing code. Rerun the reproducer plus adjacent valid and invalid cases. One passing input verifies that regression, not all causal explanations or system behavior.

The hardest debugging muscle is stopping when you have an answer that fits. Confirmation bias makes you stop too early. The discipline is to ask "what would I observe if my hypothesis were wrong?" and look for that contrary evidence before declaring the bug solved.

Good enough to ship

• State the hypothesis before changing code • Seek a reproducer for the fix; during an incident mitigate safely without waiting for perfect reproduction • Read the full stack trace, not just the top line

Expert tier

• Binary-search the suspect set systematically • Use git bisect when the bug is "started failing at some commit" • Routinely seek contrary evidence before declaring the bug solved

Reading stack traces (and async state machines)

In an ordinary unmodified synchronous exception trace, the most recent frame is shown first and callers follow below it. Inlining, truncation and rethrowing can omit or reshape frames. Start with application frames, but retain framework frames that explain serialization, cancellation, DI or boundary behavior; do not filter them out before understanding the path.

Async stack traces are harder because the compiler lowers an async method into generated state-machine machinery; its representation can vary by build/compiler. A frame may read MoveNext() rather than the method you wrote; the calling frame may be the AwaitTaskContinuation rather than the awaiter. Modern runtimes/debuggers may display logical async method names; exact formatting depends on runtime, compiler, optimization and available symbols, not a universal .NET 6 cutoff.

The pattern to recognise: a method Foo containing an await produces a compiler-generated state machine such as <Foo>d__3 (<>c__DisplayClass... instead denotes a closure/display class) whose MoveNext runs the method's continuations. When you see MoveNext in a trace, look at the enclosing class name to find the actual method.

System.NullReferenceException: Object reference not set to an instance of an object.
   at MyApp.Services.OrderService.<GetAsync>d__7.MoveNext() in OrderService.cs:line 42
   at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AwaitUnsafeOnCompleted...
   at MyApp.Controllers.OrdersController.<GetById>d__5.MoveNext() in OrdersController.cs:line 23

This schematic trace points to OrderService.GetAsync at line 42 and OrdersController.GetById at line 23 as investigation locations. Confirm the await relationship in matching source/symbols; generated names are implementation details, not a stable parsing API. Keep the intermediate frame with the evidence even if it is not the immediate suspect.

Missing frames happen when the runtime optimises away tail calls or when exceptions cross task boundaries. Look for inner exceptions (ex.InnerException) — AggregateException can contain multiple failures. TaskCanceledException indicates cancellation and does not necessarily hide another “real” error; inspect token/deadline state as well as any actual inner exception. ex.StackTrace may be one stack; ex.InnerException.StackTrace the other.

When useful, search the stable error text or code after removing tokens, customer data, paths and identifiers that must not leave the environment. Check the matching library/runtime version in primary docs or a reproducible issue. Search hits generate hypotheses; neither a unique message nor three matching results establishes the cause.

Good enough to ship

• Read the full trace; prioritize application frames without discarding relevant framework evidence • Find the inner exception when there is one • Translate MoveNext back to the async method it belongs to

Expert tier

• Decode AggregateException trees from concurrent code • Use dotnet-dump + dotnet-stack for hung processes • Use approved user-mode dumps with matching symbols in WinDbg/LLDB for native/managed diagnosis; kernel debugging is a different capture and privilege boundary

Performance basics: measure first, big-O on hot paths

Two avoidable performance mistakes are optimizing before measuring and treating every path as hot.

Measure first. Use traces/profiles (dotnet-trace or browser DevTools) to identify candidate CPU, waiting or allocation costs under a representative workload. BenchmarkDotNet is primarily an isolated benchmarking harness, not automatically a whole-application profiler. A first capture guides investigation; sampling, workload and missing signals limit what it proves.

Once you know the hot paths, big-O matters. An O(n²) algorithm on a 10-item list is fine; on a 100,000-item list it is a tarpit. Hash-based lookups (HashSet, Dictionary) replace nested loops; ordered iteration with merge-style algorithms replaces repeated containment checks. The Computer Science you learned in 2014 is the same Computer Science that runs production traffic in 2026.

Allocation awareness matters in hot paths in .NET because garbage collection is not free. Allocation does not make an object a GC root. Roots include live stack references, static fields and GC handles; reachability from them determines retention; large object heap allocations are expensive; LINQ chains in tight loops can allocate per iteration. The fix is not "never allocate"; it is "know where your hot path allocates and reduce it where it matters". A span/memory slice is a view over backing storage, not a copy of the elements; obtaining backing storage or calling ToArray can still allocate. With ArrayPool, track ownership, return each rented buffer correctly, avoid use-after-return and clear sensitive contents when needed. These techniques trade simpler ownership for potential allocation savings; measure before adopting them.

The cost of premature optimisation is paid in readability. Code optimised for a path that is not hot is code the next reader has to decode for no gain. Default to clarity; optimise the proven hot paths only.

The frontend has its own version: measure with Lighthouse and the browser's Performance tab. Inspect bundle/network cost and long tasks; distinguish lab diagnostics from field Core Web Vitals: LCP (loading), INP (interactivity), and CLS (visual stability), evaluated over the field distribution. TTI is not a current Core Web Vital. The intuitions (useMemo everywhere) are usually wrong; the measurement (one component re-renders 200 times per second) is the signal.

Good enough to ship

• Profile before optimising • Replace nested loops with hash-based lookups when n grows • Measure actual enumeration/allocation costs before replacing LINQ in a hot path

Expert tier

• Use BenchmarkDotNet for micro-benchmarks; results inform decisions • Span<T> / pooling on the hottest paths • Frontend: monitor Core Web Vitals in production

Writing PR-sized changes

A large mixed-purpose PR is harder to review thoroughly; line count is a warning signal, not a probability estimate. The reviewer skims, hopes for the best, and approves; bugs that a smaller diff would have caught ship. The discipline is to break work into PRs the reviewer can hold in their head.

A useful local planning heuristic is around 300 lines of meaningful diff, not a universal ceiling. Split by coherent, independently testable intent; a larger mechanical change can be simpler than a small subtle authorization change. The splits are usually structural — refactor first, then add the feature; introduce the new interface first, then the implementations; move first, then modify. Each split is a separate, mergeable PR with its own intent.

The PR description is the second discipline. Write why before what. The diff shows what changed; the description must explain why this change, why now, what the tradeoffs were, and how the reviewer should approach the review. A description that says "Adds support for X" is wasted bytes. One that says "Customers in EU need feature X by date Y; we considered approaches A, B, C; chose B because of latency; this PR implements B, follow-up PRs add monitoring and migrate existing data" gives the reviewer the context to evaluate the design.

Self-review before requesting. Open the PR, read your own diff, leave inline comments on the parts you anticipate questions about. Reviewers value this enormously — it signals you have thought about your work and that their time will be used efficiently.

Reviewing others. The reciprocal skill. Read the PR description first; understand the why. Run the code mentally; identify the parts that interact with the rest of the system. Comment on what matters (correctness, maintainability, design) and not on what does not (formatting handled by linter, personal style). Approve when you would be comfortable having the code in production; do not rubber-stamp.

Good enough to ship

• Coherent, independently testable PRs; use size as a review-cost signal, not a hard correctness rule • Description explains why, not just what • Self-review before requesting

Expert tier

• Coach the team toward PR-sized changes • Reviews catch design issues, not just bugs • Approve with substantive feedback, not rubber-stamping

Documenting decisions: ADRs and design docs

Code shows what; comments show why this line; decision documents show why this approach. One useful form is the Architecture Decision Record (ADR) — a short markdown file capturing one decision, its context, the alternatives considered, and the consequences.

# ADR-007: Evaluate Cosmos DB for the analytics aggregates store

## Status
Proposed — synthetic example, not an approved architecture

## Context
The analytics service must store rollup aggregates queried by dashboard pages
at p95 latency under 100ms across three regions. Volume estimate is 10M items,
growing to 100M.

## Decision
Propose Cosmos DB for NoSQL, partitioned by tenant id, with multi-region
writes only if conflict policy and supported consistency meet the workload.
Validate hot-tenant skew and regional latency with measured data before acceptance.

## Alternatives considered
- PostgreSQL with logical replication: compare conflict handling, write routing
  and operations for the intended topology; do not infer a multi-writer
  application contract merely from the presence of replication.
- A Mongo-compatible service: evaluate query/driver compatibility separately;
  no unsupported product-maturity claim substitutes for a benchmark.
- Redis as primary store: not selected until persistence, recovery and retention
  requirements are demonstrated for the chosen product/configuration.

## Consequences
- Pricing and global-write benefits remain hypotheses pending workload, region,
  consistency and capacity measurements; no universal cost ordering is assumed.
- Review partition-key skew and cross-partition queries against the access patterns.
- An emulator may support local/CI development for a compatible platform/API,
  but cannot validate multi-region replication, production latency or capacity.
- Acceptance requires named reviewers, measured criteria and a migration/rollback
  plan; until then this record remains Proposed.

The ADR is short, scannable, and dated by its filename or front matter. Future-you (or a new teammate) can use it to recover why the system looks the way it does. It can reduce repeated debate, but neither a five-minute understanding time nor an annual re-litigation rate is guaranteed.

Design docs are the heavier sibling — used for larger changes (new service, major refactor, architecture shift). They cover problem, goals, non-goals, proposed approach, alternatives, rollout plan, risks, open questions. They are reviewed before the code; they prevent the "build it then realise it doesn't fit" tax.

The discipline that pays off: write the document before merging the decision. After-the-fact docs are weaker because the writer has already committed and is justifying rather than evaluating. A doc written during the decision phase forces the engineer to articulate the alternatives, which often surfaces the better choice.

Good enough to ship

• ADR per non-trivial decision; lives in the repo • Short, scannable, dated • Alternatives explicitly listed

Expert tier

• Design docs for major changes; reviewed before code • Preserve the original decision history; link the replacement ADR and mark the old record superseded • Team has a culture of "show me the ADR" in review

title: A common decision-making trap

You are tempted to "just write the code" because writing is the visible output. The senior move is to spend disproportionately more time on the bits before and after the code — reading the existing system, framing the decision, writing the docs, shaping the review. The right balance varies by task; the point is to budget for investigation, review and learning rather than count code as the only output.

Worked examples

Reading a new repo, the first 60 minutes

00-10  Find the composition root. ASP.NET Core: Program.cs. React: main.tsx
       or App.tsx. Read it once. Note services registered, middleware added,
       routes mapped.
10-25  Pick one endpoint or screen. Trace controller → service → repo, or
       component → hook → API call. Note the layering and naming conventions.
25-35  Read the project's README and CONTRIBUTING. Skim the docs/ folder if
       it exists.
35-45  Open the integration test for the endpoint you traced. Read it.
       The test shows an exercised contract.
45-60  Run the build + tests locally. Fix anything broken in setup.
       Now you can move.

What to notice:

  • The timeboxes illustrate a reading strategy, not a measured threefold productivity advantage.
  • The composition root + one trace + one test is the minimum viable map.
  • A local build checks the exercised build path; it does not establish parity with production operating systems, configuration or services.

A hypothesis-driven debug session

Bug: customers report "save failed" intermittently on the orders page.
 
Hypothesis 1: race between client-side validation and server-side save.
  Experiment: add log on server save entry, replay with delayed client.
  Result: correlated client/network evidence shows no request was sent. Server-log absence alone could also mean missing telemetry; narrow the hypothesis rather than declaring proof from absence.
 
Hypothesis 2: client throws before sending. JavaScript error.
  Experiment: open browser DevTools, reproduce.
  Result: console shows an undefined line-item dereference before dispatch.
  A minimal new-item fixture reproduces the same branch; test a populated
  item and missing item separately before choosing a domain contract.
 
Domain choice for this synthetic scenario: missing line item is invalid;
a present draft item without a persisted id is valid and uses the create path.
Fix: validate item presence at entry, then route by persisted-id presence.
Verify in the proposed component suite: missing item is rejected clearly,
a draft item creates, and a persisted item updates without a regression. A sanitized HAR records requests, not complete UI state;
it cannot by itself reproduce a pre-request exception.

What to notice:

  • Under the stated synthetic observations, the original request-race hypothesis is weakened by a reproduced pre-dispatch throw. Missing server logs alone would not rule it out.
  • The bug message ("save failed") was not what the bug was (client-side throw).
  • Separate missing-item, valid-draft and persisted-item regressions make the domain choice explicit. This is a proposed test design, not a component test executed here.

A well-formed PR description

## Why
 
The /orders endpoint exposes an owner email to callers who lack permission.
Our resource-access contract must distinguish authorized support viewers
from unrelated users before projecting any fields.
 
## What
 
This PR first authorizes access to the order. Owners receive permitted full
fields; explicitly authorized support viewers receive a minimal projection
without email; unrelated or cross-tenant callers are denied.
OrderAccessHandler evaluates authenticated tenant, subject and support policy.
 
## How to review
 
- Start with `OrderAccessHandler.cs`: the authz logic.
- Then `OrdersController.GetById`: authorization before projection.
- Then `OrderAccessHandlerTests.cs` + `OrdersControllerTests.cs`: contract tests
  cover owner, authorized support, unrelated user and cross-tenant cases.
 
## Trade-offs
 
- Masked emails can remain identifying. Removing email is not a substitute
  for denying unauthorized order access; authorized support needs its own policy.
- Performance assumption: an extra lookup may be needed unless the authorized
  query already returns ownership. Measure query count, latency and cost at the
  expected workload; 10 rps alone would not establish acceptability.
 
## Out of scope
 
- Privacy-reviewed access-decision audit without response PII. Tracked in #4271.
- Migration of historical logs. Tracked in #4272.

What to notice:

  • The reviewer knows why before they read the diff.
  • A reading order is suggested; respects the reviewer's time.
  • Trade-offs are surfaced, not buried.
  • Out-of-scope items get tickets; nothing falls through cracks.

An ADR for a small but durable choice

# ADR-014: Use System.Text.Json over Newtonsoft.Json in new code
 
## Status
Accepted, 2026-04-12 (fictional team/date for this worked example)
 
## Context
The codebase has historically used Newtonsoft.Json. Newer code is being added
to a service that ships allocations in the request path. System.Text.Json
is the ASP.NET Core default. Allocation gains are a hypothesis to measure
against our own converters, payloads and runtime, not a universal result.
 
## Decision
New code uses System.Text.Json. Existing Newtonsoft code is left alone; we
migrate file-by-file when those files are otherwise modified.
 
## Alternatives considered
- Migrate everything at once: rejected; high churn, low isolated value.
- Keep Newtonsoft: supported where its features/converters avoid migration risk.
  New endpoints with compatible contracts use the built-in default.
 
## Consequences
- Engineers must know both libraries for the migration period.
- Polymorphic deserialisation requires different attributes (System.Text.Json
  requires an explicit supported polymorphism contract). Neither library
  makes untrusted deserialization safe without correct settings/validation.
- The team's linting rules add a warning when Newtonsoft is imported in new files.

What to notice:

  • Short, decisive, alternatives stated.
  • Records compatibility/security assumptions explicitly; decisions compose.
  • Consequences include the cost (knowing both libraries during migration).

Hands-on exercises

  1. Goal: Read a new (to you) open-source repo in 60 minutes. Steps: (1) Pick a repo. (2) Open it, find the composition root, trace one feature end to end, read one test, build it locally. (3) Write a one-page map of the repo. You're done when you can explain to a friend how the repo is structured without referring back.

  2. Goal: Practice hypothesis-driven debugging on a known bug. Steps: (1) Find a recent bug in your team's tracker. (2) Before reading the fix, state three hypotheses about the root cause. (3) Read the fix; rate which hypothesis was closest. (4) Note what would have led you to the correct hypothesis faster. You're done when you can articulate the technique that would have shortened the investigation.

  3. Goal: Translate an async stack trace. Steps: (1) Obtain an authorized, sanitized async stack trace or a local reproducer. (2) For each frame, identify whether it's your code or framework. (3) For your-code frames, identify the actual method (MoveNext → enclosing class). (4) Reconstruct the call path in plain English. You're done when you can read async traces as fluently as sync traces.

  4. Goal: Profile and optimise one hot path. Steps: (1) Use dotnet-trace or another suitable profiler to locate a candidate; use BenchmarkDotNet for a controlled isolated comparison. (2) Identify the dominant cost (allocation, CPU, IO). (3) Make one targeted optimisation. (4) Re-measure. (5) Decide whether the gain was worth the readability cost. You're done when you have data-driven evidence of the change's effect.

  5. Goal: Split a 1000-line PR into three. Steps: (1) Find a large draft PR (yours or a teammate's). (2) Identify the logical splits (refactor first, then feature; introduce interface, then implementations). (3) Open three smaller PRs in dependency order. (4) Compare review experience. You're done when each PR has one reviewable purpose, preserves behavior or makes one explicit change, and passes its own tests; no review-time guarantee follows from line count.

  6. Goal: Write an ADR for a recent decision. Steps: (1) Pick a non-trivial choice your team made in the last month. (2) Write it up using the five-part format (status, context, decision, alternatives, consequences). (3) Open a PR adding it to docs/adr/. (4) Request review. You're done when the ADR is merged and a teammate has said it surfaced something they did not know.

Self-check questions

  1. What is the composition root of an ASP.NET Core app? Of a React app?
  2. Describe hypothesis-driven debugging in three sentences.
  3. What is the first thing you do when handed a new repo you've never seen?
  4. Why are tests good documentation, and what kind of test is best?
  5. Translate MyApp.Services.OrderService.<GetAsync>d__7.MoveNext() back to the method it belongs to.
  6. Why is "measure first" the rule for performance work?
  7. Name three .NET tools for performance measurement and when to use each.
  8. What's the upper bound on a reviewable PR size, and why?
  9. What does an ADR contain that a comment in code cannot?
  10. When do you write a design doc instead of an ADR?
  11. What's the difference between rubber-stamping a review and approving one?
  12. Why write the decision document before merging the decision?

On-call, incident writing, mentoring and rewrite decisions

The description promised more than PR mechanics. These practices connect them to operational responsibility.

Worked incident note

Synthetic incident: at 10:02 UTC save failures rise after build D; at 10:05 the on-call freezes promotion; at 10:09 an authorized feature rollback restores the prior path; at 10:15 error rate returns to baseline. Impact: 240 failed attempts, not automatically 240 unique users. Evidence: version-tagged request counts, sanitized client exception and a missing-line-item fixture. Uncertainty: delayed telemetry makes exact start time uncertain. Cause: a newly allowed draft item lacked the shape assumed by a submit handler. Contributing conditions: test data contained only persisted items and the canary lacked draft creation traffic. Actions: validate the domain boundary, add a draft-item test, include that journey in canary coverage, assign owners and verification dates. This is blameless analysis of system conditions, not “the engineer forgot to test”.

While on call, declare roles (incident lead, investigator, communicator), record facts and timestamps, and use reversible authorized mitigation first. Preserve a redacted timeline before artifacts expire. Do not wait for perfect reproduction to reduce impact, and do not claim the rollback alone established cause.

Mentoring and deliberate learning

A useful review comment explains the risk and offers a test: “An absent tenant check can expose another tenant's order; add the Bob-versus-Alice case before changing the projection.” Ask the learner to predict the result, run the smallest experiment, then explain the discrepancy. Keep a short error log and revisit one failed prediction after a week. Measure increasing independence on a concrete task rather than hours of tutorials or speed relative to colleagues.

Rewrite versus incremental repair: worked decision

Suppose an old import service has fragile parsing but stable billing rules. A rewrite might simplify interfaces yet lose undocumented billing edge cases. First capture representative inputs and output contracts, instrument failures, and isolate a parser seam. Replace that seam behind a feature flag; compare old/new results on authorized non-sensitive fixtures and keep a rollback path. Rewrite the whole service only when specific architectural constraints cannot be removed incrementally and migration, parity and rollback costs are funded. Age and lack of recent commits are not evidence for deletion.

Worked assessment answers

  1. A typical ASP.NET Core host is wired in Program.cs. A client React app may bootstrap in main.tsx, but framework routes/layouts and multiple hosts can have separate roots. Identify actual entry points rather than assuming one filename.
  2. State a falsifiable hypothesis, choose a controlled observation where competing causes predict different results, and update the hypothesis from the evidence. A matching observation can support more than one cause; seek a contrary case before calling it confirmed.
  3. Read repository instructions and setup/security constraints before running code, then locate entry points and trace one feature to its external dependencies. Write a map with unknowns and check it against tests and an owner, rather than claiming the whole repo is understood in an hour.
  4. Tests encode exercised expectations, not the complete specification. Integration tests reveal wiring and policy combinations; focused unit tests explain edge contracts. Choose according to the question and compare expected behavior with requirements.
  5. <GetAsync>d__7.MoveNext() is generated execution machinery for OrderService.GetAsync. A display class is a different compiler artifact. Follow application frames, inner failures and correlation context; do not blindly discard framework frames when they explain cancellation, serialization or DI.
  6. Profile representative work before optimizing so the change targets an observed bottleneck. Preserve baseline, workload, environment and distributions; a faster microbenchmark can still hurt end-to-end latency or semantics.
  7. dotnet-counters provides high-level runtime pressure; dotnet-trace captures event/profile evidence; BenchmarkDotNet compares isolated operations under controlled measurement. They answer different questions, and none alone reproduces production load or finds every I/O wait.
  8. There is no universal upper bound. Around 300 meaningful lines is a review planning cue. A coherent mechanical rename may be large; a ten-line authorization change can require deep review. Split so every intermediate commit remains testable and deployable.
  9. An ADR records architectural context, alternatives, decision, consequences and status across code locations; a line comment usually explains a local constraint. Keep rejected alternatives and the evidence that could reverse the choice.
  10. Use a design doc when coordinating a broader change: goals/non-goals, interfaces, rollout, migration, risks and unresolved questions need discussion. Record resulting durable choices in ADRs; neither document size nor a template grants approval.
  11. Substantive approval checks the behavior, negative cases and integration risks against the review policy; rubber-stamping only changes a status. Questions or risks can remain blocking even on a tiny diff.
  12. Writing during deliberation makes alternatives and assumptions explicit while they can still change. Mark unresolved choices Proposed, record the decision maker/evidence on acceptance, and preserve history when superseded; a retrospective document should say it is retrospective.

Worked practice solutions

Exercise 1 solution: Draw entry → auth → controller → service → data boundary for one actual feature and link the corresponding test. List multiple hosts and unknowns, confirm one path with an owner, and run only the authorized local setup. The one-hour timebox limits investigation; it does not certify completeness.

Exercise 2 solution: Before reading the historical fix, write predictions for a client pre-dispatch throw, a server failure and missing telemetry. Browser exception plus zero outgoing request narrows differently than missing server logs alone. Read the fix, compare each prediction with evidence, and record what remained ambiguous; closeness to a known fix is not independent causal proof.

Exercise 3 solution: Translate <GetAsync>d__7.MoveNext() to the GetAsync state-machine context using matching source/symbols. Preserve the full sanitized trace and relevant inner exceptions/framework frames. Use bare throw; inside a catch when rethrowing the same exception; throw ex; restarts its stack history. Treat generated names as implementation details.

Exercise 4 solution: Record baseline runtime/configuration, input distribution and a representative profile. Change one measured bottleneck, rerun correctness/edge tests, and compare repeated like-for-like measurements including allocation and tail effects. Reject an optimization that changes duplicate/order/equality semantics or merely exploits a warmed cache absent in the baseline.

Exercise 5 solution: Split a mixed change into behavior-preserving seam extraction, feature introduction and migration/removal after compatibility checks. Each PR states its purpose, dependencies and negative tests and remains independently testable; do not merge an intermediate authorization bypass to satisfy a size target. Review time and line-count targets remain planning signals, not pass criteria.

Exercise 6 solution: Use status, context, decision, alternatives and consequences. For the Cosmos scenario keep status Proposed until topology, partition skew, consistency, latency/cost and rollback acceptance criteria are reviewed; an emulator does not settle those questions. Record the accepted decision separately with owners/date/evidence, preserve the rejected alternative, and link a superseding ADR if evidence later changes. These are reference outcomes, not PRs or approvals performed here.

Allocation counterexample: a new temporary object can become unreachable immediately; a tiny object reachable through a static dictionary may live indefinitely. Retention analysis follows roots and references, not the number of new expressions. For 10,000 items, a nested membership scan can require up to 100 million comparisons while a hash-index plan performs roughly 20,000 insert/lookups in the average case, at an additional-memory and hash-behavior cost. Compare semantics (duplicates, equality, ordering) before celebrating the operation count.

High-signal resources

Optional bibliography and discovery indexes, not evidence that every book edition or linked post was verified. The primary passages supporting this review are saved separately. Recommendations below do not certify a current author role, topic coverage, career outcome or universal best practice.

Official docs

Books or courses

  • The Pragmatic Programmer — Hunt & Thomas. Optional software-practice reading; editions differ, so no rolling age or canonical ranking is asserted.
  • A Philosophy of Software Design — John Ousterhout. Optional reading on software-design complexity; verify the chosen edition.
  • Designing Data-Intensive Applications — Martin Kleppmann. Optional systems reading; check edition/authorship and contents for the topic at hand.

Practitioner posts

Weekly milestones

Suggested timeboxes only; do not run unfamiliar repositories, capture production data or open/merge PRs without the applicable authorization. Career results and review times are not guaranteed.

  1. Day 1: Read a new open-source repo in 60 minutes; write the one-page map. Answer self-check 1, 3, 4.
  2. Day 2: Practice hypothesis-driven debugging on a recent bug; document the lesson. Answer self-check 2.
  3. Day 3: Translate three authorized, sanitized async stack traces. Use a representative trace/profile to locate a hot path, then BenchmarkDotNet to compare a suitable isolated operation. Answer self-check 5, 6, 7.
  4. Day 4-5: Split one large PR into three smaller ones. Self-review and write the description for each. Answer self-check 8, 11.
  5. Day 6-7: Write one ADR for a recent decision; merge it. Read one design doc your team has produced; note what made it useful or not. Answer self-check 9, 10, 12.

How it shows up in the capstone

A proposed capstone should demonstrate these habits as artifacts. It would include docs/adr/ decision records for every non-obvious choice (auth library, frontend framework, deployment topology). The README's "first 30 minutes" section is the onboarding map — it names the composition root, points at the first endpoint to read, and lists the integration test that documents the contract.

The proposed PR history would contain coherent changes with substantive descriptions; docs/design/ would explain changes needing broader coordination. Link bug reports, review decisions and resulting ADRs. These are acceptance artifacts to create, not repository contents verified by this review.

Judge progress by concrete artifacts: a checked dependency map, a hypothesis with a falsifying test and a reviewable decision record. Time to productivity and senior-level expectations depend on context; no one-day or promotion guarantee follows from completing the chapter.


Previous chapter → Ch 21 — Testing strategy
Back to hub → From Novice to Fluent on the Modern Microsoft Web Stack

Corrected contracts and failure analysis

Treat debugging as discrimination between hypotheses. A breakpoint is an experiment: choose a location where candidate causes predict different values, inspect variables and the call stack, and preserve the smallest reproducible input. A stack trace is evidence of a failure path, not proof the topmost application frame caused the underlying defect. Inside the catch handler, preserve the same exception's stack with throw; rather than throw ex;; rethrowing later/outside that handler needs an appropriate mechanism such as ExceptionDispatchInfo.

Performance claims need baseline, workload, environment, distributions and a counterfactual. Warm-up, caching and sample size can make a microbenchmark misleading. A PR should explain the behavior change, alternatives, failure cases and exact tests, not just enumerate files. An ADR records context, decision, consequences and supersession; it need not turn a small reversible choice into a permanent rule.

Boundary exercise with solution

Two fixes both stop a crash: add a null check everywhere or reject one malformed API field. How would you choose?

Solution and reasoning

Trace the value from the trust boundary, build the minimal malformed input and assert the contract. If null is invalid at entry, reject it there with a useful error and keep internal invariants. If absence is a valid domain state, model it explicitly. A broad catch or scattered checks can conceal the defect rather than fix it.

Source-backed review notes

Official/maintainer raw and canonical snapshots were refreshed on 9 October 2026. The batch-18 packet retains exact governing quotations, extraction rules and hashes. Mutable documentation is not an installed-runtime pin; historical/synthetic examples remain separate from observed company or service outcomes.

Pause / Recall / Apply

Can you explain it without the page?

Close the example. Reconstruct the core idea, then change one assumption. Mark complete when you’re ready; you can always undo it.

Stored in this browser only. No account, no sync. Clearing browser data removes your record.