Interview bookGroundworkInterview book
6 / 27 written chapters

R2 · Part 2 · The technical rounds

Machine coding

You get a small product spec and a laptop. There is no algorithm in this round — the entire test is whether code you write under pressure looks like code from someone who has shipped. Your background should make this your strongest round, and it is where unprepared candidates with better resumes lose.

6 questions18 follow-ups5 traps8 min read
Length
90–120 min
Who
Senior engineer, often silent
Decides
The startup loop, almost entirely
Fail mode
One giant file, nothing running

Everything open, top to bottom.

07On this page
  1. Build a task board with drag-and-drop between three columns, persisted locally.
  2. Build a searchable, filterable, paginated data table over a mock API.
  3. Build a multi-step form with per-step validation and a review step.
  4. Build a booking or calendar slot picker with conflict detection.
  5. Backend variant — build a small REST API in NestJS.
  6. What do you say out loud while you are coding?
  7. The file skeleton to have in your fingers
WeightWhat is scoredWhat it means in the room
30%It worksThe happy path runs end to end. A beautiful non-working submission scores below an ugly working one, every time.
25%Separation of concernsComponents, hooks, services, types in separate files with clear boundaries. No business logic inside a JSX return.
20%ExtensibilityThey will say "now add X" in the last ten minutes. If that takes thirty seconds, you pass.
15%States & edgesLoading, empty, error, disabled-while-submitting. Most candidates ship only the success state.
10%Naming & typesNo any. No data2. Types that describe the domain, not the shape of a fetch response.
The first ten minutes

Do not open the editor. Spend six to eight minutes out loud: restate the requirements in your own words, ask two clarifying questions, say what you will build and what you are explicitly leaving out, then sketch the file structure. Interviewers score this. Candidates who start typing at second thirty almost always finish with a mess.

R2.1

Build a task board with drag-and-drop between three columns, persisted locally.

What they are really testingData modelling. The drag is theatre; the state shape is the exam.

Skip a drag library unless it is explicitly allowed — the HTML5 dragstart / dragover / drop trio is enough and shows you know the platform. The decision they are watching for is normalised state:

type Task = { id: string; title: string; createdAt: number }

type Board = {
  tasks: Record<string, Task>          // id → task, one source of truth
  columns: Record<string, string[]>    // columnId → ordered ids
  order: string[]                      // column order
}

// moving a card is two array operations, not a tree walk
function move(b: Board, id: string, from: string, to: string, at: number): Board {
  const src = b.columns[from].filter(x => x !== id)
  const dst = from === to ? src : [...b.columns[to]]
  dst.splice(at, 0, id)
  return { ...b, columns: { ...b.columns, [from]: src, [to]: dst } }
}
The answer that loses the room

Opening a drag-and-drop library's docs in minute two. You spend twenty minutes learning its API instead of showing your data model, and when it misbehaves you have nothing working and nothing of your own to talk about. Build the drag on the platform events unless they tell you otherwise.

The same round at three levels
2–3 years

The bar: something that works by the end. Correctness and finishing beat structure.

“Before I start — I will keep tasks flat, keyed by id, each with a columnId and an order, because then moving a card is one write instead of splicing between two arrays. I will get one card dragging between two columns first, then persistence, then the empty state. One file for now; I would split it into components if this were real.”

Why it passes here: Nobody is grading architecture at two years — they are grading whether you freeze. A working single-file board beats an elegant unfinished one every time. Saying “I would split this if it were real” once buys you the structural credit without costing you minutes.

5–7 years

The bar: five minutes on types and file boundaries first, then the happy path end to end, then states and edges — and something that runs at every point in the ninety minutes.

“Let me get the data model down before I touch the drag logic, because the drag logic falls out of it. Tasks flat in a record keyed by id, each with columnId and a fractional order so a reorder is one field and not a re-index of the column. Columns as an array of ids. That makes a move a single update and makes ‘which column is this in’ O(1) instead of a scan. Then: drag between two columns, persistence, and the states — empty column, drag cancelled, reload.”

Why it lands: The flat-versus-nested decision is this round. Nested arrays of task objects force a deep clone on every drag and turn “which column is this card in” into an O(n·m) search. Choosing it is worth something; saying out loud why you chose it is worth more, because the interviewer is writing down your reasoning, not your code.

10+ years

The bar: state what you will not build, design the seam the next feature needs, and keep the last ten minutes free.

“In ninety minutes I am going to build the board, the drag, and local persistence. I am not going to build auth, multiple boards, or server sync — but I will put persistence behind a four-method interface so swapping localStorage for an API is one file and no component changes. If I run short, the thing I cut is animation, not the empty and error states. Tell me if you would rather I traded that differently.”

Why this is the level above: The last ten minutes of a machine-coding round are almost always “now add X” — undo, a second tab, reload without losing order. The candidate who designed a seam adds it in four minutes; the one who did not starts rewriting and runs out of clock. Naming the cut list up front is a scope negotiation with your interviewer, which is the actual job at this level.

Nested arrays of task objects force a deep clone on every drag and make "which column is this card in" an O(n·m) search. Say that out loud as you choose — the reasoning scores higher than the result.

They will push with
  1. Now persist it and reload without losing order.
  2. Add an undo.
  3. What happens if two tabs are open?
How did you do?
R2.2

Build a searchable, filterable, paginated data table over a mock API.

What they are really testingWhether three interacting features fight each other. Most candidates build them independently and produce bugs.

Derive the visible rows in one pipeline, memoised, and reset the page index whenever the filter or query changes. That reset is the bug they are looking for: filter down to three results while on page 4 and an unprepared implementation renders an empty table.

const rows = useMemo(() => {
  const q = query.trim().toLowerCase()
  return data
    .filter(r => !q || r.name.toLowerCase().includes(q))
    .filter(r => !status || r.status === status)
    .sort(BY[sortKey])
}, [data, query, status, sortKey])

const pageRows = rows.slice(page * SIZE, page * SIZE + SIZE)

useEffect(() => { setPage(0) }, [query, status])  // the bit everyone forgets
The answer that loses the room

Three pieces of state and three effects — one fetching when the query changes, one filtering, one paginating — each writing state the others read. It renders several times per keystroke, it flickers, and the empty-page bug appears anyway. Derive the visible rows; do not sync them.

Debounce the search input at ~300ms and say why. Put the fetch in a custom hook (useTasks), never inline in the component body — that single move is most of the "separation of concerns" score.

They will push with
  1. Now make sorting server-side.
  2. The dataset is 50,000 rows — what changes?
  3. Make the filter state survive a page refresh.
How did you do?
R2.3

Build a multi-step form with per-step validation and a review step.

What they are really testingExtensibility. The follow-up is always "add a fourth step" and they are timing you.

Drive the steps from a config array, never a switch. Adding a step then means adding one object.

const STEPS = [
  { id: 'account', title: 'Account', fields: ['email','password'], schema: accountSchema },
  { id: 'profile', title: 'Profile', fields: ['name','phone'],     schema: profileSchema },
  { id: 'review',  title: 'Review',  fields: [],                   schema: null },
]

const step = STEPS[i]
const errors = step.schema ? validate(step.schema, form) : {}
const canNext = Object.keys(errors).length === 0
The answer that loses the room

A switch over the step index with its own useState inside each step component. It works for three steps, loses the user's input when they press back, and turns "add a fourth step" into a change in four places while the interviewer watches the clock.

Keep every step's data in one parent object rather than per-step state, so the review step is a read and going back does not lose input. Mention Zod and derive the TypeScript types from the schema with z.infer, so the runtime check and the compile-time type cannot drift apart.

They will push with
  1. Add a fourth step.
  2. Persist a half-finished form and resume it.
  3. The last step submits — handle the failure.
How did you do?
R2.4

Build a booking or calendar slot picker with conflict detection.

What they are really testingInterval logic and timezones. If a booking product is on your resume, expect it.

If you have shipped a booking product, say so up front — it buys you credibility for the whole round. Then model slots as half-open intervals [start, end), which makes back-to-back slots not overlap, and write the predicate as a named pure function:

// half-open: [aStart, aEnd) and [bStart, bEnd)
const overlaps = (a: Slot, b: Slot) =>
  a.start < b.end && b.start < a.end

// 10:00–11:00 and 11:00–12:00 → false. Correct.
// Using <= here is the classic off-by-one that breaks adjacency.
The answer that loses the room

Storing slots as local-time strings such as "2026-10-04 10:00". They compare correctly until the first user in another timezone or the first daylight-saving change abroad, and then conflicts are detected wrongly in both directions.

Keep everything in UTC internally and format only at the edge. If they ask about recurring slots, the answer is: store the rule, expand to concrete instances for the visible window, and store exceptions separately — never store a thousand rows for a weekly repeat.

They will push with
  1. Two people book the last seat at the same moment.
  2. Handle a user in a different timezone.
  3. Now support recurring weekly slots.
How did you do?
R2.5

Backend variant — build a small REST API in NestJS.

What they are really testingIf your resume claims clean layering, whether it is real when you have to type it in ninety minutes.

Given your stack this may replace the frontend problem. Scaffold with the CLI, then make the layering visible: controller does HTTP only, service holds logic, repository touches data, DTOs validate at the boundary, one module per domain, a global exception filter.

src/
  bookings/
    bookings.controller.ts   // HTTP only — no logic
    bookings.service.ts      // the actual rules
    bookings.repository.ts   // data access
    dto/create-booking.dto.ts
    entities/booking.entity.ts
    bookings.module.ts
  common/
    filters/http-exception.filter.ts
    interceptors/logging.interceptor.ts
The answer that loses the room

Injecting the repository straight into the controller and writing the rules there because it is quicker to type. It runs, and it throws away the one thing this variant is scoring — that you keep HTTP, rules and data access apart when the clock is running.

They will push with
  1. Add pagination.
  2. Make POST idempotent.
  3. Where would you put a transaction?
How did you do?
R2.6

What do you say out loud while you are coding?

What they are really testingNothing is being asked here — but the interviewer is scoring your narration, and silence scores zero.

  • "I am building the data model first, because everything else falls out of it."
  • "I am storing this normalised so a reorder is a splice rather than a deep clone."
  • "This goes in a hook so the component stays presentational and the logic is testable on its own."
  • "Debouncing here at 300ms — without it we fire a request per keystroke."
  • "I am doing the empty state now rather than at the end, because it is the state people actually see first."
  • "I would use Zod here in real code; for time I am hand-rolling it, but I would not ship this."
  • "This is the seam where optimistic updates would go."
  • "If this list went past a few thousand rows I would virtualise it. I am not doing that now because it is not the bottleneck at this size."
  • "That is the happy path working. With the remaining twenty minutes I will add the error state, then tidy naming."

The last one matters most. Announcing your plan for the time you have left is the single clearest signal of a senior candidate rather than a fast one.

R2.7

The file skeleton to have in your fingers

What they are really testingSpeed. If you have to invent a structure under time pressure you will invent a bad one.

Rehearse this until you can produce it without thinking:

src/
  components/        // presentational only: props in, events out
  features/<name>/   // the feature: its hooks, components, types
  hooks/             // useDebounce, useLocalStorage, useFetch
  lib/               // api client, formatters, pure helpers
  types/             // domain types

Two rules that survive contact with a timer: a component that fetches is a feature, not a component; and anything you would unit-test goes in lib/ or a hook, never inside JSX.

They will push with
  1. Where does the API client live, and why there?
  2. This component fetches its own data. Is it still a component?
  3. Where would the test for this hook go?
How did you do?
Practise this round on the site
  • Patterns & architecture — the separation this round scores 25% on
  • Testing — for the last ten minutes, when they ask you to prove it works
  • The playground — build a small CRUD feature in ninety minutes until it is boring

Finished this round?

0 of 27 rounds read in this book