.NET Backend Developer Top 100 Interview Questions
100 .NET backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable C# code. Covers the language and runtime, async and concurrency, the web pipeline, data access, caching, security, testing, diagnostics, performance and system design, from the basics to production incidents.
Questions 81-100 of 100
Q81.How would you design order processing in .NET so that saving data and publishing events never get out of sync?
Use the outbox pattern: in the same database transaction that saves the order, also insert the event into an outbox table. A separate background process reads unpublished outbox rows and publishes them to the message broker, marking them as sent. Either both the order and its event are stored, or neither is.
flowchart LR A["Place order request"] --> B["One transaction: insert order and outbox row"] B --> C["Commit"] C --> D["Outbox publisher background service"] D --> E["Message broker"] E --> F["Consumers: email, inventory, analytics"] D --> G["Mark outbox row as published"]Why it is needed: saving to the database and then publishing directly can fail between the two steps, leaving an order without its event, or an event for an order that was rolled back.
Design details:
- At-least-once delivery: the publisher may resend after a crash, so every event has a unique ID and consumers are idempotent.
- Ordering: publish in insertion order per aggregate, such as per order, when consumers depend on order.
- Cleanup: delete or archive published rows after a retention period.
- Monitoring: alert on the age of the oldest unpublished row.
The same idea works in reverse with an inbox table, which lets consumers detect duplicate messages.
Q82.How would you design real-time notifications for a .NET web application?
Keep persistent connections, such as WebSockets with fallbacks, between clients and a set of gateway instances, and use a backplane, a shared publish-subscribe channel, so a notification raised on any instance reaches users connected to any other.
flowchart LR A["Domain events from services"] --> B["Notification service"] B --> C["Backplane: publish-subscribe channel"] C --> D["Gateway instance 1"] C --> E["Gateway instance 2"] D --> F["Connected users"] E --> G["Connected users"] B --> H["Stored notifications for offline users"]Key decisions:
- Authentication on connect, with users joined to groups, such as user ID or team, for targeted messages.
- Persistence: store notifications so users who were offline can fetch missed ones; real-time delivery is a bonus, not the only path.
- Delivery semantics: at-most-once over the live connection, with the stored list as the source of truth.
- Scaling: connections consume memory, so size gateway instances by connection count, and spread reconnections after deployments.
- Backpressure: drop or merge messages for slow clients rather than buffering without limit.
- Fallback: clients poll when persistent connections are blocked.
Q83.How would you design a multi-tenant API in .NET?
Identify the tenant on every request, carry it through the whole request and enforce isolation at the data layer, so no code path can accidentally return another tenant's data. Choose the data isolation model by compliance needs, scale and cost.
Shared tables with a tenant column Schema per tenant Database per tenant Every row has a tenant ID; queries always filter by it Each tenant has its own schema in a shared database Each tenant has its own database Cheapest and simplest to operate at scale Better isolation, moderate overhead Strongest isolation, highest cost Risk: a missing filter leaks data Risk: migrations across many schemas Risk: operating many databases Making isolation reliable:
- Resolve the tenant once, from the token or host name, in middleware, and store it in a scoped tenant context.
- Apply filters automatically, with global query filters in the mapper or row-level security in the database, rather than relying on every query.
- Include the tenant in cache keys, log fields and metrics.
- Test isolation: automated tests that request another tenant's resource and expect 404.
- Limit noisy tenants with per-tenant rate limits and quotas.
Q84.How would you design reliable background job processing in a .NET system?
Store jobs durably, process them with worker services that limit concurrency, retry failures with backoff and move jobs that keep failing to a dead-letter store. Workers must assume that any job can run more than once.
flowchart TD A["API enqueues job in the same transaction as its data"] --> B["Durable job store or queue"] B --> C["Worker service with bounded concurrency"] C --> D{"Result"} D -->|Success| E["Mark complete"] D -->|Transient failure| F["Schedule retry with backoff"] D -->|Permanent failure or attempts exhausted| G["Dead-letter and alert"]Design details:
- Idempotent handlers: check state before acting, or record processed job IDs.
- Leases: a worker claims a job for a limited time, and another worker can take it if the lease expires after a crash.
- Separate queues by priority and duration, so long reports do not delay password reset emails.
- Scoped dependencies: create a dependency injection scope per job.
- Graceful shutdown: stop claiming new jobs, finish or release current ones.
- Scheduling: recurring jobs need a single scheduler, or a lock, so they do not run once per instance.
- Observability: queue depth, oldest job age, duration, failure rate and dead-letter size per job type.
Q85.How would you implement an LRU cache in C#?
Combine a dictionary for lookups with a linked list ordered by recency. The dictionary maps each key to its list node, so both lookups and moving a node to the front take constant time, and the least recently used entry is always at the end of the list.
Operations:
- Get: find the node, move it to the front and return its value.
- Put: update and move an existing node, or add a new one at the front; if the count exceeds capacity, remove the last node and its dictionary entry.
Production considerations:
- Thread safety: protect operations with a lock, or shard the cache by key hash for high concurrency.
- Expiry: add timestamps when data can become stale.
- Memory-based limits for values of different sizes.
- Prefer existing caches in production when they meet the need; implementing one is mainly a test of data structure understanding.
Example
After reading
a, addingcevictsb, and readingaagain moves it to the front.var cache = new LruCache<string, int>(2); cache.Put("a", 1); cache.Put("b", 2); cache.TryGet("a", out _); cache.Put("c", 3); Console.WriteLine(cache.TryGet("b", out _)); // False Console.WriteLine(cache.TryGet("a", out int a) ? a : -1); // 1 Console.WriteLine(string.Join(",", cache.Keys)); // a,c class LruCache<TKey, TValue> where TKey : notnull { private readonly int capacity; private readonly Dictionary<TKey, LinkedListNode<(TKey Key, TValue Value)>> map = []; private readonly LinkedList<(TKey Key, TValue Value)> order = new(); public LruCache(int capacity) => this.capacity = capacity; public IEnumerable<TKey> Keys => order.Select(e => e.Key); public bool TryGet(TKey key, out TValue value) { if (map.TryGetValue(key, out var node)) { order.Remove(node); order.AddFirst(node); value = node.Value.Value; return true; } value = default!; return false; } public void Put(TKey key, TValue value) { if (map.Remove(key, out var existing)) order.Remove(existing); map[key] = order.AddFirst((key, value)); if (map.Count > capacity) { var last = order.Last!; order.RemoveLast(); map.Remove(last.Value.Key); } } }Q86.How would you produce a grouped sales report with LINQ?
Group the records by the reporting dimension, compute aggregates for each group and sort the result. Nested grouping finds values within each group, such as the top product per region, and the newer
CountByandAggregateByoperators compute simple per-key aggregates without building intermediate groups.Steps:
GroupBythe key, such as region.Selecta result per group withSum,CountorMax.- For "top item per group," group again inside each group and order by the aggregate.
- Break ties deterministically with
ThenBy, so results are stable. - Order the final rows for presentation.
Performance notes:
- In memory, LINQ is fine for thousands of rows; for millions, aggregate in the database, where indexes and query engines are far more efficient.
- With a database provider, the same query shape translates to SQL
GROUP BYwhen every part is translatable. - Use
decimalfor money, so totals are exact.
Example
Revenue per region with the top product in each, plus order counts per region.
var orders = new[] { new Order("north", "lamp", 2, 450m), new Order("south", "desk", 1, 7200m), new Order("north", "desk", 1, 7200m), new Order("north", "lamp", 1, 450m), new Order("south", "pen", 10, 20m), }; var report = orders .GroupBy(o => o.Region) .Select(g => new { Region = g.Key, Revenue = g.Sum(o => o.Quantity * o.Price), Top = g.GroupBy(o => o.Product) .OrderByDescending(p => p.Sum(o => o.Quantity * o.Price)) .ThenBy(p => p.Key) .First().Key, }) .OrderByDescending(r => r.Revenue); foreach (var r in report) Console.WriteLine($"{r.Region}: revenue={r.Revenue} top={r.Top}"); Console.WriteLine(string.Join(", ", orders.CountBy(o => o.Region).Select(kv => $"{kv.Key}={kv.Value}"))); // Output: // north: revenue=8550 top=desk // south: revenue=7400 top=desk // north=3, south=2 record Order(string Region, string Product, int Quantity, decimal Price);Q87.How would you consume a channel in batches, flushing by size or by time?
Collect items from the channel until the batch reaches a maximum size or a short time limit passes since the batch started, then flush it. Batching cuts the cost of writes, such as database inserts or API calls, while the time limit keeps latency low when traffic is light.
Algorithm:
- Wait until at least one item is available.
- Start a timer for the batch.
- Read items while the batch is not full; if none is immediately available, wait for more until the timer expires.
- Flush whatever has been collected, then start over.
- When the channel is completed and empty, flush the final partial batch and stop.
Production details:
- Handle flush failures: retry the batch, or send it to a dead-letter store, without losing items.
- Pass a cancellation token for shutdown, and flush pending items before exiting.
- Expose batch sizes and flush latency as metrics to tune the limits.
Example
Six items arrive together, then three more after a pause; the size limit, the time limit and completion each end a batch.
using System.Threading.Channels; var channel = Channel.CreateBounded<int>(100); var batches = new List<string>(); Task consumer = ConsumeInBatchesAsync(channel.Reader, 4, TimeSpan.FromMilliseconds(50), b => batches.Add(string.Join(",", b))); for (int i = 1; i <= 6; i++) await channel.Writer.WriteAsync(i); await Task.Delay(150); for (int i = 7; i <= 9; i++) await channel.Writer.WriteAsync(i); channel.Writer.Complete(); await consumer; Console.WriteLine(string.Join(" | ", batches)); // 1,2,3,4 | 5,6 | 7,8,9 static async Task ConsumeInBatchesAsync(ChannelReader<int> reader, int maxBatch, TimeSpan maxWait, Action<List<int>> flush) { var batch = new List<int>(maxBatch); while (await reader.WaitToReadAsync()) { using var timer = new CancellationTokenSource(maxWait); try { while (batch.Count < maxBatch) { if (reader.TryRead(out int item)) { batch.Add(item); continue; } if (!await reader.WaitToReadAsync(timer.Token)) break; } } catch (OperationCanceledException) { } if (batch.Count > 0) { flush([.. batch]); batch.Clear(); } } }Q88.How would you parse and validate CSV input in C#, collecting all errors?
Read the input line by line, split each line into fields, validate and convert each field with culture-invariant parsing and collect errors with their line numbers instead of stopping at the first one. Return both the valid rows and the errors, so callers can decide whether to accept a partial import.
Steps:
- Skip the header, or use it to map columns by name.
- For each line, split into fields and check the field count.
- Use
TryParsewithCultureInfo.InvariantCulture, so decimals parse the same on every server. - Record a clear error for each failing field, with the line number.
- Return results in a result type, not by throwing.
Real-world considerations:
- Quoted fields with commas need a proper CSV parser, not
Split. - Stream large files instead of loading them whole.
- Limit the number of errors reported, so a wrong file does not produce millions of messages.
- Normalize whitespace and character encodings.
Example
Two valid rows are kept, and two bad rows are reported with their line numbers.
using System.Globalization; string csv = """ sku,quantity,price LAMP-1,3,450.00 DESK-2,x,7200 ,1,10 PEN-3,10,20.50 """; var (rows, errors) = Parse(csv); decimal total = rows.Sum(r => r.Quantity * r.Price); Console.WriteLine($"{rows.Count} valid, total {total.ToString(CultureInfo.InvariantCulture)}"); errors.ForEach(Console.WriteLine); // Output: // 2 valid, total 1555.00 // line 3: quantity 'x' is not a whole number // line 4: sku is required static (List<Row> Rows, List<string> Errors) Parse(string text) { var rows = new List<Row>(); var errors = new List<string>(); string[] lines = text.Split('\n'); for (int i = 1; i < lines.Length; i++) { string[] f = lines[i].Trim().Split(','); int lineNo = i + 1; if (f.Length != 3) { errors.Add($"line {lineNo}: expected 3 fields"); continue; } if (string.IsNullOrWhiteSpace(f[0])) { errors.Add($"line {lineNo}: sku is required"); continue; } if (!int.TryParse(f[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int qty)) { errors.Add($"line {lineNo}: quantity '{f[1]}' is not a whole number"); continue; } if (!decimal.TryParse(f[2], NumberStyles.Number, CultureInfo.InvariantCulture, out decimal price)) { errors.Add($"line {lineNo}: price '{f[2]}' is not a number"); continue; } rows.Add(new Row(f[0], qty, price)); } return (rows, errors); } record Row(string Sku, int Quantity, decimal Price);Q89.How would you implement a token bucket rate limiter in C# that is easy to test?
Keep a token count that refills continuously at a fixed rate up to a capacity. Each request takes one token if available; otherwise it is rejected. Reading the time from an injected
TimeProvidermakes the limiter deterministic in tests.How it works:
- On each request, add tokens for the time elapsed since the last update:
elapsed seconds × refill rate, capped at the capacity. - If at least one token is available, subtract it and allow the request.
- Otherwise, reject it, optionally reporting when the next token will be available.
Design points:
- Capacity controls the allowed burst; the refill rate controls the long-term average.
- Thread safety: a lock around the small update is enough for most services.
- Per-client limits: keep one bucket per key in a dictionary, and evict idle buckets.
- Distributed limits: across many instances, the state must live in a shared store.
The web stack provides ready-made limiters, but writing one shows you understand the algorithm and its trade-offs.
Example
Three tokens allow a burst of three; two seconds later, two tokens have refilled.
var clock = new ManualClock(new DateTimeOffset(2026, 3, 1, 9, 0, 0, TimeSpan.Zero)); var bucket = new TokenBucket(clock, capacity: 3, refillPerSecond: 1); Console.WriteLine(string.Join(",", Enumerable.Range(0, 5).Select(_ => bucket.TryTake()))); // True,True,True,False,False clock.Advance(TimeSpan.FromSeconds(2)); Console.WriteLine($"{bucket.TryTake()},{bucket.TryTake()},{bucket.TryTake()}"); // True,True,False class ManualClock : TimeProvider { private DateTimeOffset now; public ManualClock(DateTimeOffset start) => now = start; public override DateTimeOffset GetUtcNow() => now; public void Advance(TimeSpan by) => now += by; } class TokenBucket { private readonly TimeProvider time; private readonly double capacity; private readonly double refillPerSecond; private readonly Lock gate = new(); private double tokens; private DateTimeOffset last; public TokenBucket(TimeProvider time, double capacity, double refillPerSecond) { this.time = time; this.capacity = capacity; this.refillPerSecond = refillPerSecond; tokens = capacity; last = time.GetUtcNow(); } public bool TryTake() { lock (gate) { DateTimeOffset now = time.GetUtcNow(); tokens = Math.Min(capacity, tokens + (now - last).TotalSeconds * refillPerSecond); last = now; if (tokens < 1) return false; tokens -= 1; return true; } } }- On each request, add tokens for the time elapsed since the last update:
Q90.How would you order jobs so that each one runs after the jobs it depends on?
Treat the jobs as a directed graph and produce a topological order. Count each job's unfinished dependencies, start with the jobs that have none, and each time a job is finished, decrease the count for the jobs that depend on it, adding those that reach zero. If some jobs never reach zero, the graph contains a cycle.
Steps:
- Build a map from each job to the jobs that depend on it, and a count of dependencies per job.
- Put every job with zero dependencies into a ready queue.
- Repeatedly take a job from the queue, add it to the result and update its dependents.
- If the result is shorter than the number of jobs, report a cycle.
Practical notes:
- Use a priority queue ordered by name, or by priority, for deterministic results when several jobs are ready.
- Jobs in the ready set at the same time can run in parallel.
- The algorithm takes time proportional to jobs plus dependencies.
Uses: build pipelines, database migrations, deployment steps and data pipelines with dependent tasks.
Example
Independent jobs come first alphabetically, and a cycle is detected and reported.
var jobs = new Dictionary<string, string[]> { ["deploy"] = ["test", "build"], ["test"] = ["build"], ["build"] = ["restore"], ["restore"] = [], ["docs"] = [], }; Console.WriteLine(string.Join(" -> ", Order(jobs))); // docs -> restore -> build -> test -> deploy try { Order(new() { ["a"] = ["b"], ["b"] = ["a"] }); } catch (InvalidOperationException e) { Console.WriteLine(e.Message); // cycle detected } static List<string> Order(Dictionary<string, string[]> deps) { var remaining = deps.ToDictionary(kv => kv.Key, kv => kv.Value.Length); var dependents = deps.Keys.ToDictionary(k => k, _ => new List<string>()); foreach (var (job, needs) in deps) foreach (var need in needs) dependents[need].Add(job); var ready = new PriorityQueue<string, string>(StringComparer.Ordinal); foreach (var (job, count) in remaining) if (count == 0) ready.Enqueue(job, job); var order = new List<string>(); while (ready.TryDequeue(out var job, out _)) { order.Add(job); foreach (var next in dependents[job]) if (--remaining[next] == 0) ready.Enqueue(next, next); } if (order.Count != deps.Count) throw new InvalidOperationException("cycle detected"); return order; }Q91.How would you make a payment endpoint safe to retry with idempotency keys?
Clients send a unique idempotency key with each logical operation. The server stores the result of the first request for that key and returns the same result for any repeat, so a retry after a timeout never charges twice. Concurrent duplicates must also share one execution.
Design points:
- Key scope: keys belong to a client or user, so different clients cannot collide.
- Store the result, including the status code and body, for a retention period, such as 24 hours.
- Handle concurrency: if a second request arrives while the first is still running, it waits for the same result instead of running again.
- Check the payload: reject a reused key with a different request body, which signals a client bug.
- Failures: decide whether failed attempts are stored, so retries see the failure, or removed, so retries can try again; transient failures are usually removed.
- Durability: in production, store keys in the database, ideally in the same transaction as the payment record.
The in-memory version below shows the core idea with a lazy task per key.
Example
A repeated key returns the stored result, and two concurrent requests with the same key run the charge once.
using System.Collections.Concurrent; var store = new IdempotencyStore(); int charges = 0; Task<string> Charge(string key, decimal amount) => store.RunOnceAsync(key, async () => { int n = Interlocked.Increment(ref charges); await Task.Delay(20); return $"charged {amount} (charge #{n})"; }); Console.WriteLine(await Charge("key-1", 1250m)); // charged 1250 (charge #1) Console.WriteLine(await Charge("key-1", 1250m)); // charged 1250 (charge #1) string[] both = await Task.WhenAll(Charge("key-2", 900m), Charge("key-2", 900m)); Console.WriteLine(both[0] == both[1]); // True Console.WriteLine(charges); // 2 class IdempotencyStore { private readonly ConcurrentDictionary<string, Lazy<Task<string>>> results = new(); public Task<string> RunOnceAsync(string key, Func<Task<string>> action) => results.GetOrAdd(key, _ => new Lazy<Task<string>>(action)).Value; }Q92.How do you migrate a legacy .NET Framework application to modern .NET?
Migrate incrementally, keeping the application running and shipping throughout, rather than attempting a big-bang rewrite. Modern .NET is largely compatible at the language level, but some framework features, such as older web forms and server-side configuration models, have no direct equivalent.
Steps:
- Inventory: list projects, dependencies and framework features in use, and run an upgrade analysis tool to find incompatible APIs.
- Upgrade dependencies to versions that support modern .NET, or find replacements.
- Move shared libraries first, targeting a standard that both old and new runtimes can use, so they work on both during the transition.
- Migrate the web layer incrementally: route some endpoints to a new modern application behind a proxy, moving features one at a time while sharing authentication and session state.
- Replace configuration and dependency injection patterns with the modern equivalents.
- Test thoroughly, with automated tests added before migrating risky areas.
Benefits: much better performance, cross-platform deployment in containers, long-term support releases and modern tooling. Plan for some rewrite where old technologies cannot move, and budget time for it explicitly.
Q93.How do you upgrade a .NET service between major versions safely?
Upgrade on a schedule tied to the support lifecycle, read the breaking change notes, update the target framework and dependencies together and roll out gradually with monitoring.
Long-term support releases Standard-term support releases Even-numbered releases, supported for three years Odd-numbered releases, supported for a shorter period Fewer upgrades, more stability Earlier access to new features and performance gains Best for most production services Best for teams that upgrade every year anyway Upgrade checklist:
- Read breaking change notes for the runtime, the web stack and the libraries you use.
- Update the target framework, SDK version and packages in one branch, using central package management to keep versions aligned.
- Fix new analyzer warnings, which often point to changed behavior.
- Run all tests, including integration tests against real dependencies.
- Benchmark key paths, since performance usually improves but sometimes shifts.
- Roll out with a canary, watching errors, latency, memory and garbage collection metrics.
- Update container base images and the deployment pipeline's SDK.
Upgrading every year in small steps is far less risky than jumping several versions at once.
Q94.How do you secure a .NET web API end to end?
Layer defenses across transport, identity, input handling, data access, secrets and dependencies, and make secure behavior the default so individual endpoints cannot forget it.
Transport and HTTP:
- HTTPS everywhere with strict transport security, safe CORS rules and security headers.
- Request size limits and rate limits, especially on authentication and expensive endpoints.
Identity:
- Validate tokens fully, and require authorization by default with policies; mark public endpoints explicitly.
- Check resource ownership in every handler that loads data by ID.
Input and output:
- Validate requests, reject unknown fields and use parameterized queries.
- Encode output rendered in HTML, and never return internal exception details.
Secrets and data:
- Secrets from a secret store or managed identity, never from source code.
- Encrypt sensitive data at rest, and protect keys centrally.
- Hash passwords with a slow, salted algorithm if you store them at all.
Supply chain and operations:
- Audit packages for vulnerabilities in continuous integration, and update regularly.
- Log security events, such as failed logins and authorization failures, without sensitive data.
- Run with least privilege in containers, as a non-root user with a read-only file system.
Q95.How do you improve startup time and memory use for .NET services?
Measure first, then remove work from startup, precompile code and reduce what the process loads. Faster startup matters for autoscaling, scale-to-zero and quick deployments; lower memory means denser hosting and cheaper infrastructure.
Startup:
- Profile startup to see time spent in dependency injection setup, configuration, database warm-up and first requests.
- Defer expensive initialization, such as loading large caches, to background tasks after the service reports ready, if that is acceptable.
- Ready-to-run compilation precompiles assemblies, reducing just-in-time work at startup.
- Native ahead-of-time compilation gives the fastest startup, if the code and libraries are compatible.
- Source generators replace reflection-heavy startup work, such as serializers and configuration binding.
Memory:
- Trimming removes unused code from self-contained deployments.
- Garbage collection settings that adapt heap counts to the load reduce memory for lightly used services.
- Fewer allocations on hot paths reduce the heap's steady-state size.
- Right-sized caches with limits.
Verify with numbers: time to first successful request and steady-state memory under realistic load, before and after each change.
Q96.When do you choose an object-relational mapper and when hand-written SQL in .NET?
Choose by the complexity of the queries, the team's comfort with SQL and the performance requirements. Many services use both: a mapper for everyday reads and writes, and hand-written SQL for complex reports and hot paths.
Object-relational mapper Lightweight mapping with hand-written SQL Raw data access classes Queries written in LINQ, with change tracking and migrations SQL written by hand, results mapped to objects automatically Commands, parameters and readers handled manually Fastest development for typical create, read, update and delete work Full control of SQL with little mapping code Maximum control and minimum overhead Risk: hidden inefficient queries Risk: more SQL to maintain and test Risk: verbose code and mapping mistakes Best for domain-rich applications Best for read-heavy services and reporting Best for very hot paths and special features Guidelines:
- Review generated SQL for important queries, whatever approach you use.
- Keep data access behind small interfaces, so the approach can change per use case.
- Test queries against the real database engine, not in-memory substitutes.
- Measure before rewriting mapper queries by hand; many slow queries only need a projection, an index or a no-tracking read.
Q97.What do you look for when reviewing C# backend code?
Focus review time on the mistakes that compile cleanly but fail in production: async misuse, resource handling, concurrency, data access and security. Let analyzers and formatters handle style.
Checklist:
- Async: no
.Resultor.Wait(), noasync void, cancellation tokens passed everywhere, no fire-and-forget tasks without handling. - Resources:
usingorawait usingfor disposables, andHttpClientreused correctly. - Dependency injection: no scoped services captured by singletons, and no service locator calls.
- Concurrency: shared state protected, no
lockaround slow input and output, and correct use of concurrent collections. - Data access: parameterized queries, no N+1 patterns, projections for read endpoints and short transactions.
- Exceptions: specific types, no swallowed exceptions,
throw;to rethrow and consistent error responses. - Security: authorization on every endpoint, ownership checks and no secrets or personal data in logs.
- Nullability: no suppressed warnings without a reason.
- Tests: behavior and failure paths covered, with deterministic time.
Automate the routine: treat warnings as errors, enable the built-in analyzers and add custom rules for recurring review comments.
Interview tip: citing a real bug you found in review, such as a captive dependency or a missing cancellation token, and the analyzer rule you added afterwards, makes this answer memorable.
- Async: no
Q98.How do you decide between a modular monolith and microservices for a .NET system?
Start from team size, domain boundaries and operational maturity rather than architecture fashion. Most new systems benefit from a well-structured modular monolith first, with modules that can be extracted into services later if a real need appears.
Modular monolith Microservices One deployable with strict internal module boundaries Many independently deployed services Simple deployment, debugging and transactions Independent scaling, deployment and technology choices per service One database, possibly with a schema per module A database per service, with data shared through APIs and events Risk: boundaries erode without discipline Risk: network failures, distributed data consistency and operational overhead Best for small and medium teams and evolving domains Best for large organizations with stable, well-understood domains Signals that extraction is worth it:
- A module needs very different scaling or availability.
- Separate teams are blocked by shared releases.
- A module has a clear, stable interface and little shared data.
Keeping the monolith modular: separate projects or namespaces per module, internal types, module-owned data and communication through explicit interfaces or in-process events.
Q99.How should you explain a .NET backend project you personally worked on?
Tell one project as a story: the problem, your role, the key technical decisions, a hard problem you solved and the measured results. Interviewers want evidence that you understand async code, data access and production behavior, not a list of libraries.
- Context: what the service did, its users and its scale, such as requests per second or data volume.
- Your role: what you designed or built personally.
- Decisions: how you structured the solution, handled async work, accessed data, cached and secured the API, and why.
- A hard problem: thread pool starvation, a memory leak, N+1 queries or a concurrency bug, with the counters, traces or dumps that revealed the cause.
- Quality: tests, deployment practices and monitoring.
- Results: numbers such as "p95 latency fell from 800 ms to 120 ms" or "memory dropped by 40% after pooling buffers".
- Reflection: what you would do differently now.
What the interviewer is listening for:
- Understanding of the runtime: async, garbage collection, dependency injection lifetimes.
- Pragmatic trade-offs rather than pattern-driven design.
- Clear ownership and measurable impact.
Interview tip: prepare one sentence that explains the hardest bug you fixed and how you proved the fix worked; it is often the most remembered part of the interview.
Q100.How do you discuss a production incident you handled in a .NET service?
Walk through the incident in order, from detection to prevention, and show how you used the platform's diagnostics, such as runtime counters, traces and dumps, to find the cause.
- Detection: the alert or report, and how quickly it fired.
- Impact: what users experienced and for how long.
- Mitigation: rollback, scaling, a feature switch or a restart, and why you chose it.
- Diagnosis: for example, thread pool counters showing starvation, a dump showing threads blocked on
.Result, or heap snapshots showing an event subscription leak. - Root cause and contributing factors: the code change, missing tests or alerts and any process gaps.
- Fix: the change, with a test that reproduces the problem.
- Prevention: analyzers, alerts on thread pool queue length or generation 2 growth, load tests and review checklist items.
What the interviewer is listening for:
- Calm prioritization of user impact.
- Evidence-based diagnosis with the right tools.
- Blameless, system-level thinking.
- Lasting improvements that were verified afterwards.