Skip to content
RED SAGSkills

Full Stack Developer interview questions and answers

30 questions interviewers really ask, with what a strong answer covers. Read them, then test yourself in a timed mock interview.

Start a Full Stack Developer mock interview

Easy questions easy

1. What happens, step by step, when a user types your app URL into the browser and presses Enter?

A strong answer covers:

  • DNS lookup turns the domain into an IP address
  • TCP and TLS handshake set up a secure connection
  • Browser sends an HTTP request and the server or CDN returns HTML
  • Browser parses HTML, loads CSS and JavaScript, and renders the page
  • JavaScript may hydrate the page and call APIs for more data
Show model answer

First the browser finds the IP address for the domain using DNS, checking its own cache, the OS cache and then DNS resolvers. It opens a TCP connection, or QUIC for HTTP/3, and does a TLS handshake so the connection is encrypted. Then it sends an HTTP GET request. The request may be answered by a CDN edge cache, or reach my server, which might run middleware, check the session cookie, query the database and render HTML. The browser parses the HTML, builds the DOM, downloads CSS, JavaScript, fonts and images, and paints the page. In a React or Next.js app, JavaScript then hydrates the HTML so buttons become interactive, and the app may call APIs for more data. Each step is a place where performance can be improved, for example with caching, compression and smaller bundles.

2. Design the REST API endpoints for a simple blog with posts and comments. What URLs, methods and status codes would you use?

A strong answer covers:

  • Use nouns for resources and HTTP methods for actions
  • Nested routes for comments under a post
  • Correct status codes such as 200, 201, 204, 400, 401, 404
  • Pagination and filtering through query parameters
  • Validation and consistent error format
Show model answer

I would model posts and comments as resources. GET /api/posts lists posts with pagination like ?limit=20&cursor=abc, and GET /api/posts/:id returns one post. POST /api/posts creates a post and returns 201 with the new object. PATCH /api/posts/:id updates some fields and DELETE /api/posts/:id returns 204. For comments, GET /api/posts/:id/comments and POST /api/posts/:id/comments, and DELETE /api/comments/:id for a single comment. I would return 400 for invalid input with a clear JSON error body, 401 when the user is not logged in, 403 when they are not allowed, and 404 when the item does not exist. I would validate input on the server with a schema library like Zod, keep the response shape consistent, and only allow the author or an admin to edit or delete a post.

3. What is the difference between authentication and authorization? Give an example from a web app.

A strong answer covers:

  • Authentication proves who the user is
  • Authorization decides what that user is allowed to do
  • Authentication happens first, then authorization on each request
  • Authorization must be checked on the server, not only hidden in the UI
  • 401 vs 403 status codes
Show model answer

Authentication answers who are you. For example, a user logs in with email and password or Google sign-in, and the server creates a session. Authorization answers what are you allowed to do. For example, in a school app, a logged-in student can view their own marks, but only a teacher can edit marks, and only an admin can add teachers. Authentication happens first and authorization is checked on every request that touches data. An important point is that authorization must be enforced on the server, inside the API or server action, not just by hiding a button in the UI, because anyone can call the API directly. If the user is not logged in I return 401, and if they are logged in but not allowed I return 403. I also check ownership, so user A cannot read user B data by changing an id in the URL.

4. You are starting a new e-commerce app. Would you choose a SQL or a NoSQL database, and why?

A strong answer covers:

  • Relational data such as orders, users and products fits SQL well
  • Transactions and constraints matter for money and stock
  • NoSQL fits flexible schemas or very high write scale
  • Postgres can also store JSON when some flexibility is needed
  • Choice depends on access patterns and team skills
Show model answer

For a typical e-commerce app I would start with a relational database like PostgreSQL. The data is naturally relational: users have orders, orders have items, items point to products. I need ACID transactions so that creating an order and reducing stock happen together, and constraints like foreign keys and unique emails protect data quality. SQL also makes reporting easy with joins and aggregates. NoSQL databases like MongoDB or DynamoDB are good when the schema changes a lot, when data is mostly read by a single key, or when write volume is huge and needs easy horizontal scaling. Postgres also has JSONB columns, so I can store flexible product attributes without leaving SQL. So my default is Postgres, and I would add something like Redis for caching or a search engine later if the access patterns need it.

5. Explain client-side rendering, server-side rendering and static site generation. When would you use each?

A strong answer covers:

  • CSR renders in the browser after JavaScript loads
  • SSR renders HTML on the server for each request
  • SSG builds HTML at build time and serves it from a CDN
  • Trade-offs in SEO, speed, freshness and server cost
  • Frameworks like Next.js mix these per route
Show model answer

With client-side rendering, the server sends a mostly empty HTML page and JavaScript builds the UI in the browser. It is fine for dashboards behind a login where SEO does not matter, but the first load can be slow. With server-side rendering, the server builds HTML for each request, so the user and search engines see content quickly and the data is always fresh, but each request costs server time. Static site generation builds pages at build time and serves them from a CDN, which is the fastest and cheapest, and great for blogs, docs and marketing pages, but data can be stale until you rebuild or revalidate. In Next.js I can mix them: static pages for marketing, revalidated static pages for product listings, dynamic server rendering for personalized pages, and client components only where I need interactivity.

6. In a web app with a React frontend and an API, where should input validation happen, and why?

A strong answer covers:

  • Client-side validation is for fast feedback and better UX
  • Server-side validation is required because clients can be bypassed
  • Database constraints are the last line of defense
  • Sharing one schema keeps rules consistent across tiers
  • Return clear field-level errors the UI can display
Show model answer

I validate in three places, each for a different reason. In the browser I validate for user experience, so people see mistakes immediately without a round trip. On the server I validate every request again, because anyone can skip the form and call the API with curl or a script; the server is the real security boundary. Finally, the database has constraints like NOT NULL, UNIQUE, foreign keys and CHECK rules, which protect data even if a bug slips through the application code. In a TypeScript project I like to define one schema, for example with Zod, and use it in both the form and the API handler, so limits and messages never drift. When validation fails the API returns a 400 or 422 with field-level errors, and the form shows each message next to the right input.

7. A page loads data from your API. How do you handle loading, empty and error states so users are never left confused?

A strong answer covers:

  • Design loading, empty, error and success states explicitly
  • Use a consistent error format from the API with useful status codes
  • Show friendly messages and a retry option, not raw errors
  • Distinguish expected errors (404, 422) from unexpected ones (500)
  • Log or report unexpected errors with a request ID
Show model answer

I treat every data-driven view as having four states: loading, empty, error and success, and I design each one on purpose. While loading I show a skeleton that matches the layout so the page does not jump. If the list is empty I show a helpful message and an action, such as "Create your first project". For errors, the API returns a consistent JSON shape with a proper status code, so the UI can react differently: a 404 shows "not found", a 422 shows field messages, a 401 sends the user to login, and a 500 shows a friendly message with a Retry button. In React I use an error boundary around each section so one failure does not blank the whole page. Unexpected errors are reported to monitoring with a request ID, which I also display so support can find the exact log.

8. How do you manage configuration and secrets across development, staging and production for a full-stack app?

A strong answer covers:

  • Keep config in environment variables, not hard-coded
  • Never commit secrets; commit a .env.example instead
  • Use the hosting platform or a secret manager per environment
  • Separate public client config from server-only secrets
  • Validate required variables at startup
Show model answer

I keep anything that changes between environments, such as database URLs, API keys and feature settings, in environment variables rather than in code. Locally I use a .env file that is in .gitignore, and I commit a .env.example listing the names with fake values so new developers know what to set. In staging and production the values live in the hosting platform’s settings or a secret manager like AWS Secrets Manager, with separate credentials per environment so a staging leak never touches production data. I am careful about what reaches the browser: in Next.js only NEXT_PUBLIC_ variables are bundled, so secrets never get that prefix. At startup I validate the variables with a small schema, so a missing value fails the deploy immediately instead of breaking a page later. If a secret leaks, I rotate it.

9. How would you explain a database index to a junior developer, and how do you decide which columns to index?

A strong answer covers:

  • An index is a sorted lookup structure, like a book index
  • It speeds up filters, joins and sorting on indexed columns
  • It costs disk space and slows writes
  • Choose columns from real slow queries, checked with EXPLAIN
  • Foreign keys and frequently filtered columns are common candidates
Show model answer

I compare it to the index at the back of a book. Without it you read every page to find a topic, which is a full table scan. With it you jump straight to the right pages. Most database indexes are B-trees: sorted copies of one or more columns with pointers to the rows, so lookups, range filters, joins and ORDER BY on those columns become fast. The cost is that every insert, update and delete must also update each index, and indexes take disk space, so you do not index everything. I choose indexes from real queries: I find slow ones in logs or monitoring, run EXPLAIN ANALYZE, and look for sequential scans on large tables. Foreign keys used in joins and columns used in WHERE clauses on busy pages, like user_id on orders, are usually the first candidates.

10. Describe the Git and code review workflow you follow when building a feature on a team.

A strong answer covers:

  • Create a short-lived feature branch from main
  • Make small, focused commits with clear messages
  • Open a pull request with context, screenshots and test notes
  • CI runs lint, type checks and tests before merging
  • Respond to review feedback and keep the branch up to date
Show model answer

I start by pulling the latest main and creating a short-lived branch named after the ticket. I work in small commits with clear messages, so each one is easy to understand and to revert. I try to keep the pull request small, ideally one feature or fix, because big PRs get shallow reviews. When I open it I explain what changed and why, link the ticket, add screenshots for UI changes and describe how I tested it. CI runs linting, type checks, unit tests and a preview deployment, and I fix anything red before asking for review. I treat review comments as a conversation: I either make the change or explain my reasoning politely. Before merging I rebase or merge main to resolve conflicts, then squash-merge and delete the branch so main stays clean and deployable.

Medium questions medium

1. How would you implement login for a web app? Compare server sessions with JWT access tokens and explain what you would choose.

A strong answer covers:

  • Sessions store state on the server and a random id in a cookie
  • JWTs are signed, stateless and hard to revoke before expiry
  • Use HttpOnly, Secure, SameSite cookies either way for browsers
  • Short-lived access tokens with refresh token rotation if using JWT
  • Password hashing, rate limiting and CSRF protection
Show model answer

For a normal web app where the browser talks to my own backend, I would use server sessions. On login I verify the password against an Argon2 or bcrypt hash, create a session row in the database or Redis, and set a random session id in an HttpOnly, Secure, SameSite=Lax cookie. Logout or a ban simply deletes the session, so revocation is instant. JWTs are signed tokens the server can verify without a lookup, which helps when many services or mobile clients need to verify identity. The downside is that they are hard to revoke before expiry, so I would keep access tokens short, around 5 to 15 minutes, and use rotating refresh tokens with reuse detection. In both cases I add rate limiting on login, CSRF protection for cookie auth, and I would seriously consider a proven library or provider instead of writing everything myself.

2. Users say the orders page is slow. How do you find and fix the problem across frontend, API and database?

A strong answer covers:

  • Measure first: browser DevTools, Core Web Vitals, network waterfall
  • Check API timing with logs, tracing or APM
  • Inspect slow queries with EXPLAIN ANALYZE and add indexes
  • Look for N+1 queries, large payloads and missing pagination
  • Fix, then measure again and add monitoring
Show model answer

I start by measuring, not guessing. In Chrome DevTools I look at the network waterfall and performance panel: is the time spent waiting for the API, downloading a large JavaScript bundle, or rendering a huge list? If the API is slow, I check server logs or tracing with something like OpenTelemetry to see which part of the request takes time. Often it is the database, so I run EXPLAIN ANALYZE on the slow query, look for sequential scans and add a suitable index, or fix an N+1 pattern by using a join. I also check if the API returns too much data, and add pagination or select only needed columns. On the frontend I might virtualize a long list, split a heavy bundle or cache data with a library like TanStack Query. After each fix I measure again and add an alert so we notice if it gets slow later.

3. How would you build a feature that lets users upload profile photos and PDF documents up to 100 MB?

A strong answer covers:

  • Upload directly to object storage with presigned URLs
  • Validate file type and size on the server
  • Store only metadata and object key in the database
  • Process files asynchronously, such as resizing or virus scanning
  • Serve through a CDN with access control for private files
Show model answer

I would not send large files through my API server, because it ties up memory and hits serverless body limits. Instead the client asks my API for an upload URL. The server checks the user is logged in, checks the declared file type and size, and returns a short-lived presigned URL for object storage like S3 or R2. The browser uploads directly to storage, using multipart upload for big files so it can resume. When the upload finishes, the client tells my API, or a storage event triggers it, and I save metadata like the object key, size, type and owner in the database. A background job then checks the real file type, scans for viruses and creates resized images. Public images are served through a CDN, while private PDFs are served with short-lived signed URLs after an authorization check.

4. Your API is used by a web app and an older mobile app that users do not update quickly. How do you make a breaking change safely?

A strong answer covers:

  • Prefer additive, backward compatible changes
  • Version the API when a breaking change is unavoidable
  • Run old and new versions side by side with a deprecation period
  • Measure usage of the old version before removing it
  • Contract tests or typed schemas to catch breaks early
Show model answer

First I try to avoid a breaking change at all. Adding new optional fields or new endpoints is backward compatible, so old clients keep working. If I must change the meaning or shape of a field, I add the new field next to the old one and keep filling both for a while. When a real break is needed, I create a new version, for example /v2/orders or a version header, and run both versions side by side. I mark the old version as deprecated, tell client teams, and use logs or analytics to see which app versions still call it. For mobile, I can use a minimum supported version check to ask very old apps to update. Only when usage is near zero do I remove v1. I keep an OpenAPI schema or shared types and contract tests in CI so accidental breaking changes are caught before release.

5. Describe a good CI/CD pipeline for a full stack app. What runs on each pull request and how do you deploy safely?

A strong answer covers:

  • Lint, type check, unit and integration tests on every PR
  • Preview deployments for review
  • Database migrations run as a controlled step
  • Gradual or blue-green rollout with health checks
  • Fast rollback and monitoring after deploy
Show model answer

On every pull request, GitHub Actions or a similar tool installs dependencies with a cached lockfile, then runs linting, TypeScript type checks, unit tests and integration tests against a real test database in a container. It also builds the app, and a platform like Vercel creates a preview deployment so reviewers and QA can click through the change. After merge to main, the pipeline runs database migrations as a separate step, written to be backward compatible, and then deploys. I prefer gradual rollouts or blue-green deployments with health checks, so a bad release only reaches a few users. Feature flags let us ship code turned off and enable it later. After deploy, error tracking and dashboards show if error rates or latency go up, and rollback is one click, which is another reason migrations must stay compatible with the previous version.

6. You need to build a product search page with text search, filters and sorting. Walk through the design from the UI to the database.

A strong answer covers:

  • Keep search state in the URL query string so it is shareable
  • Debounce input and cancel stale requests
  • Design a validated API with filters, sort and cursor pagination
  • Use suitable indexes or full-text search in the database
  • Consider a search engine when needs grow (typos, ranking)
  • Cache common queries and measure performance
Show model answer

On the frontend I store the search text, filters and sort in the URL query string, so results are shareable, bookmarkable and survive refresh. The input is debounced by around 300 ms, and I cancel stale requests so old responses never overwrite new ones. The API is GET /api/products with parameters like q, category, minPrice, sort and cursor; it validates and allowlists every parameter, especially sort fields, and returns items plus a next cursor. In PostgreSQL I start with full-text search using a tsvector column and a GIN index, plus B-tree indexes on common filter columns, and I check plans with EXPLAIN. Cursor pagination keeps deep pages fast. If we later need typo tolerance, synonyms or ranking tuning, I would sync data to a search engine like Meilisearch or Elasticsearch. Popular queries can be cached briefly at the CDN.

7. How would you add role-based access control (admin, editor, viewer) to an existing full-stack app?

A strong answer covers:

  • Model roles and permissions in the database, scoped per organisation if needed
  • Enforce permissions on the server for every request and action
  • Centralize checks in one helper or policy layer
  • Use the UI only to hide or disable actions, never as the security boundary
  • Check ownership of specific records, not just the role
  • Test permissions and audit sensitive actions
Show model answer

I start with the data model: users have memberships in an organisation, each with a role, and roles map to permissions such as project:edit or billing:manage. Checking permissions rather than role names makes it easier to add roles later. On the server I write one central helper, like can(user, "project:edit", project), and call it in every route handler and server action, because the server is the only real boundary. The check covers both the permission and the specific record, so an editor in one organisation cannot edit another organisation’s project by changing an ID. Database queries are also scoped by organisation ID. In the UI I use the same permission data to hide or disable buttons, purely for user experience. I add tests for each role on sensitive endpoints, follow least privilege for new roles, and keep an audit log of admin actions.

8. What is your testing strategy for a full-stack feature such as "invite a teammate by email"?

A strong answer covers:

  • Unit tests for pure logic such as validation and token rules
  • Integration tests for API routes against a real test database
  • Mock external services like email at the boundary
  • A few end-to-end tests for the critical user journey
  • Run tests in CI on every pull request and keep them fast and reliable
Show model answer

I follow a testing pyramid or trophy shape. Unit tests cover the pure logic: email validation, invite token generation and expiry rules, and the permission check for who can invite. Most of my confidence comes from integration tests that call the real API route against a real PostgreSQL test database, for example in a Docker container in CI, checking that an invite row is created, duplicates are rejected and a viewer gets 403. External services like the email provider are mocked at the boundary, so I assert that an email was requested with the right link without sending one. Then I write one or two Playwright end-to-end tests for the main journey: admin sends an invite, the invitee opens the link and joins the team. Everything runs in CI on each pull request, and I fix flaky tests quickly rather than adding retries.

9. Describe the different layers where you can cache in a typical full-stack web app, and how you keep cached data correct.

A strong answer covers:

  • Browser cache controlled by Cache-Control and ETags
  • CDN or edge cache for public pages and static assets
  • Framework or server cache, such as Next.js data cache or Redis
  • Client-side data cache such as TanStack Query
  • Invalidation via TTLs, tags or events on write
  • Never share-cache personalized data
Show model answer

I think of caching from the outside in. In the browser, Cache-Control and ETags let hashed assets be cached for a year and API responses be revalidated cheaply. A CDN caches static files and public pages close to users, often with stale-while-revalidate so updates appear quickly without slow misses. On the server, Next.js can cache rendered pages and fetched data, and Redis stores expensive query results or sessions using the cache-aside pattern. On the client, a library like TanStack Query caches API data and refetches in the background. To keep data correct, I pick TTLs based on how stale the data may be, and I invalidate on writes using tags, revalidatePath or deleting Redis keys after the database commit. The key rule is that anything personalized is marked private or no-store, so a shared cache never serves one user’s data to another.

10. You need to show in-app notifications in real time. How do you choose between polling, server-sent events and WebSockets, and how would you build it?

A strong answer covers:

  • Polling is simplest but adds latency and load
  • Server-sent events suit one-way server-to-client updates
  • WebSockets suit two-way, low-latency interaction
  • Store notifications in the database as the source of truth
  • Use pub/sub (e.g. Redis) to fan out across server instances
  • Handle reconnects and missed messages
Show model answer

Notifications flow one way, from server to client, so I usually choose server-sent events: they work over normal HTTP, reconnect automatically and send a Last-Event-ID so the server can replay missed events. Polling every 30 seconds is a fine first version if real time is not critical. WebSockets are worth the extra complexity when the client also sends frequent messages, as in chat or collaborative editing. Either way, notifications are first written to a database table, which is the source of truth for the unread count and history. After saving, the API publishes an event to Redis pub/sub or a managed real-time service, and every server instance pushes it to connected clients of that user. On serverless platforms, long-lived connections are limited, so a managed service like Ably or Pusher is often simpler.

Hard questions hard

1. You are building a multi-tenant B2B SaaS app. How would you design data isolation between tenants, and what are the trade-offs?

A strong answer covers:

  • Options: shared tables with tenant_id, schema per tenant, database per tenant
  • Enforce tenant filtering centrally, for example with Postgres row level security
  • Tenant id comes from the authenticated session, never from user input
  • Trade-offs in cost, isolation, migrations and noisy neighbours
  • Tests and audits that prove cross-tenant access is impossible
Show model answer

There are three common models. Shared tables with a tenant_id column are cheapest and easiest to migrate, but one missing WHERE clause can leak data. Schema per tenant gives stronger separation but makes migrations slow with thousands of tenants. Database per tenant gives the best isolation and per-customer backups, and suits large enterprise customers, but costs more and needs automation. For most products I start with shared tables and make isolation hard to get wrong: the tenant id comes from the authenticated session, never from the request body, and I enforce it centrally with Postgres row level security or a data access layer that always adds the tenant filter. Every tenant-owned table has tenant_id in its indexes and unique constraints. I add rate limits per tenant against noisy neighbours, automated tests that try cross-tenant access, and a path to move a big customer to its own database later.

2. Your app works well for 1,000 users. Traffic is expected to grow to 1 million users. How would you prepare the system, and in what order?

A strong answer covers:

  • Measure and find the real bottlenecks before changing architecture
  • Stateless app servers behind a load balancer with autoscaling
  • CDN and caching for static and repeated reads
  • Database: indexes, connection pooling, read replicas, then partitioning
  • Background queues for slow work, plus monitoring and load testing
Show model answer

I would first measure. I add observability, such as metrics, logs and tracing, and run load tests to find what breaks first, because guessing leads to complex systems we do not need. Usually the order is: make app servers stateless, keeping sessions and uploads in shared stores, so I can run many instances behind a load balancer with autoscaling. Put static assets and cacheable pages behind a CDN. Then the database, which is usually the real bottleneck: fix slow queries and indexes, add connection pooling, cache hot reads in Redis with clear invalidation, and add read replicas for read-heavy pages while being careful about replica lag. Slow work like emails, image processing and reports moves to a queue with workers. Only if one database still cannot handle writes would I consider partitioning or sharding. Throughout, I keep rate limits, graceful degradation and runbooks ready.

3. An order flow touches your database, a payment provider and an email service. How do you keep the system consistent when any step can fail?

A strong answer covers:

  • No single transaction across external systems
  • Idempotency keys for retries with the payment provider
  • Transactional outbox for reliable events
  • Saga style steps with compensating actions
  • Webhooks verified and processed idempotently; reconciliation jobs
Show model answer

I accept that there is no single transaction across my database, the payment provider and email, so I design for retries and partial failure. I model the order as a state machine: pending, paid, confirmed, failed. Creating the order and a pending payment record happens in one database transaction. Calls to the payment provider use an idempotency key, so a retry after a timeout cannot charge twice. The provider tells me the final result through a webhook, which I verify by signature and process idempotently, because webhooks can arrive late, twice or out of order. When the order becomes paid, I write an event to an outbox table in the same transaction, and a worker sends the email with at-least-once delivery. If a later step fails, I run a compensating action such as a refund, saga style. Finally, a daily reconciliation job compares my records with the provider to catch anything missed.

4. You need to split a large users table column "name" into "first_name" and "last_name" on a table with 50 million rows, with no downtime. Walk through your plan.

A strong answer covers:

  • Expand and contract across several deploys
  • Add nullable columns without long locks
  • Dual write from the application, then backfill in small batches
  • Switch reads and verify data before dropping the old column
  • Each step must be compatible with the previous app version and reversible
Show model answer

I would use expand and contract over several releases. Step one: add first_name and last_name as nullable columns with no default that rewrites the table, so the ALTER is quick and takes only a brief lock; I set a lock_timeout so it fails fast instead of blocking traffic. Step two: deploy code that writes to both the old and new columns on every create and update, while still reading the old one. Step three: backfill old rows in small batches, for example 5,000 rows at a time by primary key, with pauses so replication lag and load stay low; the job must be resumable. Step four: verify that the new columns match, then deploy code that reads from the new columns. Step five, in a later release once nothing uses it: stop writing name and drop it. At every step the previous app version still works, so a rollback is always safe.

5. For a large e-commerce site in Next.js, how would you choose the rendering and caching strategy for the home page, product pages, cart and account pages?

A strong answer covers:

  • Static or cached rendering for pages shared by all users
  • Revalidation by time or on-demand tags when products change
  • Dynamic, personalized parts streamed or rendered separately
  • Cart and account are user-specific and never cached publicly
  • Trade-offs between freshness, cost, SEO and Core Web Vitals
Show model answer

I would decide per route based on how personal and how fresh the data must be. The home page and category pages are mostly the same for everyone, so I prerender them and cache them at the CDN, with revalidation by time or on-demand by cache tags when the catalog changes. Product pages matter most for SEO, so I prerender popular ones and render the rest on first request and cache them; when a price or description changes, the admin action invalidates that product tag. Fast-changing parts like live stock or personalized recommendations I render dynamically inside a Suspense boundary, so the static shell streams instantly, which is what partial prerendering is designed for. Cart, checkout and account pages are per user, so they are rendered dynamically and marked private, never stored in a shared cache. I would watch Core Web Vitals and cache hit rates to tune this.

6. Design an e-commerce checkout end to end: cart, payment with a provider like Stripe, inventory and order confirmation. What can go wrong and how do you make it reliable?

A strong answer covers:

  • Server calculates prices and totals, never trusting the client
  • Reserve or atomically decrement inventory to avoid overselling
  • Use idempotency keys for payment requests
  • Treat verified webhooks as the source of truth for payment status
  • Model the order as a state machine and make handlers idempotent
  • Send emails and other side effects through a queue or outbox
Show model answer

The client only sends cart item IDs and quantities; the server looks up prices, discounts and tax, so totals cannot be tampered with. When the user starts checkout, I create an order in PENDING state and reserve stock with an atomic conditional update, so two buyers cannot take the last item; reservations expire if payment never completes. I create a Stripe PaymentIntent with an idempotency key tied to the order, so retries never double charge. The browser confirms the payment, but I mark the order PAID only when a signature-verified webhook arrives, because the user might close the tab. Webhook handlers are idempotent, since providers retry, and the order moves through a clear state machine. Confirmation emails and fulfilment go through an outbox or job queue. A daily reconciliation job compares orders with the provider’s records to catch anything missed.

7. Your startup’s Next.js monolith is growing and some engineers want to split it into microservices. How do you decide, and what would you do?

A strong answer covers:

  • Identify the real pain: deploy speed, scaling, team ownership or reliability
  • Microservices add network failures, distributed data and ops cost
  • A modular monolith with clear boundaries is often the better first step
  • Extract services only where scaling or ownership clearly needs it
  • Use the strangler pattern and keep data ownership per service
Show model answer

First I ask what problem we are actually solving. Slow builds, unclear ownership, one hot path that needs different scaling, or frequent outages caused by unrelated code all have different fixes. Microservices bring real costs: network calls that fail, distributed transactions, harder debugging, more deployment pipelines and the need for good observability. For a small team that cost usually outweighs the benefit. So my default is a modular monolith: clear domain modules such as billing, catalog and accounts, with explicit interfaces, no reaching into each other’s tables, and lint rules that enforce boundaries. If one part truly needs independent scaling or a separate team, like video processing or search indexing, I extract it using the strangler pattern behind a stable API, give it its own data, and communicate through events where possible. I measure whether the split improved the original pain.

8. Ten minutes after a deploy, 500 errors jump from 0.1% to 8% on the main API. Walk through how you handle the incident.

A strong answer covers:

  • Mitigate first: roll back or disable the feature flag
  • Communicate status to the team and stakeholders
  • Use metrics, logs and traces to find the failing path
  • Compare the deploy diff, including config and migrations
  • Confirm recovery with metrics before closing
  • Run a blameless postmortem with follow-up actions
Show model answer

My first goal is to stop the damage, not to find the root cause. Because the spike started right after a deploy, I roll back to the previous version or turn off the feature flag, and I post in the incident channel who is leading and what we are doing. One catch: if the deploy included a database migration, I check that the old code still works with the new schema before rolling back. Once error rates recover, I investigate using the dashboards, the error tracker’s grouped stack traces and distributed traces for failing requests, filtering by endpoint and region. I compare the diff, including environment variables, dependency upgrades and migrations, and reproduce the issue in staging. After the fix ships safely, I write a blameless postmortem covering the timeline, impact, root cause and actions, such as better tests, canary deploys or an alert that would have fired sooner.

9. Customers need to import CSV files with up to two million rows into your app. Design the feature end to end.

A strong answer covers:

  • Upload directly to object storage with a presigned URL
  • Process asynchronously with a job queue and workers
  • Stream and parse in batches instead of loading the whole file
  • Validate rows, collect errors and make imports resumable and idempotent
  • Show progress and results in the UI
  • Use bulk inserts or COPY and watch database load
Show model answer

A two-million-row file cannot be handled inside one HTTP request. The browser asks the API for a presigned URL and uploads directly to S3, then the API creates an import record with status QUEUED and enqueues a job. A worker streams the file from storage, parses it row by row and processes batches of, say, 5,000 rows, so memory stays flat. Each row is validated with the same schema the API uses; bad rows are written to an error report instead of failing the whole import. Batches are written with bulk inserts or PostgreSQL COPY into a staging table, then merged with upserts keyed on a natural ID, which makes retries idempotent and lets a crashed job resume from its last checkpoint. The worker updates progress on the import record, and the UI polls or subscribes to show it, finishing with a summary and downloadable error file.

10. You are opening your product’s API to outside partners. How do you design authentication, authorization, rate limiting and change management for it?

A strong answer covers:

  • Use OAuth 2.0 scopes for user-delegated access and hashed API keys for server-to-server use
  • Scope every request to the partner’s tenant and permissions
  • Apply per-client rate limits and quotas with clear headers
  • Sign outgoing webhooks and support key rotation
  • Version the API, publish an OpenAPI contract and deprecate with notice
  • Log and monitor usage per client for abuse and debugging
Show model answer

For partners acting on behalf of our users, I use OAuth 2.0 authorization code flow with narrow scopes like orders:read, so users approve exactly what the partner can do. For server-to-server integrations I issue API keys with a visible prefix, store only their hash, allow several active keys for rotation and let customers revoke them. Every request is scoped to the client’s tenant and scopes in a central authorization layer. Rate limits and quotas are per client, enforced in a shared store, with 429 responses and RateLimit headers. Outgoing webhooks are signed with HMAC and a timestamp to stop replay. The API is described by an OpenAPI contract, versioned, and changed additively; breaking changes get a new version, deprecation headers and months of notice. Per-client logs and dashboards help support partners and spot abuse.