{}The Interview
Handbook

Tracks / Full-stack

End-to-end design & the take-home

mid 9 questions · 7 min read fullstackarchitecturetake-home

Questions in this set 9
  1. 01Walk me through building a feature end to end.
  2. 02How do you design authentication for a web app plus a mobile client?
  3. 03When do you need real-time, and what do you use?
  4. 04How do you handle forms, validation and errors across the stack?
  5. 05How do you keep frontend and backend in sync as a team?
  6. 06What is the difference between optimistic and pessimistic UI updates?
  7. 07How do you approach a take-home assignment?
  8. 08What questions should you ask before designing anything?
  9. 09How do you approach an unfamiliar codebase?
01

Walk me through building a feature end to end.

Take "let a user upload a profile photo" and narrate every layer — this is the archetypal full-stack question:

  1. Contract first. POST /v1/users/me/avatar{ url }. Decide limits: 5 MB, JPEG/PNG/WebP only.
  2. Upload strategy. Do not proxy the bytes through your API server. Issue a pre-signed URL (POST /v1/uploads{ upload_url, key }), have the browser PUT directly to S3/R2, then confirm with POST /v1/users/me/avatar { key }. This keeps your servers out of the data path and works with any file size.
  3. Validation. Content type and size on the pre-sign request; verify the actual bytes server-side after upload (magic-number sniffing, not the extension or client-supplied MIME type — a .jpg can be an HTML file, which is stored XSS if you serve it from your own origin). Serve user content from a separate domain with Content-Disposition and a restrictive CSP.
  4. Processing. Enqueue a job to generate thumbnails and strip EXIF (which contains GPS coordinates — a real privacy leak). The endpoint returns immediately with the original.
  5. Data model. users.avatar_key, plus an uploads table for orphan cleanup, since a user can pre-sign and never confirm.
  6. Delivery. CDN in front of the bucket, immutable cache headers with the key hashed, so a new upload is a new URL and invalidation is unnecessary.
  7. Frontend. Optimistic preview from the local file, progress via XMLHttpRequest/fetch streams, retry on failure, and a fallback avatar.
  8. Operations. Metrics on upload failures, alerting on the processing queue depth, a size quota per user, and a rate limit on the pre-sign endpoint (otherwise it is free storage for an attacker).

The point of the answer is that you thought about failure, security and cost — not just the happy path.

02

How do you design authentication for a web app plus a mobile client?

Web: session cookie — HttpOnly, Secure, SameSite=Lax, server-side session store for revocation. Immune to token theft via XSS, needs CSRF protection.

Mobile / third-party: OAuth 2.0 with PKCE issuing a short-lived access token (15 min) plus a refresh token stored in the platform keychain, with refresh token rotation and reuse detection — if an old refresh token is presented, the whole family is revoked, which is how you detect theft.

Shared decisions to talk through: password reset (single-use, short-TTL, hashed token, invalidate sessions on reset), email verification, MFA (TOTP, and WebAuthn/passkeys as the modern answer — phishing-resistant because the credential is bound to the origin), account lockout and rate limiting on login, and an audit log of authentication events. Never roll your own crypto or session format; use the framework's.

03

When do you need real-time, and what do you use?

  • Polling — simplest, fine for anything where 30 seconds of staleness is acceptable. Do not dismiss it; it survives every proxy and needs no new infrastructure.
  • Server-Sent Events — one-way server→client over plain HTTP, automatic reconnection, works through most proxies. The right choice for notifications, live dashboards, and streaming LLM output.
  • WebSockets — full duplex, for chat, collaboration and multiplayer. Costs: stateful connections change how you deploy and scale, you need a pub/sub layer (Redis, a managed service) so any node can reach any user, and you must handle reconnection, backfill of missed messages, and authentication at connect time.
  • WebRTC — peer-to-peer media; only when you genuinely need audio/video or ultra-low latency.

The senior instinct is to start at the top of that list and move down only when the requirement forces it.

04

How do you handle forms, validation and errors across the stack?

Validate in both places for different reasons: client-side for fast feedback, server-side because the client can be bypassed entirely. Share the schema if the stack allows (Zod on both sides of a TypeScript app; Pydantic generating a JSON Schema the frontend consumes).

Server errors should be structured so the client can attach them to fields:

json
{ "code": "validation_error",
  "errors": [{ "field": "email", "code": "already_taken", "message": "That email is registered." }] }

Then: disable the submit button while in flight and enforce idempotency server-side (double-submit is the classic duplicate-order bug); preserve user input on failure; announce errors to screen readers with aria-live and move focus to the first invalid field; and never lose a long form's content on an error — restore it.

05

How do you keep frontend and backend in sync as a team?

Generate, do not hand-write, the contract. OpenAPI from the backend (FastAPI and DRF do it automatically) → generated TypeScript client. Or a shared schema package. Or GraphQL with codegen. The generated types then make a breaking API change a compile error in the frontend, which is exactly where you want to find it.

Process, in addition: version the API and only make additive changes; contract tests in CI; a mock server generated from the schema so the frontend is not blocked on backend delivery; and feature flags so the two sides can merge and deploy independently, turning the feature on when both are ready.

06

What is the difference between optimistic and pessimistic UI updates?

Pessimistic: wait for the server, then update. Simple, always correct, feels slow. Optimistic: update immediately, assume success, reconcile or roll back on failure. Feels instant; requires you to handle rollback, error surfacing, and out-of-order responses.

js
// React Query pattern
useMutation({
  mutationFn: toggleLike,
  onMutate: async (id) => {
    await qc.cancelQueries({ queryKey: ["post", id] });    // stop in-flight refetches
    const prev = qc.getQueryData(["post", id]);
    qc.setQueryData(["post", id], p => ({ ...p, liked: !p.liked }));
    return { prev };                                       // context for rollback
  },
  onError: (_e, id, ctx) => qc.setQueryData(["post", id], ctx.prev),
  onSettled: (_d, _e, id) => qc.invalidateQueries({ queryKey: ["post", id] }),
});

Use optimistic updates for cheap, near-certain, easily-reversible actions (likes, reordering, checkboxes). Never for payments, irreversible deletes, or anything where being wrong costs the user real money or data.

07

How do you approach a take-home assignment?

Take-homes are won on judgement, not cleverness. What reviewers actually look for:

  1. A README that is a real README. How to run it (one command, ideally docker compose up), what you built, what you deliberately left out and why, and what you would do with more time. This single file moves more take-homes from "maybe" to "yes" than any code in the repository.
  2. Working over complete. A smaller scope that runs perfectly beats a broad one with a broken setup. If it does not start on their machine, nothing else counts.
  3. Tests for the core logic. Not 100% coverage — the three or four tests that show you know what is worth testing.
  4. Clean git history with meaningful commits, not one commit called "solution".
  5. Handle the obvious edge cases and say which ones you knowingly did not handle.
  6. Respect the time limit. If they say four hours, spend four to six and note in the README what you would add next. Spending twenty hours signals bad prioritisation, and it is unfair to candidates who followed the instructions.
  7. No unnecessary architecture. Kubernetes, microservices and event sourcing for a CRUD assignment reads as poor judgement, not ambition.

Then prepare to defend it: the follow-up interview is usually "walk me through your design and tell me what you would change" — and the strongest answer already lists three things.

08

What questions should you ask before designing anything?

Who uses it, and how many? What is the read:write ratio? How fresh must the data be? What is the failure cost — is this money, or a view counter? What are we integrating with that we do not control? What is the deadline, and what can be cut? Is there an existing pattern in this codebase I should follow?

That last one is worth its own mention in an interview: a full-stack engineer's job is usually to fit into an existing system, not to design a new one from a blank page. Saying "the first thing I'd do is find out how the team already solves this" is a maturity signal that almost nobody offers.

09

How do you approach an unfamiliar codebase?

Run it first — get it working locally before reading anything, because that is where the undocumented knowledge lives. Then read from the outside in: the README, the routes/URL configuration, the data models, then one full request path end to end. Use the tests as documentation of intended behaviour. Use git log on a file to see who to ask and why the code is the way it is. Change something small and deliberately break a test to confirm you understand the feedback loop. And write down what confused you — that becomes your first useful pull request, which is a very good way to start on a new team.