Hiring for this role?
Start free with this planFree for your first open role.
Why Hirezen?
- Every interviewer runs the same script and marks the same signals.
- AI drafts the write-ups, and the debrief puts every read side by side.
- No ATS to set up first, and no bot in the call.
Backend Developer interview questionsTechnical Interview round
A 60 min interview plan with a time-boxed script, what each question is for, and the signals to score against. Key skills: Production debugging, data integrity under concurrency and multi-tenant authorisation, on one inherited invoicing API: a 9-second list endpoint, a 09:00 outage, a changes feed that loses updates, and a customer reading another's invoices.
Opening
Who is interviewing, how the round will run, and a question to settle the candidate in. The standard openingNine seconds for a page of invoices
What this part is for
Purpose
Runs over a one-page brief shown when the round starts, never sent ahead: the candidate reads it for five minutes before the 60 starts and keeps it in view, and a second sheet comes out for the last topic. Write both to this composition so the numbers agree. The service is `invoices-api`, a B2B invoicing backend on Postgres 16. About 2,000 tenants; the median has under 5,000 invoices, and the largest, moved over five weeks ago, has 1.4 million, 120,000 of them issued and unpaid. `invoices` holds 38 million rows, indexed on its primary key, `tenant_id`, `customer_id` and `status`. Six API instances behind a load balancer serve about 300 requests a second at 09:00, most in under 20 ms. Each has a pool of 10 connections and waits up to 5 seconds for one before answering 503; middleware wraps every request in a transaction committed after the response is serialised; no statement timeout is set. The load balancer checks `GET /healthz` every 10 seconds with a 2-second timeout and takes an instance out after two failures; `/healthz` runs `SELECT 1` through the same pool. The database's `max_connections` is 100, and background jobs hold 20. Problem one: `GET /v1/invoices?status=issued&page=N&per_page=100` returns invoices newest first, each with its customer and lines, plus a `total`. A trace from 09:04 of page 1,200 for the largest tenant: waiting for a connection, 3.8 s; a `count(*)` with the same filter, 1.4 s; `SELECT * FROM invoices WHERE tenant_id = $1 AND status = $2 ORDER BY created_at DESC LIMIT 100 OFFSET 119900`, 3.6 s; 200 single-row queries for customers and lines, 0.4 s between them; serialisation, 0.2 s; 9.4 s in all. Problem two: every weekday for three weeks, from 09:00 for up to ten minutes, as many as 38% of requests to every endpoint get a 503 — `GET /v1/me`, which reads one row, included — database CPU sits at 100%, and at the worst minute two of the six instances are in rotation. The tenant's ERP connector starts a full sync at 09:00: 16 workers split the 1,200 pages, give up on a request after 10 seconds and retry it at once, up to five times. A colleague proposes 12 instances with pools of 20. Problem three: until three weeks ago the connector followed `GET /v1/invoices/changes?since=T`, which returns issued invoices whose `updated_at` is later than T, oldest first, 100 a page; the client sends the largest `updated_at` it has seen as its next `since`. The ORM sets `updated_at` from the server's clock when it saves a row. The vendor switched to the full crawl after the tenant found three kinds of change missing from its ERP: invoices paid in the app still unpaid there; late fees, which a nightly job adds to overdue invoices with one SQL `UPDATE`, never arriving; and a few edits lost at random — one a correction to invoice 48113207, saved at 10:00:00.2 by a request that committed at 10:00:06.1, while a sync ran from 10:00:03 to 10:00:05 and stored 10:00:04.8 as its next `since`. The second sheet is an email to support at 09:40 today from a customer's finance admin — "I changed the number in a PDF download link and got another company's invoice" — and the handler for `GET /v1/invoices/{id}/pdf`, added last year: it checks the API token, loads the row with `SELECT * FROM invoices WHERE id = $1`, and streams `pdfs/{id}.pdf` from object storage. Invoice ids are sequential integers; access logs record token id, path, status and bytes for 30 days. Hold four cards for when they are asked for: the page query run alone at 14:00 — a bitmap scan of the `tenant_id` index reading 1.38 million rows, a filter on status keeping about 120,000, a sort on `created_at`; 1.9 s at page 1,200, 1.7 s at page 1, the count alone 0.9 s; what reads `total` — only a progress line in the connector's log; the access logs joined to invoice owners — 340 PDFs downloaded across tenants in 30 days, 290 of them by one token in twenty minutes last Thursday, ids in sequence; and other routes that load by id alone — the invoice lines and the attachments. From a candidate who knows MySQL better, accept the equivalent mechanism and score the reasoning, not the syntax. Book 75 minutes: five to read before the 60, and the close after it.
I'm [YOUR_NAME], and I work on the backend at [COMPANY_NAME]. This page is a service you'd inherit in your first week, with three things going wrong in it; we'll take them in order and finish on something that came in this morning. Take five minutes with it. There's no code to write — I want to hear how you'd work on it, and if you need a fact that isn't on the page, ask; if we'd know it, I'll tell you.
What this line is for
Purpose
Frames the hour as an inheritance, not a quiz, and makes asking for evidence normal, so the candidate reasons from the page rather than listing what goes wrong with APIs.
Here is one request from that customer's 09:00 sync: page 1,200, 9.4 seconds. Tell me where the time goes, what you would change and in what order, and how you would know each change worked.
What this question is for, and what to listen for
Purpose
Production debugging, read on one request. The trace holds a textbook problem — 200 queries — worth 4% of the time; the read is whether the candidate adds the spans up before reaching for a fix they know, and asks for the plan before proposing an index.
Signals to score
- Adds the trace up and ranks it before proposing anything: pool wait 3.8 s, page query 3.6, count 1.4, lookups 0.4, serialisation 0.2
- Reads the pool wait as queueing behind other slow requests, not as a pool that is too small
- Asks for the plan, and infers from page 1 costing what page 1,200 does that every page reads all the tenant's rows and sorts the unpaid ones
- Proposes an index matching the filter and the sort — `tenant_id`, `status`, `created_at`, then `id` — and says why `id` is last
- Explains that OFFSET still reads every skipped row with that index, and adds a cursor on `created_at` and `id`, keeping `page`, capped, until the vendor moves
- Notices that invoices issued or paid mid-crawl shift the pages, so the connector misses some and reads others twice
- Asks who reads `total`, and drops it or makes it optional rather than counting 120,000 rows per page
- Names the 200 lookups as N+1 and batches them, while placing them at about 4% of the request
- Sees that the crawl exists because the changes feed was abandoned, so a feed the connector trusts removes most of the load
- Measures the fix at 09:00: the tenant's p99, the pool wait, and the statement's time in `pg_stat_statements`
Follow-up questions
- The trace has 200 queries in it. Is that your first fix?
- Run alone, page 1 takes about as long as page 1,200. What does that tell you?
- With your index in place, what does page 1,200 still cost?
- Invoices get paid while the connector is paging. What does it see?
- What does `total` cost, and who reads it?
Every endpoint down at 09:00
What this part is for
Purpose
Production debugging again, at the scale of the service: not why one request is slow but how one tenant's slow requests turn into every tenant's 503s, and what stops the next one.
From the numbers on the page, walk me through how one customer's sync takes every endpoint down, and what you would change so the next slow endpoint can only hurt its own callers.
What this question is for, and what to listen for
Purpose
Tests whether the candidate can explain a cascade from a pool, a health check, a retry policy and a connection limit, see that the service let one endpoint take everything, and contain it without buying capacity. Many fix the query and stop.
Signals to score
- Tells the chain: sixteen page queries pin the database CPU, every query slows, connections are held longer, and the pools empty into 503s
- Reasons with rate times hold time: 300 requests a second at 20 ms keep 6 of 60 connections busy; at 200 ms, all 60
- Notices the middleware's transaction holds each connection for the whole request, serialisation included
- Explains that `/healthz` shares the pool, so the load balancer pulls working instances and piles their traffic on the rest
- Says the connector's 10-second give-up and instant retry leave the abandoned query running beside a second copy
- Rejects 12 instances with pools of 20: 260 connections against a limit of 100, and more queries on a saturated CPU
- Caps concurrent list requests per tenant, answering the excess with 429 and Retry-After
- Sets a statement timeout on the API's database role, so a query nobody is waiting for stops using CPU
- Makes the health check answer for the instance, not for the database through the request pool
Follow-up questions
- `GET /v1/me` reads one row. Why does it return 503 at 09:03?
- What does the load balancer do when `/healthz` can't get a connection?
- A colleague wants 12 instances with pools of 20. What happens at 09:00 tomorrow?
- The connector gives up after 10 seconds and retries. What happens to the request it gave up on?
- Once the page query takes 20 ms, why keep any of this?
The changes the feed never sent
What this part is for
Purpose
Data integrity under concurrency is read here, on the feed the connector abandoned: every response it gives matches its definition, and it still loses changes. A feed the connector can trust is also what ends the 09:00 crawl.
Before the crawl, the connector followed the changes feed and lost three kinds of change. For each, tell me exactly how the feed lost it, then redesign the feed so a client that follows it never misses one.
What this question is for, and what to listen for
Purpose
The read is whether the candidate explains each loss by its mechanism — what a filter excludes, what a bulk write skips, when a row becomes visible — before reaching for a new technology. The third loss separates those who know a timestamp is taken before the commit that makes the row visible.
Signals to score
- Explains the unpaid invoices: the feed returns only `issued` invoices, so a payment takes an invoice out of the feed instead of into it
- Explains the late fees: the job's SQL `UPDATE` bypasses the ORM, so `updated_at` never moves
- Reads the example: stamped 10:00:00.2 but invisible until 10:00:06.1, by when the client's `since` was already 10:00:04.8
- Generalises it: a write that commits after a later-stamped write has been read is lost for good, and slow commits make that routine
- Returns every changed invoice with its current status, and a tombstone for anything deleted
- Sets `updated_at` in the database with a trigger, so no write path can skip it and one clock stamps every row
- Knows any stamp a trigger takes — `now()` or `clock_timestamp()` — precedes the commit, so it does not fix the example
- Stops each response, and the `since` it implies, just before the oldest open write transaction began, or follows a commit-ordered log instead
- Pages on `updated_at` and `id` together, so rows sharing a timestamp are not skipped at a page boundary
- Makes the client's apply idempotent, and adds a per-day count or checksum to catch the next silent loss
Follow-up questions
- Paid invoices still show unpaid in the ERP. Which part of the feed's definition does that?
- The correction was saved at 10:00:00.2. Why did the 10:00:03 sync miss it, and why did every later one?
- The late-fee job now sets `updated_at` on 9,000 invoices in one statement. What does the connector receive?
- What is the newest `since` the server can safely hand out?
- How will you hear about the next lost change before the tenant's accountants do?
Another company's invoice
What this part is for
Purpose
Multi-tenant authorisation, read on a leak a customer reported this morning; hand the second sheet over now. It looks like one missing condition in one query; the read is whether the candidate closes it, measures it, finds its siblings, and makes it impossible to repeat rather than unlikely.
A customer changed the number in a download link and got another company's invoice. It's 10:00. Tell me what you do in the next hour, how you find out what else was exposed, and what you change so that no endpoint in this codebase can make this mistake again.
What this question is for, and what to listen for
Purpose
Tests whether the candidate treats tenant isolation as something the data layer guarantees rather than something each handler remembers, and handles a leak in order: close it, keep the evidence, measure it, tell people. Many fix the handler and stop; others offer UUIDs as the fix.
Signals to score
- Closes it within the hour — the tenant condition in the query, or the route switched off — before the investigation, not after
- Copies the access logs somewhere safe this morning, since their 30-day retention deletes a day of evidence every day
- Answers another tenant's id with 404, not 403, because a tenant-scoped query cannot tell "not yours" from "not there"
- Joins the logs to invoice owners to list cross-tenant downloads in the 30 days logged, and treats that as a floor for a route open since last year
- Treats the 290 sequential downloads from one token as scripted enumeration, malicious or a broken integration: revokes the token and finds out whose it is
- Searches for every other query on a tenant's table that lacks the tenant, rather than calling it one bug
- Makes scoping the default: a data-access layer that will not query a tenant's table without a tenant, or row-level security behind it
- Adds a test that calls every route with a second tenant's token and expects 404
- Rejects UUIDs as the fix: harder to guess is not the same as checked
- Brings in whoever owns security and privacy today, and plans to tell the affected tenants
Follow-up questions
- Do you fix it now, or wait until you know how far it goes?
- The logs keep 30 days. What do you do about that this morning?
- Should another company's invoice get a 403 or a 404?
- Someone proposes switching invoice ids to UUIDs. Is that the fix?
- How do you stop the next endpoint someone writes from doing this?
Closing
That's the service. What do you want to ask me about how we run ours — who reviews a new endpoint before it ships, who is paged when 09:00 goes wrong, which endpoint we're least proud of?
What this line is for
Purpose
Not scored. The close is for the candidate to learn what they would be walking into; anything notable goes in the notes.
And so you hear it from me rather than in your first week: [tell the candidate one true, specific weakness in your own backend — an endpoint everyone knows is slow, a feed a customer stopped trusting, a route nobody has checked for tenant scoping]. That would be yours to work on.
What this line is for
Purpose
Ends on something true and unflattering about your own service. It is the honest pitch to the engineer this round is looking for, and fair warning to one hoping for a codebase with nothing left to fix. Say it only if it is still true.
Backend Developer interviews — common questions
- Who is this Backend Developer interview plan for?
- It is written for the interviewer, not the candidate: the hiring manager, engineer or panel member running the Technical Interview round for a Backend Developer role. It gives you a 60 min script to follow in the conversation — 4 questions with what each one is for and the signals to score against — so you are not writing the round from scratch the night before.
- What does the Technical Interview round assess?
- This round is focused on: Production debugging, data integrity under concurrency and multi-tenant authorisation, on one inherited invoicing API: a 9-second list endpoint, a 09:00 outage, a changes feed that loses updates, and a customer reading another's invoices. It works through Nine seconds for a page of invoices, Every endpoint down at 09:00, The changes the feed never sent and Another company's invoice, scoring against 39 observable signals, with follow-up prompts on all 4 questions for going deeper where an answer is thin.
- How is the 60 min split up?
- 60 min on 4 questions. The questions take in Nine seconds for a page of invoices (18 min), Every endpoint down at 09:00 (10 min), The changes the feed never sent (18 min) and Another company's invoice (14 min). The timings are there so the round stays on schedule and every candidate gets the same shape of interview — which is what makes two candidates comparable afterwards.
- What other rounds should I run for a Backend Developer?
A single round does not cover a whole role. The other rounds in this library for a Backend Developer:
Hiring for this role?
Open this plan in Hirezen and make it a position in one click.
- Every interviewer runs the same script and marks the same signals.
- AI drafts the write-ups, and the debrief puts every read side by side.
- No ATS to set up first, and no bot in the call.
Free for your first open role.