Full-Stack Questions
The Experience-Level Round
Ownership, trade-offs, debugging stories, code review and system-design-lite questions with model answers.
On this page
- What changes between junior, 4+ years and senior
- How to tell a story: STAR with numbers
- The nine classic questions, with example answers
- Live coding and take-home tasks for full-stack roles
- System-design-lite prompts for 4+ years
- Code-review exercise
- Pitfalls that cost offers
- Questions to ask the interviewer
- Level and salary conversation basics
- Interview questions
- Key takeaways
- What's next
In one line: at 4+ years the interviewer stops asking "do you know X?" and starts asking "can I trust you to own a feature, make a sensible trade-off, find a bug without help, and tell me about it clearly?". This lesson shows how to prove that, with story examples, system-design-lite answers and a code-review exercise.
Earlier lessons covered the tools. This one covers you. The round has many names: "experience round", "manager round", "hiring-bar round", "behavioural round". The questions are about your past work and about how you think when a problem is open. The people who fail it are usually not weak engineers. They tell vague stories, say "we" for everything, never mention numbers, or start coding before they ask a question.
For decision stories at a larger scale (rewrite or refactor, build or buy, zero-downtime migrations) read Debugging, Lesson 14. For incident handling read Production Incidents. This lesson keeps the examples short and full-stack flavoured.
What changes between junior, 4+ years and senior
| Junior (0 to 2 years) | 4+ years (the "solid mid / early senior" bar) | Senior and above | |
|---|---|---|---|
| Scope | A task someone gave you | A whole feature or service, from idea to production, mostly alone | A project across teams, or a technical direction |
| Questions asked | "What is X?" | "Why did you pick X? What went wrong? What would you change?" | "How do you choose between three bad options and bring people along?" |
| Bugs | Can fix with help | Finds the cause alone, using logs and measurements | Builds the habits and tools so the bug class stops |
| Ambiguity | Waits for details | Asks good questions, writes down assumptions, proceeds | Shapes the problem itself |
| Other people | Learns from the team | Reviews code well, helps juniors, speaks to product and design | Grows a team; decides who does what |
One public rubric from large companies says the same thing: junior stories are about your own tasks, senior stories are about a team (often three or more people), and staff stories are about several teams. At 4+ years, the safest story size is "a feature or system that I owned, that affected my team and users, with a number as proof."
What the interviewer is really scoring, in plain words:
- Ownership: did you see it through to production and afterwards, or stop at "merged"?
- Judgement: did you weigh options and know what you gave up?
- Debugging skill: do you measure first, then guess?
- Communication: can you explain it so a non-expert follows?
- Learning: what would you do differently now?
- Collaboration: can you disagree without a fight, and can you say no politely?
How to tell a story: STAR with numbers
STAR stands for Situation, Task, Action, Result. It is a way to order a story so it does not wander.
- Situation (1 or 2 sentences): where and when, and why it mattered.
- Task: what you were responsible for.
- Action (the longest part, about half of your time): what you did, step by step, and why. Say "I", not "we". If a team did it, say which part was yours.
- Result: a number, if possible (time, money, errors, users). Then what you learned or would change.
Rules that help:
- Prepare five stories and reuse them. One story can answer many questions: for example "the checkout outage" can be a hard bug, an outage, a mistake and a lesson about monitoring.
- Front-load the result. Start with "We cut the dashboard load time from 6 s to 1.2 s" and then explain how. If the interviewer interrupts, they already heard the result.
- Be honest. Do not invent numbers. If you do not remember, say "about" and give a range. A small true story beats a big false one, and interviewers ask follow-up questions until the false one breaks.
- Say what went wrong. A story with no mistake sounds untrue. Say your mistake in one clear sentence, then what you changed.
- Keep it under two minutes, then stop and let them ask.
Do not blame a person ("the backend developer was lazy"). Describe the situation and your actions. Also do not name your current employer's secrets or customers. Say "a payments service" or "an internal admin tool".
The nine classic questions, with example answers
The model answers are in the Interview questions section below. They are examples: generic, realistic and full-stack flavoured. Use them to learn the shape, then replace the details with your real story. These are the nine:
- A hard bug you solved.
- An outage or production incident.
- A technical trade-off you made.
- A disagreement with a teammate.
- Mentoring someone.
- An estimate that was wrong (or how you estimate).
- A time you said no.
- Tech debt.
- Improving performance, with numbers.
Live coding and take-home tasks for full-stack roles
Live coding means you write code while someone watches, usually for 45 to 60 minutes. A take-home is a small project you do alone at home, usually for two to four hours, and send back.
Live coding tips
- Repeat the problem and ask 2 or 3 questions before you type. "What is the input size? What should happen on bad input? Do you want it to work, or to be production-ready?" Not asking questions is the most common pitfall.
- Say your plan in one minute, then start with the simplest version that works. Make it correct, then make it clean, then fast if needed.
- Think aloud. The interviewer cannot see your thoughts. Say "I will use a Map here because I need lookup by id".
- Run the code early and often. A small working piece beats a big broken one.
- Test with your own examples, including an empty input and a bad input. Say the cases out loud.
- If you are stuck, say what you tried and what you think the cause is. Ask for a hint without shame. A hint costs less than silence.
- Know your tools: your editor, the test runner, how to read a stack trace. For full-stack tasks, the usual shapes are: build a small REST endpoint with validation, then a React component that uses it with loading and error states.
Take-home tips
- Read the brief twice and write down what is out of scope. Send one short clarifying email if something is truly unclear.
- Do less, but finish it. A small app that runs with one command beats a big one with missing parts. Add a short README: how to run, what you built, what you skipped on purpose, and what you would do next.
- Show the basics: input validation, error handling, a few meaningful tests (not 100 shallow ones), types, clear commit messages, no secrets in the repo.
- Do not over-engineer. Microservices, a message bus and three state libraries for a to-do list make reviewers worry about you, not admire you.
- Keep to the time limit they gave. If you spent more, say so. Mention trade-offs in the README, because the follow-up interview will often be a code walk-through of your own task.
System-design-lite prompts for 4+ years
Full system design (millions of users, sharding) is for senior roles. At 4+ years you get "system-design-lite": one feature, one team, a normal database, with a few thousand to a few hundred thousand users. They want a clear structure and good questions, not a perfect diagram. See also the frame in How to Run a Practical Design Interview.
Use this order:
- Clarify (users, the main flows, size, what is out of scope).
- Data model (tables or collections, main fields, indexes).
- API (endpoints, request and response, errors, pagination).
- Main flow (what happens on the server and in the UI).
- Problems at scale and failure (slow queries, duplicates, retries, permissions).
- Trade-offs and what you would do first.
Prompt A: a dashboard with filters and CSV/Excel export
- Clarify: how many rows (thousands or millions)? How fresh must the data be? Who may see which rows? Is the export small enough to download at once?
- Data and API:
GET /reports?from&to&status&cursor&limitwith server-side filtering and paging (see Cursor pagination). Put indexes on the columns used in filters. Put filters in the URL so links can be shared and the back button works. - UI: debounce text filters, cancel older requests, keep the previous data visible while loading (no flicker), show an empty state and an error state.
- Export: small (under a few thousand rows) can be a streamed response. Large exports are a background job:
POST /exportsreturns202with an id, a worker builds the file and stores it, the UI polls or gets a notification, and the user downloads from a time-limited link. Stream the rows to the file instead of loading all into memory. - Gotchas: check permissions in the export too (a common leak), escape cells that begin with
=,+,-or@(CSV formula injection), keep the same filters in the export as on screen, and use the same query code for both.
Prompt B: a comments system (with replies)
- Clarify: only top-level comments, or replies? Edit and delete? Moderation? Mentions? Real-time updates?
- Data:
comments(id, postId, parentId nullable, authorId, body, createdAt, deletedAt). Index(postId, createdAt). Limit nesting to one or two levels in the UI; deep trees are hard to read and to query. - API:
GET /posts/:id/comments?cursorreturns top-level comments with a few replies, andGET /comments/:id/replies?cursorloads more.POSTneeds authentication, a length limit and rate limiting. Use soft delete (keep the row, hide the body) so replies stay attached. - Safety: treat the body as text and escape it on output; if you allow markdown, sanitize the HTML. Add spam control (rate limits, links limit, report button).
- Freshness: start with refetch on focus or a manual refresh; add SSE for live updates only if needed. Optimistic UI for the author's own comment.
- Counts: keep a
commentCounton the post or compute it with a cached query; do notCOUNT(*)on every page view.
Prompt C: notifications
Details are in Design a Notification System. The short structure: an event happens (someone replied), a notification record is created for each recipient (userId, type, payload, readAt, createdAt), and delivery channels (in-app, email, push) are separate jobs on a queue with retries. The user can set preferences per channel. In-app: a bell with an unread count (GET /notifications?cursor, POST /notifications/read), updated by polling or SSE. Watch out for: duplicates (use a unique key such as type + entity + recipient), spam (group "5 new comments"), and sending email inside the HTTP request, which makes the request slow and unreliable.
Prompt D: a file import (CSV with 100,000 rows)
- Clarify: file size, columns, what to do with bad rows (reject all, or import good ones and report bad ones), can the user import the same file twice?
- Flow: upload the file straight to object storage with a pre-signed URL (Design File Upload), then
POST /importswith the file key. A worker streams the file, validates each row with a schema, and writes in batches (for example 500 rows per insert) inside short transactions. - Idempotency: use a natural key (email, SKU) with an
upsert, so a retry or a second upload does not create duplicates. - Feedback: an
importsrecord withstatus,total,processed,failed; the UI shows progress; a downloadable error report lists row number and reason. - Failure: the worker can crash midway, so save progress (last processed row) and make the job resumable. Limit file size and rows. Treat the file as untrusted (formula injection, encoding, huge cells).
Code-review exercise
Many companies show you a short, flawed snippet and ask "review this like a pull request". They want to see a sensible order (security and correctness first, then performance, then style), clear comments with reasons, and kindness. Try to find the problems yourself before you read the list.
// server.jsconst express = require('express')const jwt = require('jsonwebtoken')const cors = require('cors')const app = express()app.use(cors())app.use(express.json())app.post('/api/login', async (req, res) => { const user = await User.findOne({ email: req.body.email, password: req.body.password }) const token = jwt.sign({ id: user._id }, 'secret123') res.json({ token, user })})app.get('/api/orders', async (req, res) => { const orders = await Order.find({ userId: req.query.userId }) for (const o of orders) { o.customer = await User.findById(o.customerId) } res.json(orders)})app.put('/api/orders/:id', async (req, res) => { await Order.findByIdAndUpdate(req.params.id, req.body) res.send('ok')})app.listen(3000)// Orders.jsxfunction Orders({ userId }) { const [orders, setOrders] = useState([]) useEffect(() => { fetch(`/api/orders?userId=${userId}`) .then(r => r.json()) .then(setOrders) }) return ( <div> {orders.map((o, i) => ( <div key={i} dangerouslySetInnerHTML={{ __html: o.note }} /> ))} </div> )}What to point out, in a good order
Blockers (must fix before merge):
- Passwords compared in plain text in the login query (
password: req.body.password). Passwords must be stored as a slow hash (bcrypt, scrypt or argon2) and compared with the library's compare function. - Query injection risk. With MongoDB, a body like
{"email":"a@b.co","password":{"$ne":null}}is an operator, not a string, and can match any password. Validate that the fields are strings (a schema such as Zod) before querying. See Validation at the Edge. - Wrong login crashes. If no user matches,
userisnullanduser._idthrows. I ran the same shape on Express 5: the client gets a500HTML page that includes the stack trace outside production. It should be401with a generic message, the same for "no such user" and "wrong password". - Hard-coded secret and no expiry in
jwt.sign(..., 'secret123'). Read the secret from the environment, and setexpiresIn(short, for example 15 minutes). Also the token is never checked anywhere: there is no auth middleware at all. - The user object is sent back, probably including the password hash. Return only the fields the client needs.
- IDOR (insecure direct object reference):
GET /api/orders?userId=...trusts the id from the client, so anyone can read anyone's orders. Take the user id from the verified token. PUThas no ownership check and allows mass assignment. Anyone can edit any order, andreq.bodycan setuserId,totalorstatus. Check that the order belongs to the caller (403otherwise), and accept only a whitelist of fields.- React:
useEffectwithout a dependency array runs after every render, and the fetch sets state, which renders again, so it loops forever. I tested the same shape: more than ten requests in two seconds. Add[userId]. dangerouslySetInnerHTMLwith server data is stored XSS. Render{o.note}as text, or sanitize the HTML.
Should fix:
- N+1 queries in the loop: one extra query per order. Fetch all customers in one query (
$in,include, or a lookup). See Prisma in Production. - No pagination or limit on the list. No validation on
paramsandquery. No404when the order does not exist. cors()with no options allows every origin. Use an allow-list.- No error handling in the React fetch: no
r.okcheck, no loading state, no error state, and no abort of old requests (a race condition whenuserIdchanges). key={i}(index as key) causes wrong updates when the list changes. Useo.id.
Nice to have: res.send('ok') should be JSON with the updated order, use async error handling (Express 5 forwards rejected promises to the error handler, Express 4 does not), add tests for the permission rules, and log with a request id.
I wrote a fixed version of the server and ran it with in-memory fakes. A wrong password gave 401; an operator object as the password gave 400; no token gave 401; the list used the id from the token and loaded customers in one query; a body with userId and total gave 400; a valid { "status": "paid" } gave 200; and another user's token gave 403. The key pieces:
const Login = z.object({ email: z.email(), password: z.string().min(1) })app.post('/api/login', async (req, res) => { const body = Login.safeParse(req.body) if (!body.success) return res.status(400).json({ error: 'invalid_input' }) const user = await db.userByEmail(body.data.email) const ok = user && (await bcrypt.compare(body.data.password, user.passwordHash)) if (!user || !ok) return res.status(401).json({ error: 'invalid_credentials' }) const token = jwt.sign({ sub: user.id }, JWT_SECRET, { expiresIn: '15m' }) res.json({ token, user: { id: user.id, name: user.name } })})const Patch = z.object({ status: z.enum(['new', 'paid', 'shipped']) }).strict()app.put('/api/orders/:id', auth, async (req, res) => { const order = await db.order(String(req.params.id)) if (!order) return res.status(404).json({ error: 'not_found' }) if (order.userId !== (req as any).userId) return res.status(403).json({ error: 'forbidden' }) const body = Patch.safeParse(req.body) if (!body.success) return res.status(400).json({ error: 'invalid_input' }) Object.assign(order, body.data) res.json(order)})useEffect(() => { const ctrl = new AbortController() fetch('/api/orders', { headers: { authorization: `Bearer ${token}` }, signal: ctrl.signal }) .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json() as Promise<Order[]> }) .then(setOrders) .catch(e => { if (e.name !== 'AbortError') setError(e.message) }) return () => ctrl.abort() // cancel the old request when token changes or the component unmounts}, [token])How to give review comments: say what, why, and a suggestion. Label the weight ("blocker", "suggestion", "nit"). Praise one good thing. Ask a question when you are unsure ("is userId verified somewhere else?"). Never write "this is wrong" about a person; write about the code.
Pitfalls that cost offers
- Over-engineering. Proposing queues, caches and microservices for a problem with 500 users. Start simple and say when you would add more ("if exports grow past 50,000 rows, I would move them to a job").
- Not asking questions. Jumping into code or a diagram without clarifying. Ask two or three, state your assumptions, and move on.
- Saying "we" for everything, so the interviewer cannot see your part.
- No numbers. "It was faster" is weak. "p95 went from 2.4 s to 600 ms" is strong.
- Blaming others, or no mistakes in any story.
- Rambling. Stories over three minutes. Practise with a timer.
- Knowing tools but not reasons. "I used Redis" with no answer to "why, and what if it was down?".
- Arguing with the interviewer. Disagree with facts and calm; accept a hint.
- Hiding what you do not know. Say "I have not used that; I would approach it like this" and show your thinking.
Questions to ask the interviewer
At the end you are almost always asked "do you have questions?". Having none looks like you are not interested. Good questions show what you care about, and they help you learn if the job is good. Pick four or five:
- What is the biggest technical challenge the team faces now?
- What was the worst production incident in the last year, and what changed afterwards?
- What is the most expensive early technical decision you are still living with?
- How does the team balance new features and maintenance (tech debt)?
- What would you want the person in this role to solve in the first three months?
- How do code review, testing and deployment work here? How often do you deploy?
- How are on-call and incidents handled? Is there a post-incident review without blame?
- What does growth look like here, and how is performance judged?
- Who would I work with most closely, and how do product and design work with engineers?
Level and salary conversation basics
Keep this short and calm.
- Know the level before you talk about money. A title like "Software Engineer II" or "Senior" means different things at different companies. Ask how the company defines the level and what scope it expects. Many companies call roughly 2 to 5 years "mid" and 5+ years "senior", but the scope matters more than the years.
- Know the market. Look at pay ranges for your city, level and stack (public salary data, job posts that list a range, people you know). A range is more useful than one number.
- Do not give your current salary first. Say "I am looking for a role in the range of X to Y for this scope". If the law or the form asks for it, answer honestly, but you can say what you are targeting.
- Think in total pay: base, bonus, equity, benefits, leave, learning budget, remote policy, notice period. Get the offer in writing.
- Negotiate once, with reasons, after you have an offer: your scope, market range and other options. Be polite and keep the door open. A "no" on one item can become a "yes" on another (sign-on bonus, review date, level).
- Do not accept or refuse in the call. Say thanks and ask for a day or two.
Interview questions
The answers are examples written in the first person. The numbers are made up to show the shape. Replace them with your own true story.
What do interviewers look for at 4+ years, compared with junior and senior? (Must know)
At 4+ years I am expected to own a feature from idea to production without close help. That means asking good questions when requirements are vague, choosing a design and knowing what I gave up, finding bugs by measuring, reviewing other people's code and helping juniors. A junior is judged on learning and finishing given tasks. A senior is judged on scope: several people, several months, or a technical direction, plus how well they bring others along. So my stories should be about something I owned, that affected my team and users, and that has a number.
Tell me about a hard bug you solved. (Must know, STAR)
Situation: after a release, about 2 percent of customers saw an order that was saved twice, in a Next.js app with an Express API. Task: I was the engineer on call, so finding the cause was mine. Action: I did not guess. I counted duplicates by time and found they came in pairs within 300 ms. I added a request id to the logs and saw the same POST /orders arriving twice from the same browser. In the code the submit button had no disabled state, and on slow networks users clicked again; also a retry in our HTTP wrapper re-sent POST after a timeout. I reproduced it with the browser's network throttling. The fix had three parts: disable the button while pending, remove automatic retry for POST, and add an idempotency key with a unique index on the server so a repeat returns the first result. Result: duplicates dropped from about 40 a week to zero over the next two months. Lesson: I now treat every POST that creates money or records as needing an idempotency key from day one, and I added a log field that shows retries.
Tell me about an outage or production incident. (Must know, STAR)
Situation: on a Monday morning the API returned errors for about 25 minutes. Task: I was the on-call engineer and I owned the incident until we recovered. Action: first I checked the dashboard: error rate up, database connections at the limit. I told the team in the incident channel what I saw, so nobody worked blind. A deploy had gone out 10 minutes before; it added a report query without an index that held connections open. I chose the safe action first, a rollback, not a fix on the spot. Errors fell within 3 minutes. Afterwards I added the index, wrote a test that ran the query on a large dataset, and wrote a short post-incident note without blaming anyone. Result: recovery in 25 minutes; I added an alert on connection-pool usage and a check in review for new queries on big tables. Lesson: stop the damage first, find the cause second, and make the same failure harder next time. See Production Incidents.
Tell me about a technical trade-off you made. (Must know, STAR)
Situation: we had to build in-app notifications in three weeks. Task: choose how to deliver them. Action: I compared WebSockets, server-sent events and polling every 30 seconds. WebSockets needed sticky sessions and a pub/sub layer we did not have. Polling was simple but wasteful at scale. I proposed polling first, behind one small client function, with a plan to switch to SSE later. I wrote down what we gave up: up to 30 seconds of delay and more requests. I measured: 2,000 active users meant about 70 requests per second, easy for the API with a cheap indexed query. Result: we shipped on time. Six months later, with usage up five times, we swapped to SSE by changing only the client function. Lesson: pick the simplest option that meets today's need, isolate it so you can change it, and write the trade-off down so others understand it.
Tell me about a disagreement with a teammate. (Must know, STAR)
Situation: a teammate wanted to add a global state library for a form wizard; I thought plain React state and a URL parameter were enough. Task: reach a decision without hurting the relationship. Action: I asked them to explain the problem they were trying to avoid, and it was prop drilling across four components. I did not argue taste. I proposed a small spike: each of us would build the same step in an hour and we would compare lines of code and how easy it was to test. We also asked a third teammate to review. The simple version was 40 lines shorter and needed no new dependency; but I accepted their point that one shared hook was cleaner than passing five props, so we used a small context for the wizard only. Result: we decided in a day, shipped on time, and the teammate later asked me to review their design docs. Lesson: turn opinions into a small experiment, and be ready to take the good part of the other idea. More in Debugging, Lesson 14.
Tell me about a time you mentored someone. (Senior signal, STAR)
Situation: a junior developer on my team took long on tasks and their pull requests had many review comments. Task: help them become independent, without doing their work. Action: I set up a weekly 30-minute session. First I asked what they found hardest; it was debugging. I showed my method on a real bug: reproduce, narrow down, form a guess, test it. I paired on their next bug but let them type. I changed my review comments from "change this" to "why do you think this is safe?" and I wrote down repeated issues as a short checklist. Result: after three months their pull requests needed about half the review rounds, and they took a small feature alone. Lesson: teach the method, not the answer, and measure by how much less help they need.
How do you estimate work, and tell me about an estimate that was wrong. (Senior signal, STAR)
Situation: I estimated a "simple" CSV import at 3 days. Task: deliver it and manage expectations. Action: it took 8. I had not asked about file size, bad rows or duplicates. The real files had 200,000 rows and mixed encodings, so I had to stream, batch and add an error report. When I saw it would slip (on day 3, not day 8), I told the product manager the new range and the reasons, and offered a smaller first version: import with a basic error list now, progress bar later. Result: the first version shipped on day 6, and the rest followed. Lesson: now I break work into pieces, ask about the unknowns first, give a range (best case, likely, worst case), add time for review, testing and deployment, and update the estimate early when facts change. An estimate is a forecast, not a promise, and bad news gets worse when it is late.
Tell me about a time you said no. (Senior signal, STAR)
Situation: a stakeholder asked for a "real-time" analytics dashboard for the next sprint, with live updates every second. Task: protect the team's plan and still help. Action: I did not say a flat no. I asked what decision the dashboard supports. The answer was a daily review, so data that is a few minutes old was enough. I offered an option: refresh every 5 minutes from a cached query this sprint, which was two days of work, with a live version later only if the need was proven. I explained the cost of "every second": a new streaming path, load on the database, and about three weeks. Result: they accepted the 5-minute version, and after a month nobody asked for live updates. Lesson: "no" works best as "not that, but this", based on the real goal, with the cost made visible.
How do you handle technical debt? (Senior signal, STAR)
Situation: our order service had a 700-line function that every change broke. Task: reduce the risk without stopping feature work. Action: I did not ask for a "rewrite month". I measured: it caused 5 of the last 12 bugs. I proposed to pay the debt in small steps next to feature work: first add tests around the current behaviour, then extract one piece per pull request, always behind passing tests. I put the work in the sprint as visible tickets and linked each to the bugs it would prevent. Result: over 6 weeks the function shrank to four smaller modules, and bugs from that area fell from about 2 a month to 0 or 1. Lesson: debt is a business risk, so describe it in cost (bugs, time, risk), fix it in small safe steps, and do not hide it.
Tell me about a time you improved performance, with numbers. (Must know, STAR)
Situation: the orders page took about 6 seconds to load for large customers. Task: bring it under 2 seconds. Action: I measured first. The network panel showed the API took 4.8 s, so the problem was the server, not React. The logs showed 1 query for orders and then one query per order for customer data (an N+1 problem), with about 200 orders per page. I replaced the loop with one query using include, added a composite index on (userId, createdAt) after reading EXPLAIN, and moved from offset to cursor pagination with a page size of 50. On the frontend I stopped re-rendering the whole table on every filter change. Result: API time went from 4.8 s to 0.4 s (p95), page load from 6 s to 1.2 s, and database CPU fell by about a third. Lesson: measure, find the biggest cost, fix one thing at a time, and re-measure. See The API is Fast but the UI is Slow and Prisma in Production.
Design a dashboard with filters and CSV export. How do you structure the answer? (Senior signal)
I ask first: how many rows, how fresh must the data be, who may see which rows, and how large can an export be? Then I give the structure. Server-side filtering and cursor paging with indexes on the filter columns; filters in the URL; debounced inputs and cancelled old requests; loading, empty and error states. For export: small ones stream directly, large ones become a background job that returns 202 and an id, a worker streams rows to a file in storage, and the user gets a time-limited link. I mention the traps: permission checks inside the export, CSV formula injection, and using the same query code on screen and in the export. If asked about scale, I would add a read replica or pre-computed summary tables before anything fancy.
Design a comments system with replies. What are the key decisions? (Senior signal)
A comments table with postId, nullable parentId, authorId, body, createdAt and deletedAt, indexed on (postId, createdAt). I limit replies to one or two levels because deep trees are hard to read and to query. The API uses cursor pagination for top-level comments and a separate endpoint for more replies. Writes need authentication, length limits and rate limits. Delete is a soft delete so replies stay in place. Bodies are escaped on output, or sanitized if markdown is allowed. I keep a commentCount on the post instead of counting on every view. Live updates: refetch on focus first, SSE later if the product needs it.
Design a CSV import for 100,000 rows. What can go wrong? (Senior signal)
The browser uploads to object storage with a pre-signed URL, then calls POST /imports. A worker streams the file, validates every row with a schema, and inserts in batches of a few hundred rows. It uses upsert on a natural key so a retry or second upload does not create duplicates. An imports record stores status and counts, the UI shows progress, and a downloadable report lists failed rows with the reason. What can go wrong: the worker crashes midway (save progress and make the job resumable), huge or malicious files (limit size and rows, treat contents as untrusted), bad encodings, one bad row blocking everything (collect errors, continue), and slow imports locking tables (short transactions).
Review this Express and React code. What do you say first? (Must know)
I start with security and correctness, then performance, then style. First: plain-text password comparison and the injection risk in the login query; a null user crashing the route; a hard-coded secret and no token expiry; no auth check anywhere; sending the whole user object back; the userId taken from the query (IDOR); PUT with no ownership check and mass assignment from req.body. In React: useEffect with no dependency array loops forever, and dangerouslySetInnerHTML is stored XSS. Second: N+1 queries, no pagination, open CORS, no r.ok check or error state, and key={i}. I give each comment a reason and a suggestion, mark blockers versus nits, and say something positive about the structure. Then I show the safest first fix: derive the user id from the verified token, because that closes the biggest hole.
You get a take-home task with a four-hour limit. How do you approach it? (Gotcha)
I read the brief twice and list what is out of scope. If something blocks me I send one short question. I build the smallest version that runs end to end first, then add validation, error handling and a few tests that check real behaviour. I keep the setup to one command and write a short README: how to run it, what I built, what I skipped on purpose and what I would do next. I resist adding extra tech. I stop at the time limit and say if I went over. The trap is trying to impress with size. Reviewers prefer a small, clear, working solution and a README that shows I know its limits.
What do you ask the interviewer, and how do you talk about pay and level? (Gotcha)
I ask about real problems: the biggest technical challenge, the last serious incident and what changed after it, how the team balances features and tech debt, how deployment and code review work, and what success looks like in the first three months. On level and pay: I first learn how the company defines the level and its scope. I look at market ranges for my city, level and stack. I do not give my current salary first; I say the range I am targeting. I look at total pay, not just base. I negotiate once, politely, after I have an offer in writing, using scope and market data as reasons, and I ask for a day to decide.
Key takeaways
- At 4+ years you are judged on ownership, judgement, debugging, communication and collaboration, with stories about a feature you owned and a number to prove it.
- Use STAR: situation, task, action (about half), result with a number and a lesson. Say "I", be honest, and keep it under two minutes. Prepare five reusable stories.
- In live coding: ask questions first, think aloud, run early, test your own cases. In a take-home: finish a small thing well, with a README.
- For system-design-lite use one order: clarify, data, API, main flow, scale and failure, trade-offs. Keep it simple and say when you would add more.
- In a code review put security and correctness first (auth, injection, IDOR, mass assignment, XSS), then performance (N+1, pagination), then style. Explain every comment and label its weight.
- Avoid over-engineering, silence instead of questions, "we" for everything and blame.
- Ask good questions at the end. For pay: know the level, know the range, think in total pay, negotiate once, and get it in writing.
What's next
You have reached the end of the Full-Stack Interview Questions series. Go back to Lesson 0 for the map of all topics and mark the areas where your stories are weakest. Then practise out loud: five stories, two system-design-lite prompts and one code review, each with a timer.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Part of the study guide
Related series