cswork / libraryOpen practice workspace

Interview guides · adMarketplace

adMarketplace Software Engineer Interview Questions & Guide 2026

adMarketplace software engineer prep on operations: decide what you would need to see to know a release is going wrong.

The 60-second brief

A Software Engineer at adMarketplace sits at the intersection of high-scale data processing and sophisticated advertising technology. You are not just writing code; you are building the infrastructure that powers search advertising across a massive network of publishers and advertisers. The work requires a balance of high-performance backend engineering and intuitive frontend interfaces to manage complex data ecosystems.

Treat capacity estimation as a conversion skill rather than a table to memorise: turn a user count and an action rate into requests per second and bytes per day, then name the component that number breaks first. The figure only matters if it changes the design.

Focus areas

  • Shard hot markets while preserving per-booking ordering
  • Index provider locations for bounded proximity queries
  • Model booking lifecycle as an explicit state machine

Overview

This role is critical to the company’s ability to deliver relevant search results and optimized ad performance in real-time. You will work on systems that handle significant traffic, meaning your technical decisions directly impact the company’s bottom line and the user experience for millions. The environment is fast-paced, and success here requires a mindset that values both architectural elegance and the practical realities of a high-growth ad-tech business.

The adMarketplace interview process

adMarketplace candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.

Recruiter Screen

The title covers product work, platform work, infrastructure, mobile and frontend, and those are different jobs with different loops behind them. A screening call is the cheapest place to find out which one the seat is, and asking reads as experienced rather than fussy. The questions that separate them: what the team is on call for, what the last three projects were, and whether any round happens inside an existing repository instead of a blank file. Then say which of that you have done and which you have not. Claiming the whole posting is the fastest way to be found out one round later.

What to demonstrate

  • Whether you can locate your experience inside one flavour of the role honestly instead of claiming the entire requirements list
  • Whether you name what you have not done, which an experienced screener reads as a level signal and can plan the loop around
  • Whether what you want next matches what the seat is: someone who wants greenfield work landing on a team that mostly operates an existing system is a hire that leaves within the year

How to prepare

  • Mark every line of the posting as done, adjacent or new, and write one sentence for each adjacent line naming the closest thing you actually built
  • Split your last two years into rough percentages across feature work, operating and debugging live systems, and design or review, so a question about scope gets numbers rather than adjectives
  • Bring three questions that discriminate between seats: what the team is paged for, how much of the work is changing existing code versus standing up something new, and what shipped in the last quarter

Hiring Manager Conversation

Expect the estimate to be probed harder than the outcome. A manager asking how long something will take is checking whether your number covers the parts that reliably go wrong: migrating the rows that already exist, and the second service that has to change at the same time. A single confident number is the weak answer. The strong one states what the estimate assumes, names the unknown that could double it, and says what you would cut first if the date were fixed. Scope is the variable you control; hours are not.

What to demonstrate

  • Whether an estimate arrives with its assumptions attached, including what it excludes and how much of it is waiting on review, environments or another team
  • Whether you can name work you cut and the person you told, since a cut nobody heard about surfaces later as a surprise rather than a decision
  • How you order two tasks that both look urgent, and whether that order survives being told the deadline moved in by a week
  • Whether you separate what must be done before launch from what must be done before the thing can be left running unattended

How to prepare

  • Take the last three things you shipped and write down, from memory, the estimate you gave and the time it actually took. Bring the largest miss and why it missed; a candidate who has never missed has never committed to a date.
  • Against your current queue, write who is blocked until each item lands. Anything with nobody behind it becomes your worked example of what you would drop and how you would say so.
  • Rehearse the fixed-date answer aloud: which slice ships, which slice is deferred and written down somewhere it will be found, and the one thing you would refuse to cut because removing it makes the release unsafe rather than smaller.

Technical Assessment

The same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.

What to demonstrate

  • In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
  • In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
  • Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes

How to prepare

  • Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
  • Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
  • For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first

Onsite/Virtual Loop

Coding rounds mostly set a floor. They decide whether you clear the bar, not where you land on the ladder. Level tends to come out of the design discussion and the ownership stories, so the question worth auditing beforehand is whether the scope you describe matches the scope of the job. Work that stops at your own service, or a story whose hard part was writing the code rather than getting several people to agree on an interface, reads a level below where you think you are interviewing, and that gap is usually resolved downwards.

What to demonstrate

  • Whether the largest thing you describe owning ran end to end — the decision, the migration path, the rollout, and what you did when it went wrong — or stopped at the change you merged
  • Whether design answers include what you would not build, what you would defer, and what you would measure before committing, rather than only what the boxes are
  • Whether a disagreement in a story was settled with something checkable — a benchmark, a prototype, a written proposal — instead of by seniority or by waiting it out
  • Whether you can say which calls you made alone and which you escalated, and why the line sat where it did

How to prepare

  • Write your largest piece of owned work as a timeline of decisions — who decided what, when, and what you did when the plan broke — then delete every sentence whose subject is "we" and see how much survives
  • Take one system you know well and drill the migration answer: how old and new paths run side by side under live traffic, how you compare their outputs, what the rollback is once writes are going to both, and which step you would not automate
  • Map each line of the ladder in the job posting to a specific thing you have done, find the line you cannot support, and prepare the closest evidence you have plus an honest account of the gap

Where candidates lose points

Treating the provider's device as the source of truth for state

The app is offline part of the time and buffers transitions, so events arrive late, out of order, and occasionally duplicated after a retry. A status column written last-write-wins will therefore accept 'started' after 'completed' and resurrect a cancelled booking, which re-enables charging on it. Client timestamps make it worse because the device clock is user-settable and drifts, so ordering by them is not ordering at all. The server must assign the ordering (a per-aggregate monotonic sequence) and every transition must be a guarded write against the expected current state, with the client event treated as a request to transition rather than as the transition.

Sharding by market_id and treating the partitions as comparable

Marketplace traffic is extremely skewed: the densest market can carry an order of magnitude more load than the median one, and within it a single venue at closing time or a transport hub at a flight bank produces a burst concentrated in a handful of cells. A hash on market_id gives an even key distribution and a wildly uneven load distribution, so one partition saturates while the fleet looks half idle, and the standard remedies collide with correctness: sub-sharding by cell breaks the single-writer-per-market property the dispatch loop relies on, so the 'one live offer per provider' rule has to be enforced globally by a constraint rather than by partition locality.

Going silent while thinking

Narrate the candidates and why you are discarding them, even in fragments: sorting first would make this a two-pointer scan, but it destroys the original indices, which the output needs. From the other side of the table, a candidate thinking hard and a candidate stuck are indistinguishable until one of them speaks.

Assuming the input fits in memory

Ask how large the input is in bytes before committing to an in-memory algorithm; beyond that point the options are a single streaming pass, an external sort with bounded buffers, or a sketch that trades exactness for constant memory. An algorithm that assumes random access to the whole input is a different algorithm from one that sees each element once.

Questions & practice

How do you detect a loop in a linked list?

Medium · data structures and algorithms

Approach

  1. Restate the input: its shape, its size, and what is guaranteed about it.
  2. Name the brute-force solution and its complexity before improving on it.
  3. Choose the data structure from the access pattern, not from familiarity.

Follow-up

  • What is the worst case, and how likely is it on real data?
  • Which test case would catch an off-by-one here?

How would you find the intersection of two huge lists?

Medium · data structures and algorithms

Approach

  1. Name the brute-force solution and its complexity before improving on it.
  2. Restate the input: its shape, its size, and what is guaranteed about it.
  3. State the target complexity and say which constraint rules the naive version out.

Follow-up

  • Which test case would catch an off-by-one here?
  • What is the worst case, and how likely is it on real data?

How do you differentiate between Vector and ArrayList?

Medium · data structures and algorithms

Approach

  1. Name the brute-force solution and its complexity before improving on it.
  2. Restate the input: its shape, its size, and what is guaranteed about it.
  3. Walk one small example through your approach before writing the whole thing.

Follow-up

  • What is the worst case, and how likely is it on real data?
  • Which test case would catch an off-by-one here?

How would you find the middle of a linked list?

Medium · data structures and algorithms

Approach

  1. Walk one small example through your approach before writing the whole thing.
  2. Choose the data structure from the access pattern, not from familiarity.
  3. State the target complexity and say which constraint rules the naive version out.

Follow-up

  • Which test case would catch an off-by-one here?
  • What is the worst case, and how likely is it on real data?

Why the naive proximity scan fails at market scale

Hard · spatial index · query planning · complexity · scale

provider_presence holds 120,000 rows for one dense market, with lat, lon, cell_id, status and expires_at. The dispatch loop runs every 500 ms and, for each of up to 400 open requests, needs the ten nearest idle providers within 3 km. The obvious implementation computes a great-circle distance for every row and sorts. It returns the correct answer. Quantify why it cannot be shipped, give a design that holds a p99 under 30 ms, and state precisely what that design gives up.

Approach

  1. Cost it in numbers, not adjectives: 400 requests times 120,000 rows is 48 million distance evaluations per cycle, and at a 500 ms cadence that is 96 million per second, plus 400 sorts of 120,000 elements. The defect is not that the haversine formula is slow; it is that the work per request is proportional to fleet size while the whole dispatch budget is a couple of seconds end to end.
  2. Explain why no B-tree rescues it. The predicate is a function of two columns, so an index on lat, or a composite on (lat, lon), can restrict only the leading column and the rest is a filter. A degree bounding box is index-assisted on that leading column but over-selects: the square circumscribing a circle of radius r has area 4r^2 against the circle's pi*r^2, so under locally uniform density about 21 percent of the rows that survive the box fall outside the radius, (4 - pi)/4, and still need an exact second pass. Keep the two ratios apart: 4/pi - 1, about 27 percent, is how much more area the box covers than the circle — the extra work done, not the false-positive share of what comes back. Converting metres to a longitude delta divides by cos(latitude), which inflates the box toward the poles.
  3. Give two designs that work. In PostgreSQL: a geography column with a GiST index and ST_DWithin(pos, point, 3000), which is index-assisted, plus ORDER BY pos <-> point LIMIT 10 for ordered nearest-neighbour. ST_Distance(...) < 3000 written as a predicate is not index-assisted and is the version written by accident. Outside PostgreSQL: bucket on cell_id in an in-memory store and read the block of cells covering the radius, which keeps the candidate set in the low thousands.
  4. Argue the storage split with the write rate rather than by preference: 100,000 providers heartbeating every 4 seconds is about 25,000 writes per second, which is presence traffic competing for the same WAL as bookings, for state that is worthless 30 seconds later. Redis matches the durability this data actually needs: GEOADD stores members in a sorted set scored by a 52-bit interleaved geohash, and GEOSEARCH ... BYRADIUS reads it. Expiry has to be built rather than assumed, because a sorted set has no per-member TTL — EXPIRE applies to the whole key, and per-field expiry exists only for hashes, via HEXPIRE from Redis 7.4. Have each heartbeat also ZADD presence:seen <epoch_seconds> <provider_id>, sweep with ZRANGEBYSCORE presence:seen -inf (now - 30) and ZREM those members from both keys in one pipeline, and re-check the stored heartbeat at offer time so a member that outlived a missed sweep is still discarded. Snapshot periodically for analytics.
  5. State what is given up. Great-circle distance is a lower bound on road distance, so a radius prefilter on it never drops a provider whose road distance is inside the radius — the shortlist is admissible. Ranking on it is not defensible: it ignores rivers, one-way systems and the direction of travel. Shortlist by distance, then rank the shortlist by a routing ETA, paying that call on 50 candidates rather than 120,000.
  6. Name the residual failure modes so the design is not oversold: cell bucketing misses a provider just across a boundary unless the neighbouring cells are queried, and a radius that returns nothing must widen rather than fail — with a bounded number of widenings, because a request that expires unmatched is a first-class outcome, not an error.

Worked solution (30 min)

  1. Write the naive cost: 400 x 120,000 = 48M distance evaluations per cycle, 96M per second at a 500 ms cadence, before the sorts.
  2. Assume the market spans about 40 km by 40 km, giving a density of 120,000 / 1,600 = 75 providers per square kilometre.
  3. Size a geohash-6 cell: 360 / 2^15 degrees of longitude is about 1.22 km and 180 / 2^15 degrees of latitude is about 0.61 km, so a 3 km radius needs about 3 rings east-west and 5 rings north-south, a 7 x 11 block of 77 cells.
  4. Compute the candidate set: 77 cells x 0.744 square kilometres each is about 57 square kilometres, so about 4,300 candidates, against the exact circle's pi x 9 = 28.3 square kilometres and about 2,120 providers.
  5. Compare: 120,000 / 4,300 is roughly a 28x reduction in rows scanned, with about half the survivors inside the true radius and needing the exact distance filter.
  6. Redo step 1 with a fleet of 12,000 to see where the naive plan becomes acceptable.

Expected result: The naive plan does about 96 million distance evaluations per second. A geohash-6 ring query at the stated density returns about 4,300 candidates per request, roughly 28 times fewer rows, of which about half fall inside the 3 km circle. The cost becomes proportional to local density rather than to fleet size, which is the property that matters as the fleet grows.

Verify it yourself

  • Every provider inside the 3 km circle lies inside the queried cell block — confirm the block's half-extent is at least 3 km on both axes, or the query silently misses providers near the edge.
  • At a fleet of 12,000 the naive plan drops to 9.6 million evaluations per second and may well be fine; the answer has to be a number, not a reflex.
  • The chosen design's cost scales with local density while the naive one scales with fleet size — state which of the two your proposal actually has.

Follow-up

  • At what fleet size does the naive version start meeting the budget again — show the arithmetic rather than guessing.
  • The 3 km radius returns zero idle providers at 03:00; what does the loop do next, and when does it stop trying?
  • Two dispatch partitions read the same idle provider from the index within one cycle — which layer prevents the double assignment, and why not this one?

Add a no-overlap reservation constraint to a live table

Hard · online migrations · lock contention · exclusion constraints

booking holds 400 million rows and takes roughly 1,200 writes per second. Rental and freelance bookings store start_at and end_at, and overlaps are currently prevented by an application-level select-then-insert that has produced 61 double bookings in six months. Add reserved_during TSTZRANGE and enforce non-overlap per listing_id across non-cancelled rows. Give the ordered migration, the exact statements, the lock each one takes, and say what you do about the step that has no concurrent path.

Approach

  1. ALTER TABLE booking ADD COLUMN reserved_during tstzrange; — nullable with no default is catalog-only and does not rewrite the table, but it still takes ACCESS EXCLUSIVE briefly, and that request queues behind any open transaction while blocking every reader that arrives behind it. So each DDL statement runs with SET lock_timeout = '2s' and a retry loop. Reject the tidier GENERATED ALWAYS AS (tstzrange(start_at, end_at, '[)')) STORED form: adding a stored generated column rewrites all 400 million rows under that lock.
  2. Deploy dual-write before backfilling, not after. The application writes reserved_during alongside start_at and end_at on every insert and update; backfilling first leaves a growing gap of rows the backfill has already swept past.
  3. Backfill by keyset pagination over the primary key in batches of a few thousand, one transaction per batch, pausing between batches: UPDATE booking SET reserved_during = tstzrange(start_at, end_at, '[)') WHERE booking_id > $last AND booking_id <= $last + $n AND reserved_during IS NULL AND start_at IS NOT NULL;. Every touched row is rewritten, so this is 400 million dead tuples' worth of vacuum pressure and WAL; pace the batches against replication lag and n_dead_tup rather than running as fast as the database accepts.
  4. Find the pre-existing violations before you attempt the constraint, because the 61 known double bookings will abort the DDL only after it has already built the entire index. Build CREATE INDEX CONCURRENTLY booking_listing_range_idx ON booking USING gist (listing_id, reserved_during) WHERE status <> 'cancelled'; (needs CREATE EXTENSION btree_gist), then self-join with a.listing_id = b.listing_id AND a.booking_id < b.booking_id AND a.reserved_during && b.reserved_during and resolve each conflict as a product decision, not a data edit.
  5. Confront the step with no concurrent path: ALTER TABLE booking ADD CONSTRAINT booking_no_overlap EXCLUDE USING gist (listing_id WITH =, reserved_during WITH &&) WHERE (status <> 'cancelled'); takes ACCESS EXCLUSIVE and builds its own index inline. There is no CREATE INDEX CONCURRENTLY equivalent, ADD CONSTRAINT ... USING INDEX accepts only UNIQUE and PRIMARY KEY indexes, and NOT VALID applies to CHECK and foreign-key constraints, not to exclusion constraints. Either take a bounded window sized by a timed build on a sample, or get the invariant from a BEFORE INSERT OR UPDATE trigger that first runs SELECT 1 FROM listing WHERE listing_id = NEW.listing_id FOR UPDATE, serialising writers per listing so the subsequent overlap check is safe. Say which you chose, and say that the trigger is the weaker of the two because it can be disabled and the constraint cannot.
  6. Retire the old columns in a separate deploy: stop reading start_at and end_at first, then drop them. Dropping them while any read path still uses them converts a reversible migration into an outage.

Follow-up

  • booking is partitioned by market_id, and PostgreSQL does not allow an exclusion constraint on a partitioned table. What does a per-partition constraint actually guarantee here, and is it enough?
  • The backfill reaches row 180 million and replica lag hits 40 seconds. What do you change, and what do you deliberately leave alone?
  • Half-open [) or closed [] for the range: which did you choose, and what happens to a check-out at 11:00 followed by a check-in at 11:00 under each?

Model provider deactivation, audit history and erasure together

Hard · soft deletes · audit history · temporal data · constraints

provider(provider_id, email CITEXT UNIQUE, compliance_state, payout_account_ref, legal_name, phone) is referenced by booking.provider_id and, through it, by ledger_entry. Three requirements land in the same sprint: support must deactivate and later reactivate a provider; a dispute six months later must show which compliance state was in force on a given date; and an erasure request must remove personal data while payout history stays reconcilable. A deleted_at column plus query filters is proposed. Give the schema, and say precisely what deleted_at alone breaks.

Approach

  1. Let the erasure requirement decide the layout, because it is the one that constrains the others. Split identity from personal data: provider keeps the surrogate key, compliance state, eligibility_version and the payout reference, while provider_pii(provider_id, email, legal_name, phone) holds everything erasable. Erasure deletes or crypto-shreds that row; booking and ledger_entry carry only provider_id, so every historical sum stays reproducible and no ledger row is ever touched.
  2. Enumerate what deleted_at alone breaks, concretely rather than as a style objection. UNIQUE (email) now blocks the same person re-registering, so it must become CREATE UNIQUE INDEX ... ON provider_pii (email) WHERE deleted_at IS NULL. Every query silently includes deleted rows unless it remembers the filter, and the queries that forget are the aggregates nobody re-reads. Foreign keys still resolve, which is correct and is exactly why the row cannot be hard-deleted — and why ON DELETE CASCADE anywhere on this chain would destroy financial history.
  3. Treat deactivation as a state, not a deletion: a value in the compliance state machine with legal transitions (approved -> suspended -> deactivated -> approved), and eligibility_version bumped on every change so dispatch re-reads eligibility at dispatch time instead of trusting a flag cached at login. A boolean cannot express reactivation at all, and a nullable timestamp can only express it by being nulled, which erases the history you were asked to keep.
  4. Record the audit trail as an append-only table written in the same transaction as the state change: provider_compliance_event(event_id, provider_id, from_state, to_state, actor_type, actor_id, reason, occurred_at). provider.compliance_state then becomes a cache of the latest event rather than an independent fact.
  5. Make 'what was in force on date D' a temporal query with a temporal structure: provider_compliance_period(provider_id, state, valid_during TSTZRANGE) with CREATE EXTENSION btree_gist and EXCLUDE USING gist (provider_id WITH =, valid_during WITH &&). The open period has an unbounded upper bound, and the dispute query is WHERE provider_id = $1 AND valid_during @> $d, which the GiST index serves. Closing one period and opening the next must happen in a single transaction, or the constraint rejects the overlap — which is the constraint working, not an obstacle.
  6. State the cost honestly: three artefacts now describe the same state, so nominate one writer. Either the application writes the event and a trigger maintains the period and the cached column, or the period table is derived by a job from the event log; say which, and say what a crash between two of them leaves behind.

Worked solution (40 min)

  1. Write the DDL for all four tables, including the exclusion constraint and the partial unique index on email.
  2. Insert a provider, then suspend, reactivate and deactivate them, producing four periods, and query the state at three different instants.
  3. Attempt to open a new period without closing the previous one and observe the failure.
  4. Delete the provider_pii row and re-run a payout sum for a historical period, comparing it byte for byte with the pre-erasure result.
  5. Re-register a new provider with the erased email, and separately with a live provider's email.

Expected result: The state-at-date query returns exactly one row for any instant in the provider's lifetime and zero rows for instants before onboarding. The overlapping insert fails with SQLSTATE 23P01. After erasure the historical payout sum is unchanged. Re-registration with the erased email succeeds; with a live email it fails.

Verify it yourself

  • Exactly one period per provider has an unbounded upper bound at any moment.
  • SELECT count(*) FROM provider_compliance_period WHERE provider_id = $1 AND valid_during @> $t returns 0 or 1 and never more — the exclusion constraint guarantees that, not a query or a code review.
  • No ledger_entry or booking row is modified by any part of the erasure path.

Follow-up

  • A partial unique index's predicate must be immutable. Why can it not be WHERE deleted_at > now() - interval '30 days', and how would you express a 30-day grace period instead?
  • Erasure removed the email, and a chargeback then arrives on a two-year-old booking. What can you still do, and what can you no longer do?
  • Who writes provider_compliance_period — the application, a trigger on the event table, or a batch job? Defend the choice against a crash mid-transition.

How does serialization work in Java?

Medium

Approach

  1. Clarify what is being asked and what a complete answer contains.
  2. State your assumptions explicitly before working the problem.
  3. Work from the requirement backwards to the design.

Follow-up

  • How would you know your answer was wrong?
  • What assumption would you test first?

Explain the synchronized keyword in Java.

Medium

Approach

  1. State your assumptions explicitly before working the problem.
  2. Clarify what is being asked and what a complete answer contains.
  3. Work from the requirement backwards to the design.

Follow-up

  • What assumption would you test first?
  • How would you know your answer was wrong?

Specify the idempotency contract for booking creation from a quote

Medium · idempotency · retries · payments

POST /bookings binds a consumer to an existing quote: the body carries quote_id, request_id and a tokenised instrument. The server authorises against the payment gateway and inserts a booking row; booking has UNIQUE (request_id) and quote rows are immutable with an expiry. The caller is a mobile client with a 10 s timeout that retries twice. Specify the idempotency contract: where the key comes from, what is persisted and at what point, the response when the same key arrives with a different body, the response while the first call is still in flight, and the retention floor.

Approach

  1. Tie the key to the user's intent, not to the HTTP attempt: the client mints a UUID when the consumer taps confirm, stores it, and replays it on every retry of that tap. A key generated per request defeats the whole mechanism while leaving it visibly in place, which is the failure mode that survives code review.
  2. Persist the key before doing anything external. Insert an idempotency record with a UNIQUE constraint on the key and a status of in_progress, and commit that before calling the gateway - if it is written after the call, a crash in between leaves a charge with nothing pointing at it. Store a fingerprint alongside it: a hash over the canonicalised fields that must not change, here quote_id, request_id and the instrument token.
  3. Define the three arrival cases precisely. A new key runs the operation. A known key whose fingerprint matches and whose record is completed replays the stored status code and body byte-identically, including when that status was a 4xx. A known key whose fingerprint differs is refused with 422 and a distinct code, without executing - the client has reused a key for a different intent, and guessing which one it meant is how you double-book.
  4. Handle the in-flight case explicitly rather than by waiting: return 409 with Retry-After: 1 and a code the client can act on. A 202 invites the client to move on as though a booking exists, and blocking the second request until the first finishes just moves the timeout.
  5. Set retention from the client's behaviour, not from taste: the record must outlive the longest retry horizon the client can produce plus the gateway reconciliation lag for unknown outcomes, so 24 hours is a floor here, held in a TTL-partitioned table. Note the interaction with quote expiry: a replay must return the stored response and must not re-validate the quote, because the booking already exists and re-checking an expired quote would turn a successful retry into a spurious failure. UNIQUE (request_id) is the backstop for a client that loses its key entirely - map that violation to the existing booking's representation rather than a 500.

Worked solution (30 min)

  1. Write the key provenance rule and the exact moment the client generates and discards a key.
  2. Implement the reservation insert with its UNIQUE constraint, committed before the gateway call, and the read-back path on conflict.
  3. Define the fingerprint over the canonicalised triple and the 422 refusal on mismatch.
  4. On completion, write the status code, response body and booking_id in the same transaction that flips the record to completed.
  5. Exercise three interleavings: a sequential retry after a timeout, two concurrent requests sharing a key, and a replay issued after the quote has expired.

Expected result: Three attempts of one tap produce one booking row and one authorisation; the concurrent duplicate receives 409 with Retry-After rather than a second authorisation; the post-expiry replay returns the stored 201; a changed body under the same key is refused with no side effect.

Verify it yourself

  • payment_attempt holds exactly one row for that booking and kind after every interleaving, and the gateway's own record agrees.
  • A replay returns the same HTTP status as the original, including when the original was a 4xx.
  • Shortening retention below the client's retry horizon reproduces the double booking, which is how you find the floor rather than guessing it.

Follow-up

  • The gateway call times out and the record stays in_progress forever because the process died. What sweeps it, and what does the endpoint return in the meantime?
  • The consumer taps confirm twice deliberately, wanting two bookings. How does the contract tell that apart from a retry?
  • Where does the idempotency record live relative to the booking row, and what breaks if they are in different databases?

Quote latency spikes every sixty seconds during a demand-signal outage

Hard · cache stampede · partial outage · retry amplification · degradation

The demand multiplier is cached per (cell, tier) with a 60-second TTL and refreshed from an aggregator. The aggregator starts returning 504s for about 30% of cells. Quote p99 goes from 40 ms to 3.1 s in a sawtooth with a 60-second period, aggregator load triples although request volume is flat, and the dispatch loop begins missing its two-second budget so accept windows are wasted. Each refresh retries three times with a two-second timeout. Give the ordered checklist, the two independent defects, and the degraded behaviour you ship.

Approach

  1. Chart cache hit rate per cell at one-second resolution first. A sawtooth whose period equals the TTL is the signature of synchronized expiry -- entries written together by a warm-up or by an earlier mass refresh expire together -- and it distinguishes a stampede from a dependency that is merely slow, before you touch the dependency at all.
  2. Then put origin call rate next to incoming request rate. Load tripling while requests stay flat is retry amplification, not demand: three attempts per miss, and each concurrent miss on the same key mounts its own set of attempts because nothing coalesces them. The multiplier falls straight out of the ratio.
  3. Then characterise the dependency's failure shape -- which cells, which regions, whether the 504s are uniform or concentrated. A partial failure across 30% of cells is a different incident from a whole-region loss, and the correct degraded behaviour differs between them.
  4. Separate the two defects, because fixing either alone leaves the incident standing. First, there is no single-flight, so N concurrent misses on one key produce N origin calls rather than one. Second, refresh treats an error as absence and evicts, so a failing dependency empties the cache instead of leaving it populated. Independently each is survivable; together they convert a 30% dependency failure into a cache with a near-zero hit rate.
  5. Ship the degraded behaviour explicitly rather than leaving it implicit. Serve the last known multiplier past its TTL under a bounded staleness ceiling, and once that ceiling is exceeded fall back to the base rate rather than to the last surge value -- charging a multiplier you can no longer justify is the asymmetric error and it surfaces as disputes and chargebacks rather than as an alert. Add per-key single-flight, jitter the TTL so entries stop expiring in lockstep, and replace per-call retry counts with a retry budget capped as a fraction of total calls behind a circuit breaker.
  6. State what does not change, because this is the part people break while fixing the rest. Quotes already issued are immutable and stay honoured for their full lifetime; the booking binds to quote_id, so the degraded path must never reprice a booking already bound to one. A quote that expires mid-flow is a user-visible re-quote, not a silent background refresh.

Follow-up

  • Single-flight makes one caller wait on the origin. What does that waiter do when the origin exceeds the dispatch budget, and who decides -- the cache or the caller?
  • You choose a five-minute staleness ceiling. Justify the number against what the multiplier actually does over five minutes in a dense cell.
  • The aggregator recovers. Describe the recovery traffic and what stops it from becoming a second stampede.

Suggested practice checklist

Four days sample coding, design, fundamentals and the practical rounds at deliberately shallow depth, which is enough to surface the topics you did not know were in scope. That map, rather than a guess made on day one, decides where the last three days go.

Day 1 — Coding, one pass at shallow depth

  • Solve one problem from each of six families, an array with two pointers, hash counting, binary search, a tree traversal, a graph traversal and one dynamic program, under a hard twenty-minute cap with no extensions, marking each finished, late, or stalled.
  • For every stall, write the exact move you could not make rather than the subject, so the note reads could not turn the recurrence into a loop rather than bad at dynamic programming.
  • Fix nothing today. The value of the pass is the unfixed record.

Deliverable: Six timed attempts marked finished, late or stalled, each stall carrying a named blocking move.

Day 2 — Design, one pass at shallow depth

  • Spend twenty minutes each on three different shapes, a read-heavy feed, a write-heavy ingest path, and something needing a transaction across two entities, stopping each at requirements, interface and data model.
  • After each, write the first question you could not answer, which is usually a number you could not estimate or a failure mode you had no vocabulary for.
  • Mark which of the three you would be most relieved not to be asked, and treat that as data rather than as a preference.

Deliverable: Three shallow designs, each with the first unanswerable question written at the bottom.

Day 3 — Fundamentals and the practical rounds

  • Answer eight short questions in writing at four minutes each, covering the material that fills the gaps between the big rounds: what happens between a URL and a rendered page, what an index costs on write, when a process is preferable to a thread, and what conditions a deadlock requires.
  • Do one thirty-minute practical task of the kind a take-home compresses: read an unfamiliar two-hundred-line file and write what it does, what you would change, and the one thing you remain unsure of.
  • Score every answer fluent, correct but slow, or absent, and keep the absent ones visible.

Deliverable: Eight scored short answers and one written reading of unfamiliar code.

Day 4 — The rounds that are about you, and the map

  • Deliver three behavioural answers aloud against a timer, a conflict, a failure you owned, and a decision made without enough information, marking any that ran past three minutes or contained no number.
  • Assemble the map: every marked item from days one to three on a single page, sorted by how likely it is to appear in your loop rather than by how uncomfortable it felt.
  • Choose exactly two areas for the remaining three days and write down what you are deliberately abandoning.

Deliverable: A one-page scored map of the whole surface area with two areas chosen and the rest explicitly abandoned.

Day 5 — First chosen area, to the depth you skipped

  • Work the higher-ranked area in four focused blocks, choosing items one level above where you stalled rather than repeating what already works.
  • After each block write the rule you extracted in one sentence with its precondition attached, since a rule carrying no precondition is exactly what fails under a variation.
  • Re-attempt the day-one or day-two item that exposed this area and compare against the original timing.

Deliverable: Four worked blocks, a timed re-attempt against the original, and three one-sentence rules with preconditions.

Day 6 — Second chosen area, where the gap is coverage rather than speed

  • Treat the second area differently from the first. Day five drilled something you could already half-do; this one is usually a topic you had simply never met, so build one worked reference example end to end and keep it, rather than attempting six problems badly.
  • Write down the vocabulary you were missing on day two or three, five terms at most, each with the one sentence that makes it usable in an answer rather than the textbook definition.
  • Redo the shallow attempt that exposed this area and note whether you now fail later in the problem, because moving the failure point is the realistic gain from a single day and is worth more than a score that did not change.

Deliverable: One worked reference example for the newly covered area, a five-term vocabulary list, and a note on where the failure point moved.

Day 7 — Reassemble the loop

  • Sit two rounds back to back with no gap, ordering them so the area you chose second comes last, because the map was built from rested, isolated attempts and the loop will reach your weaker area when you are already spent.
  • Write where the second round suffered from the first, which is normally the point at which structure collapses into narration.
  • Reduce the week to one page holding only the rules you can state without reading them.

Deliverable: Mock notes on cross-round carryover plus a one-page card of rules you can recite from memory.

The failure that was yours

Keep one story where the bad call was yours rather than a dependency's or a manager's. Name the check that would have caught it, whether you added that check afterwards, and whether it has fired since. Answers that route blame outward end the conversation early; answers that end in a guardrail someone still relies on tend to open it up.

What is your experience with building microservices?

Medium · behavioural and engineering judgement

Approach

  1. Pick a story where you made the decision, not one where you watched it.
  2. State the situation in two sentences and spend the rest on the reasoning.
  3. Give the blast radius: what could have broken, and what you measured.

Follow-up

  • What did you decide not to do, and why?
  • How did you know your change caused the improvement?

How do you communicate technical decisions to non-technical stakeholders?

Medium · behavioural and engineering judgement

Approach

  1. Close with what you would do differently, concretely.
  2. State the situation in two sentences and spend the rest on the reasoning.
  3. Pick a story where you made the decision, not one where you watched it.

Follow-up

  • What would you do differently if you ran that again?
  • How did you know your change caused the improvement?

Unblocking a stuck engineer without taking the keyboard

Easy · mentoring · leverage · knowledge sharing

Describe a time you unblocked someone who had been stuck for more than a day. Say how you learned they were stuck, what you diagnosed the real blocker to be - missing context, a wrong mental model, a genuinely hard bug, or reluctance to ask - and what you actually did. State explicitly whether you took the keyboard and what that cost. Then say what changed so the next person is not stuck in the same place: a document, a test, a renamed function, a constraint that turns the mistake into an error.

Approach

  1. Diagnose the blocker type first, because the responses diverge sharply. Missing context is a five-minute fix; a wrong mental model has to be corrected out loud and checked; reluctance to ask is a team-norm problem you cannot solve in one sitting and should not pretend you did.
  2. Describe the intervention at the level of what you said or drew - the interleaving you sketched, the question that surfaced their hidden assumption - rather than the word 'pairing', which conveys nothing about what you contributed.
  3. Be explicit about taking the keyboard. It is sometimes right under time pressure and it always trades their learning for your speed. Naming the trade is the difference between mentoring and rescuing.
  4. Verify the unblock rather than assuming it: did they finish it alone, did they hit the same wall a fortnight later, did they later explain it to someone else. That last one is the strongest available evidence.
  5. Name the durable artifact. A mentoring answer with no residue describes one act of help; the leverage is in the test, the comment, or the constraint that makes the same confusion impossible next time.

Follow-up

  • How long did you let them struggle before stepping in, and how did you choose that duration?
  • What did you get wrong about why they were stuck?
  • When is taking the keyboard the correct call rather than the easy one?