Prepare for .NET Core developer interviews with questions grouped by experience level, from fundamentals to production-scale architecture.
Junior (0-2 years)
.NET Core (now unified into .NET 6 and later as simply '.NET') is a cross-platform, open-source runtime that runs on Windows, Linux, and macOS, unlike the .NET Framework, which is Windows-only. It's also modular, so an application only brings in the packages it actually needs, which makes it lighter and faster to deploy, especially inside containers.
The Common Language Runtime is the execution engine that runs .NET applications: it handles memory management through garbage collection, just-in-time compilation of intermediate language (IL) code into native machine code, and exception handling. Every .NET language (C#, F#, VB.NET) compiles down to the same IL, which the CLR then executes.
.NET Framework is the original, Windows-only implementation. .NET Core is the cross-platform rewrite. .NET Standard was a specification, not a runtime, defining a common set of APIs that both could implement, so a library built against it could run on either. Since .NET 5, Microsoft unified everything under a single '.NET' going forward, and .NET Standard is now mostly legacy.
It's the application's entry point, containing the Main method (or, in newer minimal-hosting-model projects, top-level statements) that configures and starts the application, wiring up services, middleware, and the web host before the application begins handling requests.
.NET's package manager, used to install, update, and manage third-party and Microsoft-provided libraries in a project, similar in role to npm for JavaScript or pip for Python.
A console application runs a single Main method and exits when it finishes, useful for scripts, batch jobs, or CLI tools. A web application hosts a long-running web server (Kestrel by default) that listens for and responds to HTTP requests until explicitly shut down.
Dependency injection is built directly into the framework, not added via a third-party library. Services are registered in the DI container with a lifetime, AddSingleton, AddScoped, or AddTransient, and the framework automatically injects them into constructors wherever they're declared as a parameter, without you writing manual wiring code.
Transient creates a new instance every time the service is requested. Scoped creates one instance per HTTP request, shared across that request. Singleton creates a single instance for the entire lifetime of the application, shared across every request, which means it must be thread-safe since multiple requests can use it concurrently.
This is a common configuration error called 'captive dependency': the Scoped service gets captured by the Singleton on its first resolution and effectively becomes a singleton itself, since the Singleton is only created once, defeating the purpose of the Scoped lifetime and potentially causing stale data or threading issues. Most DI containers, including .NET's built-in one, will throw a validation error if this is detected.
Inject IConfiguration into a class and access values with a key path, like configuration["ConnectionStrings:Default"], or bind an entire section to a strongly-typed class using the Options pattern, which is generally preferred for anything beyond a single ad-hoc value.
appsettings.json holds the base configuration shared across all environments. appsettings.{EnvironmentName}.json (like appsettings.Development.json) overrides specific values for that environment only, letting you use a local database connection string in development and a production one in production, without duplicating the entire configuration file.
It's a way of binding a section of configuration directly to a strongly-typed class, then injecting IOptions<T> (or IOptionsSnapshot<T> / IOptionsMonitor<T> for reloadable configuration) wherever that configuration is needed, instead of scattering raw string-keyed configuration lookups throughout the codebase.
Middleware are components chained together to handle HTTP requests and responses, each one deciding whether to pass the request to the next component in the chain or short-circuit the pipeline entirely. They're registered in order in Program.cs, and that order matters, for example authentication middleware needs to run before authorization checks it depends on.
app.Use() registers middleware that can call the next component in the pipeline, letting the request continue onward. app.Run() registers terminal middleware that doesn't call anything further, it's the end of that branch of the pipeline, typically used as the final handler.
UseRouting() matches the incoming request to an endpoint based on the registered routes, without yet executing it. The endpoint execution itself happens later in the pipeline, after middleware like authentication and authorization have had a chance to run, which is why routing and endpoint execution are deliberately separated into two pipeline stages.
Create a class with a constructor accepting a RequestDelegate (representing the next middleware in the pipeline) and an InvokeAsync (or Invoke) method containing your logic, calling await next(context) to pass control onward. Register it in the pipeline with app.UseMiddleware<YourMiddleware>().
UseAuthentication() identifies who the caller is, validating credentials or a token and populating the request's user identity. UseAuthorization() runs afterward and decides whether that now-identified user is allowed to access the specific resource being requested. Authentication must always be registered before authorization for this to work correctly.
Cross-Origin Resource Sharing controls which external domains are allowed to call your API from a browser. In .NET Core, you register a CORS policy in the service container (specifying allowed origins, methods, and headers) and then apply it via app.UseCors() in the middleware pipeline, before the endpoints that need it.
Create a class inheriting from ControllerBase, annotate it with [ApiController] and a base route like [Route("api/[controller]")], then define action methods decorated with [HttpGet], [HttpPost], etc. [ApiController] also enables automatic model validation and cleaner error responses without extra boilerplate.
Controller-based APIs use classes and attributes (the traditional MVC-style approach). Minimal APIs, introduced more recently, let you define endpoints directly with lambda expressions in Program.cs, with far less boilerplate for simple APIs, though controllers still tend to be preferred for larger APIs where the extra structure (filters, conventions, organization) pays off.
[FromRoute] binds a value from the URL path itself, like an {id} segment. [FromQuery] binds a value from the query string. [FromBody] deserializes the request body (typically JSON) into an object. ASP.NET Core can often infer these automatically, but being explicit avoids ambiguity, especially with complex types.
Return an IActionResult (or ActionResult<T>) and use a helper method like Ok(), NotFound(), BadRequest(), or CreatedAtAction(), which both sets the status code and, where relevant, the response body, rather than always returning a plain 200 with the raw object.
Data annotations like [Required], [StringLength], and [Range] on a model's properties are automatically validated when [ApiController] is applied, and if validation fails, ASP.NET Core automatically returns a 400 Bad Request with details, before your action method body even executes.
The most common approaches are URL-based versioning (/api/v1/products), a query string parameter, or a custom header, often implemented with the Microsoft.AspNetCore.Mvc.Versioning package, which handles routing requests to the correct controller version based on the chosen scheme.
EF Core is Microsoft's object-relational mapper (ORM) for .NET, letting you work with database data as C# objects instead of writing raw SQL directly. It handles translating LINQ queries into SQL, tracking changes to loaded entities, and generating the SQL needed to persist those changes back to the database.
IEnumerable executes queries in memory, meaning filtering happens after all the data has already been loaded from the database. IQueryable builds an expression tree and defers execution, letting EF Core translate the query into SQL and filter at the database level instead, which is far more efficient for large datasets.
Migrations are a way of evolving your database schema alongside your code, incrementally. Each migration captures the changes needed to bring the database up to date with the current model, and EF Core generates and applies the corresponding SQL, so schema changes are tracked in source control rather than applied manually.
DbContext represents a session with the database, coordinating queries and change tracking. DbSet<T> represents a specific table (or entity type) within that context, used to query and manipulate rows for that entity, like context.Products for a Products table.
Add() marks an entity as new, to be inserted. Attach() starts tracking an existing entity without marking any properties as modified, useful when you already know the entity exists and just need EF Core to track it. Update() marks an existing entity, and all its properties, as modified, so a full UPDATE statement is generated even if only one field actually changed.
By adding a navigation property, a collection on the 'one' side (like a list of Orders on a Customer) and a reference plus foreign key on the 'many' side (a Customer property and CustomerId on Order). EF Core's convention-based configuration usually infers the relationship automatically from these properties, though it can also be configured explicitly using the Fluent API.
Value types (int, bool, struct) hold their data directly and are copied by value when assigned or passed to a method. Reference types (class, string, array) hold a reference to data stored elsewhere on the heap, so assigning one variable to another copies the reference, not the underlying data, meaning both variables then point to the same object.
For value types, both typically compare actual values. For reference types, == compares references by default (unless overloaded), while .Equals() can be overridden by a class to define custom value-based equality. string is a notable exception: it overrides == to compare content rather than reference, which surprises people coming from languages where == always means identity.
Introduced in C# 8, it lets the compiler warn you at compile time when a reference type that could be null is used without a null check, catching a large class of NullReferenceException bugs before runtime instead of discovering them in production.
An interface defines a contract with no implementation (traditionally; C# 8+ allows default implementations), and a class can implement multiple interfaces. An abstract class can provide shared implementation alongside abstract members, but a class can only inherit from one abstract class, since C# doesn't support multiple inheritance of classes.
Extension methods let you add new methods to an existing type without modifying its source code or creating a subclass, defined as static methods in a static class with the first parameter prefixed by `this`. LINQ's methods (like .Where() and .Select() on IEnumerable) are themselves implemented as extension methods.
Mid-Level (3-6 years)
async and await let a method run asynchronously without blocking the calling thread, freeing it up to handle other work while waiting on I/O. A common mistake is calling .Result or .Wait() on a Task synchronously instead of awaiting it, which can cause deadlocks, especially in ASP.NET Core request contexts where the synchronization context can end up waiting on itself.
Task represents an asynchronous operation that doesn't return a value, similar to a void method but awaitable. Task<T> represents an asynchronous operation that returns a value of type T once it completes, retrieved by awaiting it.
It tells the awaited task not to try to resume on the original synchronization context, which can improve performance and avoid deadlocks in library code that doesn't need to get back onto a specific UI or request context. In ASP.NET Core specifically, there's generally no synchronization context to worry about, so it matters much less there than it does in older ASP.NET (Framework) or desktop applications.
The method continues executing without waiting for that task to complete, a 'fire and forget' pattern. If the un-awaited task throws an exception, that exception can go unobserved and silently swallowed, or in some cases crash the process, depending on the .NET version and configuration, which makes this a genuinely risky pattern unless it's deliberately intentional and the exception handling is designed around it.
Start all the tasks first without awaiting each individually, collect them, then await Task.WhenAll(tasks) to wait for all of them to complete together. Awaiting each one sequentially with individual await statements would run them one after another instead of concurrently, losing the performance benefit entirely.
Action filters run at a more granular point in the pipeline, specifically around the execution of a controller action, with access to action-specific context like model binding results and action arguments. Middleware runs earlier and more generally, around the entire request, without that action-specific context. Filters are the right tool when the cross-cutting logic genuinely needs to know about the specific action being invoked.
Authorization filters run first and specifically handle authentication/authorization checks, short-circuiting the request immediately if the caller isn't allowed to proceed. Resource filters run right after authorization and can short-circuit the rest of the pipeline for other reasons, like returning a cached response, before model binding and the more expensive parts of the pipeline even execute.
Implement IModelBinder with your custom binding logic, then apply it via [ModelBinder(typeof(YourBinder))] on the parameter, useful when the default binding conventions can't correctly map incoming data (like a comma-separated query string) to the shape you need in your action method.
It enables several automatic behaviors: automatic HTTP 400 responses on model validation failure, automatic inference of binding sources ([FromBody], [FromRoute], etc.) without needing to specify them explicitly in many cases, and requiring attribute routing rather than conventional routing.
Register it in the MVC options during service configuration, options.Filters.Add<YourFilter>(), rather than decorating every individual controller or action, which ensures new controllers automatically pick up the behavior without anyone needing to remember to add the attribute.
Use exception-handling middleware (app.UseExceptionHandler(...)) registered early in the pipeline, which catches unhandled exceptions from anything downstream and converts them into a consistent error response, rather than scattering try-catch blocks across every controller action.
UseExceptionHandler middleware catches exceptions from anywhere in the request pipeline, including middleware itself. An exception filter only catches exceptions thrown during MVC action execution, so it can't catch exceptions from middleware that runs before the MVC pipeline. Most production APIs use exception-handling middleware for consistent, pipeline-wide coverage.
ASP.NET Core has built-in support for the RFC 7807 ProblemDetails format, which you can return directly from exception-handling middleware or a custom exception handler, giving API consumers a consistent, machine-readable error shape (type, title, status, detail) instead of an ad-hoc error format that varies by endpoint.
No. Catching broadly and swallowing exceptions hides real bugs and makes debugging far harder later, since failures disappear silently instead of surfacing. Centralized exception-handling middleware combined with catching only the specific exceptions you can meaningfully recover from is the better pattern.
xUnit and NUnit are the two most common, both similar in capability; xUnit has become the more common default for new .NET Core projects. MSTest is Microsoft's own framework, less commonly chosen for new projects today but still found in older codebases.
Use WebApplicationFactory<TEntryPoint> to spin up an in-memory test server hosting the actual application (with its real middleware pipeline and routing), then send real HTTP requests against it using HttpClient, letting you test the full request pipeline without deploying to a real environment.
Use a mocking library like Moq to create a fake implementation of an interface your class depends on, configuring its expected behavior, then inject that mock through the constructor, isolating the class under test from real dependencies like a database or external API.
EF Core's in-memory provider is fast and requires no external setup, but it doesn't enforce real relational constraints or behave identically to a real database engine in every edge case, like certain LINQ translations. Using a real database (often via a lightweight local instance or a test container) gives higher-fidelity results at the cost of more setup and slower test runs, which matters for tests that specifically depend on database-engine-specific behavior.
It happens when fetching a list of entities triggers one query for the list itself, plus one additional query per entity to lazily load a related property, N+1 queries instead of one efficient query. It's fixed with eager loading via .Include() to fetch related data in the same query, or explicit loading when you only need the related data conditionally.
Eager loading (.Include()) fetches related data upfront, in the same query. Lazy loading fetches related data automatically the moment a navigation property is accessed, which can silently cause the N+1 problem if you're not careful. Explicit loading lets you manually trigger loading of related data on demand, via .Entry(entity).Collection(...).Load(), giving you control over exactly when the extra query happens.
Add a concurrency token property (often a rowversion/timestamp column) to the entity. EF Core includes that value in the WHERE clause of update statements, and if the row was modified by someone else in between, zero rows match, EF Core throws a DbUpdateConcurrencyException that you can catch and handle, typically by reloading and retrying or surfacing a conflict to the user.
By default, EF Core's change tracker keeps a snapshot of every loaded entity to detect changes for future SaveChanges calls. AsNoTracking() skips that overhead for read-only queries where you have no intention of updating the data, which noticeably improves performance for reporting or list endpoints.
Use FromSqlRaw() or FromSqlInterpolated() (which safely parameterizes interpolated values to prevent SQL injection) for queries returning entities, or context.Database.ExecuteSqlRaw() for non-query commands, reserved for cases where a query is too complex or performance-sensitive to express efficiently through LINQ.
Senior (6-8 years)
IMemoryCache stores data in the process's own memory, which is fast but not shared across multiple instances of an application, meaning each instance would need to build up its own cache independently. A distributed cache like Redis, accessed via IDistributedCache, is shared across all instances, essential once an application is scaled horizontally behind a load balancer.
IHostedService defines a long-running background task that starts when the application starts and stops when it shuts down, used for things like processing a background queue or running periodic maintenance jobs inside the same process as the web application, without needing a completely separate worker service.
BackgroundService is an abstract base class that implements IHostedService for you, providing a simpler ExecuteAsync method to override, which is why most background tasks in .NET Core inherit from BackgroundService rather than implementing IHostedService's StartAsync/StopAsync directly.
Explicitly remove or update the cache entry whenever the underlying data changes, rather than relying purely on a time-based expiry, since TTL-only invalidation risks serving stale data for however long the TTL window is. A short TTL as a safety net combined with explicit invalidation on writes is a common, pragmatic middle ground.
Response caching relies on standard HTTP caching headers and can be honored by the client, a proxy, or the server. Output caching (newer, ASP.NET Core 7+) caches the entire generated response server-side, giving more control over cache duration and invalidation independent of what a client or intermediate proxy chooses to respect.
ASP.NET Core has built-in rate-limiting middleware (since .NET 7) supporting algorithms like fixed window, sliding window, and token bucket, configured per-endpoint or globally. For a multi-instance deployment, the counters need to be backed by a shared store like Redis rather than in-memory, so limits are enforced consistently across instances.
IOptions<T> is resolved once and cached for the application's lifetime, ignoring later configuration changes. IOptionsSnapshot<T> is recomputed per request (Scoped), picking up configuration changes on the next request. IOptionsMonitor<T> can notify you of configuration changes immediately via a callback, useful for singleton services that need to react to configuration updates without waiting for a new request.
Never commit secrets to source control; use User Secrets locally for development, and a proper secrets manager (Azure Key Vault, AWS Secrets Manager) in staging and production, loaded into configuration at startup. Environment variables are also commonly used as an override layer for containerized deployments.
By default, later-added providers override earlier ones: appsettings.json, then appsettings.{Environment}.json, then User Secrets (in development), then environment variables, then command-line arguments. This matters because it lets you set a sensible base configuration in files while allowing environment variables or command-line arguments to override specific values at deployment time without touching the files themselves.
Use the Options pattern's built-in validation support, ValidateDataAnnotations() or a custom IValidateOptions<T> implementation, combined with ValidateOnStart(), which causes the application to fail immediately at startup if required configuration is missing or invalid, rather than failing unpredictably later when that configuration is actually used.
Reading raw values via configuration["Some:Key"] scatters magic strings throughout the codebase and only fails at runtime if a key is missing or misspelled. Binding to a strongly-typed class via the Options pattern catches structural mismatches earlier, is easier to unit test, and gives IntelliSense support, which is why most production codebases standardize on it rather than raw lookups.
Lead (8-10 years)
Dependency Injection (the built-in IoC container), Middleware itself is essentially the Chain of Responsibility pattern, the Options pattern resembles a Builder for configuration objects, and Factory patterns show up in how the framework creates controllers and services. Recognizing these helps you work with the framework's grain instead of fighting it.
Register the base implementation and the decorator separately, then resolve the decorator by injecting the base implementation into its constructor, wiring it up manually in the service registration (or using a library like Scrutor, which adds decoration support on top of the built-in container). This lets you add cross-cutting behavior, like logging or caching, around an existing service without modifying its code.
Define an interface for the varying behavior, register multiple implementations in the DI container, then either inject IEnumerable<IStrategy> and pick the right one at runtime based on some criteria, or use a factory that resolves the correct implementation by a key, avoiding a large if-else or switch chain scattered through business logic.
Keyed services (introduced in .NET 8) let you register multiple implementations of the same interface under different string or enum keys, and resolve the specific one you need by key at the injection site, rather than resolving an entire collection and filtering it yourself in application code.
Middleware itself is typically constructed once (as effectively a singleton) for the lifetime of the application, so injecting a Scoped service directly into its constructor causes the same captive-dependency problem as with a Singleton service. Instead, you accept the Scoped dependency as a parameter on the InvokeAsync method itself, which is resolved fresh from the current request's scope rather than the middleware's own constructor.
Common approaches include organizing by feature (vertical slices) rather than by technical layer (all controllers together, all services together), which keeps related code physically close together and makes it easier to reason about and change one feature without touching unrelated ones. For very large systems, splitting into separate class libraries along clear boundaries (domain logic, infrastructure, API) also helps enforce dependency direction and prevent circular references.
ASP.NET Core Identity is a full membership system, handling user registration, password hashing, lockout policies, two-factor authentication, and external login providers out of the box. Building this yourself means re-implementing a lot of well-understood, security-sensitive logic, which is generally a bad trade-off unless you have a very specific reason Identity's model doesn't fit.
Configure JWT bearer authentication in the service container, specifying the issuer, audience, and signing key used to validate incoming tokens, then apply [Authorize] to the endpoints that require authentication. On each request, the authentication middleware validates the token's signature and claims before the request reaches the controller, keeping the API itself stateless.
Authentication verifies who the caller is, typically via a JWT bearer token or cookie. Authorization decides what that authenticated caller is allowed to do, implemented through role-based checks ([Authorize(Roles = "Admin")]) or more flexible policy-based authorization, which can express arbitrary requirements beyond simple roles.
Define a custom IAuthorizationRequirement and a corresponding AuthorizationHandler that contains the actual logic for evaluating whether the requirement is met, then register it as a named policy and apply it with [Authorize(Policy = "YourPolicy")]. This is the right approach once authorization logic goes beyond a simple role check, like requiring a user to own the specific resource they're trying to modify.
SQL injection is largely prevented by EF Core's parameterized queries as long as you avoid string-concatenating raw SQL. Mass assignment (a client sending extra fields that get bound onto properties they shouldn't be able to set, like an IsAdmin flag) is prevented by using dedicated DTOs for input binding instead of binding directly to your domain entities, so only explicitly intended fields can ever be set from a request.
Issue a short-lived JWT access token plus a longer-lived, securely stored refresh token at login. When the access token expires, the client sends the refresh token to a dedicated endpoint to obtain a new access token, without requiring the user to log in again. The refresh token itself needs to be stored securely (often in an HttpOnly cookie) and should be revocable server-side in case of compromise.
Symmetric signing (HMAC) uses the same secret key to both sign and validate tokens, simpler but requiring every service that validates tokens to hold that shared secret. Asymmetric signing (RSA/ECDSA) uses a private key to sign and a public key to validate, letting you distribute the public key widely (to multiple services or third parties) without ever exposing the private signing key, which is the safer choice once more than one service needs to validate tokens.
Staff (10+ years)
.NET Core provides the building blocks for an individual service: fast startup, small footprint, built-in DI, and good container support. What it doesn't provide out of the box is the distributed-systems tooling, service discovery, distributed tracing, centralized configuration, which typically comes from a service mesh, an orchestrator like Kubernetes, or libraries like .NET Aspire for local development orchestration.
Synchronous REST or gRPC calls (via HttpClientFactory or a generated gRPC client) are simple to reason about but couple the caller's availability to the callee's uptime and latency. Asynchronous, event-driven communication through a message broker (Azure Service Bus, RabbitMQ, Kafka) decouples services and improves resilience, at the cost of eventual consistency and a harder debugging story across a chain of asynchronous events.
Polly is the standard resilience library in .NET, providing retry policies, circuit breakers, and timeouts that can be composed together and applied to HttpClient calls via HttpClientFactory. A circuit breaker specifically stops calling a failing downstream service after a threshold of failures, failing fast instead of letting failures cascade and exhaust resources.
A retry policy re-attempts a failed call, handling brief, transient failures like a momentary network blip. A circuit breaker stops attempting calls entirely once failures cross a threshold, protecting both the caller and an already-struggling downstream service from further load. Combined, a bounded retry absorbs short transient failures while the circuit breaker prevents that same retry logic from hammering a service that's genuinely down.
Centralize configuration in a shared source, Azure App Configuration or a similar centralized service, rather than each service maintaining its own scattered configuration files, so changes can be rolled out and audited consistently. Secrets specifically should flow through a dedicated secrets manager (Azure Key Vault) rather than living in application configuration at all.
I'd weigh it by ownership and deployment independence rather than defaulting to microservices as inherently better: if a separate team owns it, it needs to scale or deploy independently, or it has meaningfully different reliability requirements, that argues for a separate service. Otherwise, a well-modularized module within an existing application is usually faster to build and cheaper to operate, and splitting services prematurely mostly adds network overhead without a matching benefit.
Run it incrementally: get shared logic extracted into .NET Standard (or multi-targeted) libraries that both the old and new applications can reference, migrate one module or one service boundary at a time, and lean on automated tests to catch regressions early rather than a long-lived migration branch that drifts from main. A full stop-the-world rewrite is rarely something the business will tolerate.
I look at whether it addresses the actual problem or just its symptom, what happens under failure rather than only the happy path, and whether it's consistent with patterns already established elsewhere in the system, since an inconsistent one-off pattern becomes a maintenance burden the whole team inherits. I'd rather ask a few pointed questions that surface the team's own blind spots than hand them a prescribed solution.
Automate what can be automated: static analysis (Roslyn analyzers), consistent formatting (.editorconfig enforced in CI), and dependency version alignment, so standards aren't a matter of opinion in code review. For architectural conventions that resist automation, I'd document a small number of high-leverage decisions with the reasoning behind them, rather than a long style guide nobody actually reads.
Weigh it against concrete benefit, performance improvements, security support lifetime (since older .NET versions eventually go out of support), features teams are actively blocked without, against the real migration cost and risk to velocity while it's underway. I'd pilot the upgrade on a lower-risk service first rather than the most critical one, with a rollback plan defined before starting.
First rule out environmental differences, thread pool starvation, connection pool sizing, and whether staging traffic actually resembles production concurrency. Then I'd want Application Insights (or equivalent) telemetry and distributed tracing to see where time is actually going, since 'intermittent under load' is very often thread pool exhaustion from blocking async calls, GC pauses under memory pressure, or connection pool contention, none of which show up reliably in low-traffic staging.
ASP.NET Core's built-in health checks middleware as the baseline, extended with custom checks for what actually matters, downstream dependency availability, database connectivity, queue depth for background processing, not just 'is the process running.' I'd alert on leading indicators and rate-of-change, a growing queue or rising error rate, rather than only static thresholds, since those catch problems before they become full outages.
Treat the public API as a contract: additive changes are generally safe, but changing or removing existing public method signatures needs a documented deprecation period (using [Obsolete] with a clear message) before actual removal, not a silent breaking change in a minor version bump. I'd also want visibility into who's actually consuming the old API before removing it.
Mitigation before root-causing: roll back a recent deploy, fail over, or shed load if that stops the bleeding, even before fully understanding why it broke. I'd also make sure one person is clearly driving the incident and communicating status, since incidents usually drag on longer because of diffuse ownership, not a lack of technical skill in the room.
Start from actual load testing at realistic traffic shapes rather than linear extrapolation, since the real bottleneck (a database, a downstream API's rate limit, a thread pool ceiling) rarely scales linearly and often shows up well before the target load. I'd identify the actual constraint first, then decide whether the fix is more instances, caching, offloading work to a background queue, or in some cases redesigning the hot path entirely.
This is a judgment question interviewers use to see how you reason under uncertainty, not to test a specific fact. A strong answer names the actual constraint that forced the decision, the realistic options that were on the table, why you picked one knowing it wasn't guaranteed to be right, and what you'd do differently with what you know now, rather than presenting the decision as obviously correct in hindsight.
I'd pair on a real piece of their code and walk through how a future reader, including them in six months, would have to reconstruct their reasoning, rather than giving generic 'write cleaner code' feedback. Concrete, example-driven feedback tied to their own code sticks far better than abstract principles about layering.
Push for a shared structured-logging format and a consistent correlation-ID convention early, propagated through headers across service calls, since retrofitting that after services have already diverged is far more painful than establishing it as a starting template. A well-documented shared library that new services are built from beats a style guide people have to remember to follow manually.




