Full Stack Interview Questions for Senior Developers — 55 Deep Answers Across Frontend, Backend, APIs & Databases (with Real Code)
55 senior full stack interview questions with deep answers and real code — frontend, backend, APIs, databases, auth, and delivery, end to end.
- Author
- Randhir Jassal
- Published
- Reading time
- 30 min read
- Views
- 8 views
Junior full-stack interviews ask "what's the difference between GET and POST." Senior full-stack interviews ask how a single click travels from the browser through your CDN, API, cache, and database and back, where you validate the same rule on the client and the server and why both, and what breaks when the network drops mid-request. A senior full-stack engineer isn't someone who knows a little of everything — it's someone who understands the seams: where the client ends and the server begins, where trust boundaries live, and where latency and failure hide. This guide is 55 questions across client-side, server-side, the client-server seam, data & persistence, auth & security, and delivery & architecture — with real code on both sides.
Each answer is a short answer you could say in one breath, a deep dive into the mechanism (with code), and the senior signal the interviewer is listening for. Every example is from Mattrx — a real multi-tenant marketing-analytics platform: a ReactJS frontend (TanStack Query), a .NET 9 / ASP.NET Core API (Clean Architecture + CQRS), Azure SQL, Redis, Kafka, and SignalR for real-time. No Foo/Bar.
How to use this guide
Full-stack seniority is about the whole path, so read across the blocks — the client questions assume you know what the server does, and vice versa. If you can explain how a value flows from a database column all the way to a pixel (and back), and where it's validated, cached, and secured on the way, you're ready.
Block A — client-side (frontend)
1. How do you structure a large frontend app?
Short answer: By feature, not by file type. Group everything a feature needs (components, hooks, API calls, types) together; keep shared/ui/lib folders for cross-cutting pieces. Feature-based structure scales because most changes stay in one folder.
Deep dive: The components/, containers/, reducers/ split by type forces every feature change to touch five folders. Mattrx groups by domain (campaigns/, reports/, billing/) with a shared ui/ and lib/.
Senior signal: You organize by feature/domain, keep cross-cutting code in shared modules, and treat "one change touches many folders" as a structure smell.
2. Client state vs server state — how do you manage each?
Short answer: They're different problems. Server state (data from the API) is remote, shared, and cached — use a data-fetching library (TanStack Query, RTK Query) that handles caching, refetching, and invalidation. Client state (UI toggles, form drafts) is local — use component state/context/signals. Don't dump server data into a global store.
Deep dive:
// Server state: the library owns caching, dedup, background refetch, invalidation.
const { data: campaigns, isLoading } = useQuery({
queryKey: ['campaigns', tenantId],
queryFn: () => api.getCampaigns(tenantId),
staleTime: 60_000, // treat as fresh for 60s -> fewer refetches
});
Senior signal: You separate server state (cache with a query library) from client state (local/UI), and don't hand-roll server caching in Redux.
3. How do you do caching, invalidation, and optimistic updates on the client?
Short answer: The query library caches by key; after a mutation you invalidate the affected keys so they refetch, or you optimistically update the cache immediately and roll back on error. Cache keys are the contract — get them right and invalidation is trivial.
Deep dive:
const archive = useMutation({
mutationFn: (id: string) => api.archiveCampaign(id),
onMutate: async (id) => { // optimistic: update UI before server responds
await qc.cancelQueries({ queryKey: ['campaigns'] });
const prev = qc.getQueryData(['campaigns']);
qc.setQueryData(['campaigns'], (c) => c?.filter((x) => x.id !== id));
return { prev }; // snapshot for rollback
},
onError: (_e, _id, ctx) => qc.setQueryData(['campaigns'], ctx?.prev), // rollback
onSettled: () => qc.invalidateQueries({ queryKey: ['campaigns'] }), // reconcile with server
});
Senior signal: You use key-based invalidation, snapshot-and-rollback for optimistic updates, and always reconcile with the server on settle.
4. CSR vs SSR vs SSG vs ISR — when each?
Short answer: CSR (client render) for app-like, behind-login screens. SSR (server render per request) for personalized, SEO-critical, always-fresh pages. SSG (build-time) for static content. ISR (static + periodic revalidation) for mostly-static content that changes occasionally — SSG's speed with freshness. Match the strategy to freshness + SEO needs.
Deep dive: Mattrx's marketing/blog pages are ISR (fast + revalidate on a schedule); the logged-in dashboard is CSR (no SEO, highly interactive).
Senior signal: You choose per page by freshness + SEO + interactivity, and know ISR gives static speed with controlled staleness.
5. How do you avoid prop drilling without over-using context?
Short answer: Prefer composition (pass components as children/props) first; use Context for genuinely global, low-frequency data (theme, current user); reach for a store only for complex shared state. Context re-renders all consumers on change, so don't put fast-changing values in it.
Deep dive: Splitting one big context into focused contexts (or using selectors) avoids re-rendering the whole tree on any change.
Senior signal: You reach for composition first, use context for stable global values, and know context's re-render cost makes it wrong for hot state.
6. How do you make a React app fast?
Short answer: Ship less JS (code-split with lazy loading), render less (memoize expensive subtrees, virtualize long lists), and fetch smart (cache + dedup). Measure with the profiler and Core Web Vitals before optimizing.
Deep dive:
// Code-split a heavy route/component so its JS loads only when needed.
const ReportBuilder = lazy(() => import('./ReportBuilder'));
// Virtualize a 10k-row table so only visible rows mount.
Senior signal: You optimize bundle size + render count + fetch strategy, and measure (profiler/CWV) before reaching for memo everywhere.
7. How do you handle forms and validation on the client?
Short answer: Use a form library (React Hook Form) with a schema (Zod) for client-side validation and instant feedback — but never trust it; the server re-validates the same rules. Share the schema between client and server when the stack allows.
Deep dive: Client validation is UX; server validation is security. Mattrx shares Zod schemas where the API is TypeScript and mirrors them in FluentValidation on the .NET side.
Senior signal: You treat client validation as UX-only, always re-validate on the server, and share/mirror the rules to avoid drift.
8. How do you handle loading, error, and empty states?
Short answer: Every async view has four states — loading, error, empty, and data — and a senior handles all four explicitly (skeletons, retry affordances, helpful empty states), not just the happy path. Suspense/error boundaries formalize this.
Deep dive: The most common bug is a component that assumes data exists; the empty and error states are where UX quality shows.
Senior signal: You design the loading/error/empty states as first-class, not afterthoughts, and use boundaries to contain failures.
9. What accessibility and responsiveness must a full-stack dev know?
Short answer: Semantic HTML (real buttons/labels/headings), keyboard navigation, sufficient contrast, aria-* only when semantics fall short, and a mobile-first responsive layout (flex/grid, relative units). Accessibility is largely free if you use the right elements.
Deep dive: A <div onClick> isn't keyboard-accessible; a <button> is. Mattrx builds on semantic elements and tests with keyboard-only navigation.
Senior signal: You reach for semantic HTML first, know the keyboard/contrast basics, and treat responsive as mobile-first.
10. Where do you store auth tokens on the client, and why?
Short answer: Prefer an httpOnly, Secure, SameSite cookie — JavaScript can't read it, so an XSS can't steal the token. localStorage is readable by any script, so a single XSS leaks the token. The trade-off: cookies need CSRF protection (SameSite + anti-forgery), which is manageable.
Deep dive: Mattrx uses httpOnly cookies for the session token with SameSite=Lax and anti-forgery on state-changing requests.
Senior signal: You choose httpOnly cookies over localStorage for tokens (XSS resistance) and know that shifts the risk to CSRF, which you then mitigate.
Block B — server-side (API & backend)
11. How do you design a clean REST API?
Short answer: Model resources (nouns), use HTTP verbs for actions (GET/POST/PUT/PATCH/DELETE), return meaningful status codes, and keep it stateless. POST /campaigns, GET /campaigns/{id}, PATCH /campaigns/{id} — not POST /createCampaign.
Deep dive:
GET /campaigns?status=active -> 200 list
POST /campaigns -> 201 + Location header
PATCH /campaigns/{id} -> 200 updated (400 invalid, 404 missing, 409 conflict)
DELETE /campaigns/{id} -> 204 no content
Senior signal: You design resource-oriented endpoints with correct verbs/status codes and keep the API stateless.
12. REST vs GraphQL vs gRPC — when each?
Short answer: REST for standard CRUD/public APIs (simple, cacheable). GraphQL when clients need flexible, aggregated queries and you want to avoid over/under-fetching (many screens, varied data needs). gRPC for fast, typed, internal service-to-service calls (binary, streaming). Match to the consumer.
Deep dive: Mattrx exposes REST publicly, uses gRPC between internal services, and considered GraphQL for the dashboard's varied data needs.
Senior signal: You pick by consumer + shape (public CRUD vs flexible client queries vs internal RPC), not by fashion.
13. How do you version an API?
Short answer: Version when you must make a breaking change — via URL (/v2/), header, or media type. Prefer additive, non-breaking changes (new optional fields) so you rarely need a new version. Support the old version through a deprecation window.
Deep dive: Every field removal/rename/semantic change is breaking; adding optional fields isn't. Mattrx adds fields freely and versions only on true breaks.
Senior signal: You default to backward-compatible changes and version only for real breaks, with a deprecation plan.
14. How does the server validate input (and why never trust the client)?
Short answer: The server re-validates every input at the boundary because the client can be bypassed (curl, a tampered request). Use model binding + a validation layer (DataAnnotations/FluentValidation), reject invalid input with 400 + details, and never assume the UI enforced anything.
Deep dive:
public sealed class CreateCampaignValidator : AbstractValidator<CreateCampaign>
{
public CreateCampaignValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(120);
RuleFor(x => x.Budget).GreaterThanOrEqualTo(0);
}
}
Senior signal: You treat the client as untrusted, validate at the API boundary, and return a consistent 400 contract.
15. Offset vs cursor pagination — which and why?
Short answer: Offset (?page=3) is simple but slow and inconsistent on deep pages (the DB scans skipped rows; inserts shift pages). Cursor/keyset (?after={lastId}) is stable and fast at any depth. Use cursor for large, frequently-changing datasets.
Deep dive:
// Keyset: WHERE id > @cursor ORDER BY id LIMIT 21 (over-fetch 1 to know "has more").
var page = await _db.Campaigns
.Where(c => c.TenantId == tenantId && c.Id > cursor)
.OrderBy(c => c.Id).Take(pageSize + 1).ToListAsync(ct);
Senior signal: You default to cursor pagination for big/live data and can explain offset's deep-page cost and drift.
16. How do you handle errors consistently across an API?
Short answer: One error contract for the whole API — ASP.NET Core's ProblemDetails (RFC 7807): a typed, structured JSON error with status, title, detail, and a trace id. Central exception handling maps exceptions to the right status + ProblemDetails; never leak stack traces.
Deep dive: Mattrx maps domain failures to 4xx and unexpected ones to 500 + a trace id the client can quote in a support request.
Senior signal: You return a single structured error shape (ProblemDetails) with a correlation id, handled centrally, never raw stack traces.
17. DTOs vs domain entities — why not return entities directly?
Short answer: Returning entities leaks your database shape, over-shares fields, risks serialization cycles, and couples the API to the schema. DTOs are the API contract — a deliberate, versioned projection of what the client needs, decoupled from persistence.
Deep dive: Mattrx projects entities to DTOs (often directly in the query), so a column rename doesn't change the API.
Senior signal: You never serialize entities to clients, and treat DTOs as the stable, intentional API contract.
18. How do you layer a backend, and where does logic go?
Short answer: Keep controllers/endpoints thin (HTTP concerns only), put business logic in a service/application layer, and data access in a repository/persistence layer. Dependencies point inward (Clean Architecture). Logic in controllers is the classic smell.
Deep dive: Mattrx uses CQRS handlers (MediatR) as the application layer; controllers just send a request and map the result.
Senior signal: You keep HTTP out of business logic, layer with inward dependencies, and put rules in the application/domain layer.
19. Why "async all the way," and what breaks it?
Short answer: Async I/O frees the thread during waits (DB, HTTP), so the server handles far more concurrent requests with fewer threads. Blocking on async (.Result/.Wait()) defeats it and can deadlock or exhaust the thread pool. Be async end-to-end.
Deep dive: One .Result in a hot path can starve the thread pool under load. Mattrx is async from controller to ToListAsync.
Senior signal: You go async end-to-end for scalability and know sync-over-async causes deadlocks/pool exhaustion.
20. How do you rate-limit and throttle an API?
Short answer: Limit requests per client/tenant/IP to protect the service and ensure fairness — token-bucket or fixed/sliding window, ideally in a shared store (Redis) so it works across instances. Return 429 with a Retry-After.
Deep dive: Mattrx rate-limits per tenant in Redis (via a gateway/middleware) so one tenant can't starve others.
Senior signal: You rate-limit per tenant/client in a shared store, return 429 + Retry-After, and place it at the edge/gateway.
21. How do you cache on the server?
Short answer: Cache-aside with Redis for hot, expensive reads: check cache, on miss load from DB and populate, set a TTL, and invalidate on write. Plus HTTP response caching/CDN for cacheable GETs. Cache the expensive and reused, not everything.
Deep dive:
var kpis = await _cache.GetOrSetAsync($"kpis:{tenantId}",
() => _repo.GetKpisAsync(tenantId, ct), TimeSpan.FromSeconds(60));
Senior signal: You use cache-aside + TTL + invalidation for hot reads, know the stale-data trade-off, and layer CDN/HTTP caching on top.
22. How do you handle slow work in a request?
Short answer: Don't do it in the request. Offload to a background job/queue (Service Bus/Kafka + a worker): the API enqueues and returns fast (202 Accepted + a status URL); a worker does the heavy lifting. Report generation, emails, exports.
Deep dive:
// API stays snappy: enqueue and return; a worker renders the report later.
await _bus.SendAsync(new GenerateReport(tenantId, spec), ct);
return Accepted($"/reports/{jobId}");
Senior signal: You move slow/unreliable work to a queue + worker, return 202 with a status resource, and keep the request path fast.
Block C — the client-server seam
23. Walk a request end-to-end: browser to database and back.
Short answer: Browser -> DNS -> CDN/edge (static or cache hit) -> load balancer -> API instance -> middleware (auth, rate-limit) -> handler -> cache/DB -> response serialized -> back through the LB/CDN -> browser renders. A senior can name where latency, caching, and failure live at each hop.
Deep dive:
Browser -> CDN -> Load Balancer -> API (auth -> handler) -> Redis? -> Azure SQL
<- JSON <- (serialize) <----------------------------------------+
Senior signal: You can trace the full path and point to where you'd cache, where TLS terminates, and where a hop can fail.
24. How do you keep the client and server API contract in sync?
Short answer: Generate the contract from a single source — OpenAPI/Swagger on the server, then generate a typed client for the frontend. Contract drift (the client assumes a shape the server changed) is caught at build time, not in production.
Deep dive: Mattrx generates a typed TS client from the API's OpenAPI spec, so a breaking server change fails the frontend build.
Senior signal: You drive a typed client from OpenAPI (or shared types) so contract drift is a compile error, not a runtime bug.
25. What is CORS and how do you configure it correctly?
Short answer: CORS is the browser's rule that a page on origin A can't call API origin B unless B opts in with Access-Control-Allow-Origin headers. Configure the API to allow your specific frontend origin(s) — never * with credentials. Preflight OPTIONS handles non-simple requests.
Deep dive: A wildcard origin with credentials is both insecure and disallowed by browsers. Mattrx allowlists its exact frontend origins.
Senior signal: You explain CORS as a browser same-origin protection, allowlist specific origins (not * with credentials), and know about preflight.
26. What serialization gotchas bite full-stack teams?
Short answer: Casing (camelCase JS vs PascalCase C# — configure the serializer), dates/timezones (use ISO-8601 UTC, not local), nulls vs missing fields, number precision (large ints/decimals), and enums (string vs int). Agree on conventions and enforce them in the serializer.
Deep dive: Mattrx serializes camelCase, UTC ISO dates, and string enums, configured once in the API so it's consistent everywhere.
Senior signal: You standardize casing/dates/enums in the serializer config and know timezone/precision bugs come from sloppy serialization.
27. How do you make a mutation retry-safe (idempotent)?
Short answer: The network can drop after the server processed a request but before the client got the response — so the client retries and double-charges/double-creates. Fix with an idempotency key: the client sends a unique key; the server records it and returns the same result for a repeat instead of re-doing the work.
Deep dive:
// First time: process + store result under the key. Repeat: return the stored result.
if (await _idem.TryGetAsync(key) is {} prior) return prior;
var result = await Process(cmd, ct);
await _idem.StoreAsync(key, result);
Senior signal: You add idempotency keys to non-idempotent mutations (payments, creates) so a retry after an ambiguous failure is safe.
28. How do optimistic UI and server reconciliation work together?
Short answer: The client updates the UI immediately (optimistic), sends the mutation, and reconciles when the server responds — commit on success, roll back on error, and refetch to sync. It makes the app feel instant while the server stays the source of truth.
Deep dive: See Q3's code — snapshot, apply, roll back on error, invalidate on settle. The server always wins the final state.
Senior signal: You make the UI feel instant but treat the server as authoritative, with rollback + reconciliation on failure.
29. How do you handle file uploads end-to-end?
Short answer: For anything non-trivial, don't stream files through your API. Have the API issue a presigned URL, let the client upload directly to blob storage (S3/Azure Blob), then notify the API to record it. This keeps the API light and scales uploads.
Deep dive:
// API returns a short-lived presigned URL; the browser PUTs the file straight to Blob storage.
var url = _blob.GetPresignedUploadUrl(container, blobName, TimeSpan.FromMinutes(10));
return Ok(new { uploadUrl = url });
Senior signal: You use presigned direct-to-storage uploads for large files and keep byte-streaming out of the API.
30. How do you handle partial failures and network errors?
Short answer: Assume the network fails. On the client: timeouts, retries with backoff for idempotent calls, and clear error UX. On the server: idempotency, transactions, and graceful degradation (serve stale/cached, disable a feature) rather than a hard failure.
Deep dive: Mattrx serves cached KPIs if the live query times out, so a slow DB degrades the dashboard instead of breaking it.
Senior signal: You design for failure at both ends — retries/backoff/timeouts on the client, idempotency/degradation on the server.
31. How do you push real-time updates to the client?
Short answer: WebSockets (via SignalR) for bidirectional, low-latency updates (live dashboards, chat, presence); SSE for simple server-to-client streams; polling as a fallback. Choose by direction, frequency, and infra support.
Deep dive:
// Server pushes a KPI update to everyone in the tenant's SignalR group.
await _hub.Clients.Group(tenantId).SendAsync("kpiUpdated", payload, ct);
Senior signal: You pick WebSockets/SignalR for bidirectional real-time, SSE for one-way streams, and know the scaling concern (a backplane like Redis across instances).
Block D — data & persistence
32. SQL vs NoSQL — how do you choose?
Short answer: SQL for structured, relational data needing transactions/joins/strong consistency (most business apps). NoSQL for flexible schemas, massive scale, or specific access patterns (document, key-value, wide-column). Choose by data shape and access pattern, not hype — and it's fine to use both.
Deep dive: Mattrx is SQL (Azure SQL) for relational campaign data + Redis (key-value) for caching + a time-series approach for high-volume events.
Senior signal: You choose by consistency needs + data shape + access pattern, and use polyglot persistence where it fits.
33. How do you manage schema migrations across a team?
Short answer: Version migrations in source control (EF Core migrations / a migration tool), apply them as a reviewed, gated deploy step (not on app startup), and make changes backward-compatible (expand/contract) so old and new code run together during a rolling deploy.
Deep dive: Add a nullable column -> backfill/dual-write -> switch reads -> drop the old, across separate deploys. Mattrx ships idempotent migration scripts from CI.
Senior signal: You version migrations, apply them in a gated step, and use expand/contract for zero-downtime schema changes.
34. Where does N+1 come from full-stack, and how do you fix it?
Short answer: From loading a list then querying per item (lazy loading, or a per-row fetch) — 1 + N round trips. Fix by loading related data in one query (join/include) or projecting exactly what the screen needs server-side, so one API call maps to one efficient query.
Deep dive:
// One projected query instead of N per-campaign lookups.
var list = await _db.Campaigns
.Select(c => new CampaignCard(c.Id, c.Name, c.Events.Count()))
.ToListAsync(ct);
Senior signal: You collapse per-item queries into one projected query and verify with the generated SQL / query count.
35. How do you keep a request's writes consistent?
Short answer: Wrap multi-step writes in a transaction so they commit atomically (all or nothing). Across services (no shared transaction), use the outbox pattern + eventual consistency instead of a distributed transaction. Keep the consistency boundary explicit.
Deep dive: Mattrx writes the state change and an outbox event in one DB transaction; a relay publishes the event so downstream stays consistent.
Senior signal: You use a transaction within one DB and outbox/eventual consistency across services, and know 2PC doesn't scale.
36. What are the caching layers, and how do you invalidate them?
Short answer: Browser cache -> CDN -> API/response cache -> distributed cache (Redis) -> DB. Each trades freshness for speed. Invalidation is the hard part: use TTLs, cache keys tied to data, and explicit purges on write. "There are only two hard things..." — cache invalidation is one.
Deep dive: Mattrx uses short TTLs + tag-based invalidation so a write clears exactly the affected cached reads.
Senior signal: You can name the cache layers, pick TTL vs explicit invalidation per layer, and treat invalidation as the real cost.
37. Why does connection pooling matter?
Short answer: Opening a DB connection is expensive; a pool reuses a fixed set across requests. Without pooling (or with a leaked/oversized pool) you exhaust DB connections under load and requests queue or fail. Use the pooler, keep connections short-lived, and don't hold one across an await you don't need.
Deep dive: Mattrx uses the connection pooler and keeps DbContext scoped per request so connections return to the pool quickly.
Senior signal: You rely on pooling, keep connections short-lived, and know connection exhaustion is a classic under-load failure.
38. What indexing basics must a full-stack dev know?
Short answer: Index the columns you filter/join/sort on; a query without a supporting index scans the whole table. Composite index order matters (most-selective/leading predicate first). Too many indexes slow writes. Read the execution plan to confirm a seek, not a scan.
Deep dive: Mattrx indexes (TenantId, CreatedAt) for the common tenant-scoped, time-ordered queries.
Senior signal: You index by query pattern, understand composite key order, and verify with the plan (seek vs scan).
39. How do you handle large datasets end-to-end?
Short answer: Never load it all. Paginate (cursor) for lists, stream for exports (constant memory), aggregate on the DB (GROUP BY / pre-computed rollups) rather than in the app, and cache hot aggregates. The UI shows a page/summary, not a million rows.
Deep dive: Mattrx serves dashboard numbers from pre-aggregated rollups and streams big CSV exports row-by-row.
Senior signal: You paginate/stream/aggregate-on-DB for scale and never pull a huge result set into app memory.
40. What does "validate at every layer" mean?
Short answer: Defense in depth: client validation (UX), API validation (security boundary), domain invariants (business rules), and DB constraints (last line — NOT NULL, unique, FK). Each layer assumes the others might be bypassed. The DB constraint catches what every layer above missed.
Deep dive: A unique index enforces "one active subscription per tenant" even if a race slips past the app check.
Senior signal: You validate at all layers and rely on DB constraints as the ultimate guarantee against races/bugs.
Block E — auth, security & cross-cutting
41. Sessions vs JWT — how do you authenticate?
Short answer: Server sessions (a session id in a cookie, state on the server/Redis) are easy to revoke but need shared session storage. JWT (self-contained, signed token) is stateless and scales horizontally but is hard to revoke before expiry. Use short-lived JWTs + refresh tokens, or sessions when instant revocation matters.
Deep dive: Mattrx uses short-lived access tokens + refresh tokens, storing them in httpOnly cookies (Q10).
Senior signal: You weigh stateless-scale (JWT) vs easy-revocation (sessions), and mitigate JWT's revocation gap with short expiry + refresh.
42. How do you do authorization (vs authentication)?
Short answer: Authentication = who you are; authorization = what you're allowed to do. Enforce authz on the server (never the client) via roles/claims (RBAC) and resource-based checks (does this user own this record?). Reaching an endpoint isn't permission to touch a specific row.
Deep dive: Mattrx checks both the role/scope and tenant ownership on every request — a valid token for tenant A can't read tenant B's data.
Senior signal: You enforce authz server-side, combine role/claim checks with resource ownership, and never trust client-side gating.
43. Explain OAuth2 / OIDC at a high level.
Short answer: OAuth2 delegates authorization (let app X access resource Y on your behalf) without sharing passwords; OIDC adds an identity layer (who you are) on top. The authorization-code flow with PKCE is the standard for web/mobile apps — redirect to the provider, get a code, exchange it (with PKCE) for tokens.
Deep dive: PKCE prevents code interception; the app never sees the user's password — only tokens from the identity provider.
Senior signal: You distinguish OAuth2 (authz delegation) from OIDC (identity), and know auth-code + PKCE is the modern default.
44. How do you isolate tenant data in a multi-tenant app?
Short answer: Every query is scoped by TenantId, enforced centrally so a developer can't forget it — a global query filter (EF Core HasQueryFilter), row-level security, or a per-tenant schema/DB for stronger isolation. The tenant comes from the authenticated token, never a request parameter.
Deep dive:
// Central defense: every Campaign query is auto-scoped to the caller's tenant.
modelBuilder.Entity<Campaign>().HasQueryFilter(c => c.TenantId == _tenant.Id);
Senior signal: You enforce tenant scoping centrally (not per-query), derive the tenant from the token, and match isolation strength to the security requirement.
45. What are the baseline security controls for a web app?
Short answer: HTTPS everywhere (HSTS), secrets in a vault (never in code/env-committed), security headers (CSP, X-Content-Type-Options, X-Frame-Options), input validation + output encoding, least-privilege DB accounts, and dependency scanning. The basics stop most attacks.
Deep dive: Mattrx sets a strict CSP, keeps secrets in Key Vault, and runs dependency + secret scanning in CI.
Senior signal: You can list the baseline (HTTPS/HSTS, vault, CSP + headers, least privilege, dep scanning) and treat them as non-negotiable.
46. Which OWASP vulnerabilities do you actively design against?
Short answer: Injection (parameterized queries), XSS (output encoding + CSP), CSRF (SameSite + anti-forgery tokens), broken access control / IDOR (resource-based authz — can this user access this id?), SSRF, and using components with known vulnerabilities. IDOR and broken access control are the most common in real apps.
Deep dive: IDOR: GET /reports/123 must check the report belongs to the caller's tenant, not just that they're logged in.
Senior signal: You name concrete mitigations per class and flag broken access control/IDOR as the most common real-world hole.
47. How do you trace a request across the full stack?
Short answer: A correlation/trace id generated at the edge and propagated through every hop (client -> API -> services -> logs), with structured logs carrying it. Then one id reconstructs the whole request across frontend and backend. Distributed tracing (OpenTelemetry) automates it.
Deep dive:
// Every log line + downstream call carries the trace id -> one request, one timeline.
using var _ = _logger.BeginScope(new Dictionary<string,object>{ ["TraceId"] = traceId });
Senior signal: You propagate a correlation id end-to-end with structured, trace-correlated logs (OpenTelemetry) so cross-stack debugging is possible.
48. What observability do you put in place?
Short answer: The three pillars — logs (structured), metrics (RED: rate/errors/duration; plus resource use), and traces — plus health checks (liveness/readiness) and alerting on SLOs. You should know something's wrong before the user reports it.
Deep dive: Mattrx ships metrics + traces to App Insights, exposes health endpoints, and alerts on error-rate/latency SLO breaches.
Senior signal: You instrument logs+metrics+traces, add health checks, and alert on SLOs — observability by design, not after an incident.
Block F — delivery & architecture
49. What does CI/CD look like for a full-stack app?
Short answer: On every push: build, lint, test (unit + integration), and on merge to main, deploy through environments (dev -> staging -> prod) with automated gates. Frontend and backend pipelines can be separate but coordinated. Fast, automated, reproducible — no manual deploys.
Deep dive: Mattrx's CI runs tests + builds, generates the migration script and typed client, and deploys the API (Azure) and frontend (Vercel) on green.
Senior signal: You automate build/test/deploy with environment gates and treat manual production steps as a risk.
50. How do you manage config and secrets across environments?
Short answer: 12-factor: config in the environment, not the code. Non-secrets in env vars/config per environment; secrets in a vault (Key Vault/Secrets Manager) injected at runtime; feature flags for progressive rollout. Same build artifact promoted across environments — only config differs.
Deep dive: Mattrx builds once and promotes the same artifact dev->staging->prod, with per-env config and Key Vault secrets.
Senior signal: You externalize config, keep secrets in a vault, promote one artifact across envs, and use flags for rollout.
51. Monorepo vs polyrepo, and sharing types across FE/BE?
Short answer: Monorepo eases sharing code/types and atomic cross-stack changes (one PR touches FE + BE); polyrepo gives independence at the cost of coordination. To share types: a shared package (TS monorepo) or generate client types from the API's OpenAPI/schema so the contract can't drift.
Deep dive: Mattrx generates TS types from the API contract, so a backend change that breaks the frontend fails the build.
Senior signal: You weigh monorepo sharing vs polyrepo independence and generate/share types to prevent contract drift.
52. What's your testing strategy across the stack?
Short answer: A test pyramid: many fast unit tests (logic, components), fewer integration tests (API + DB, with test containers), and a few end-to-end tests (Playwright/Cypress on critical user journeys). E2E is high-value but slow/flaky — use sparingly on the money paths.
Deep dive:
many -> unit (fast, isolated: domain logic, React components)
some -> integration (API + real DB via test containers, contract tests)
few -> E2E (login, publish, checkout — the critical journeys)
Senior signal: You balance the pyramid, cover critical journeys with E2E, and know an E2E-heavy suite becomes the flaky bottleneck.
53. How do you scale a full-stack app?
Short answer: Keep services stateless so you can scale horizontally behind a load balancer; push state to shared stores (DB, Redis, blob); cache aggressively; offload slow work to queues; and scale the database (read replicas, partitioning, caching) since it's the usual bottleneck. Scale the tier that's actually saturated.
Deep dive: Mattrx runs stateless API instances (session/state in Redis/DB) that autoscale on CPU/queue depth; the DB is the scaling frontier (rollups + caching + partitioning).
Senior signal: You make services stateless for horizontal scale, identify the real bottleneck (usually the DB), and scale that tier deliberately.
54. What deployment strategies reduce risk?
Short answer: Rolling (replace instances gradually), blue-green (stand up the new version, switch traffic, instant rollback), and canary (send a small % to the new version, watch metrics, then ramp). All aim for zero-downtime deploys with a fast rollback path.
Deep dive: Combined with backward-compatible migrations (Q33), these let you deploy anytime without downtime; Mattrx uses rolling deploys + canary for risky changes.
Senior signal: You choose a zero-downtime strategy with a rollback plan and pair it with backward-compatible schema changes.
55. Walk through designing a feature end-to-end.
Short answer: Take "add scheduled report emails." Frontend: a schedule form (validated) + a list of schedules (server state via query lib). API: POST /reports/schedules (validate, authz, persist). Backend: a scheduled worker enqueues due jobs; a worker renders (PuppeteerSharp) and emails. Data: a schedules table (tenant-scoped, indexed on next-run). Cross-cutting: idempotency on send, tracing, metrics, retries + DLQ. That's the full-stack loop: UI -> API -> data -> async worker -> delivery, with validation/authz/observability throughout.
Deep dive: The senior move is naming the seams: where you validate (client + API), where trust is enforced (server authz + tenant scope), what runs async (rendering/email off the request path), and how it fails safely (retries, DLQ, idempotent send).
Senior signal: You design across all tiers at once, place validation/authz/async/observability deliberately, and reason about the failure modes at each seam — not just the happy path.
Bonus — what makes an answer "senior" in full stack?
Short answer: A junior knows both sides; a senior owns the seams and the whole path. "Where do you validate this?" gets "on the form" from a junior and "on the client for UX and on the server as the trust boundary, with a DB constraint as the last line — because the client can be bypassed" from a senior.
Deep dive: The recurring senior lens across all 55: the client is untrusted, the server is authoritative, the network fails, and every layer (cache, DB, auth) has a trade-off. A senior full-stack engineer is T-shaped — broad across the stack, deep in one or two areas — and always reasons end-to-end: where does the data flow, where is it validated, cached, secured, and where can it fail?
Senior signal: You reason about trust boundaries, the end-to-end path, and failure at every seam — and you can go deep on demand in any single layer.
The model to carry forward
Full-stack seniority isn't knowing every framework — it's owning the path from a database column to a pixel and back, and knowing what happens at each hop. The client is for UX and is never trusted; the server is the authority and validates everything; the network will fail so design for retries and idempotency; caching and async make it fast but add staleness and complexity you must manage; and security/observability are woven through every layer, not bolted on. Answer each question with the seam, the trade-off, and the failure it prevents, and you'll sound like someone who has built and operated a real system end-to-end — which is the signal.
Further reading
- React Interview Questions for Senior Developers — Hooks, Reconciliation, Fiber & Performance (Deep Answers)
- ASP.NET Core Interview Questions for Senior Developers — Middleware, DI, Kestrel & Auth (Deep Answers)
- How to Crack the System Design Interview — A Repeatable Framework
Prepping for a senior full-stack interview and want these drilled live, or a mock that runs a feature end-to-end from UI to database? I'm always happy to help — reach me at randhir.jassal@gmail.com.
Get the next issue
A short, curated email with the newest posts and questions.