Interview guides · Audinate
Audinate Software Engineer Interview Questions & Guide 2026
API design practice for Audinate software engineer candidates: compatibility, versioning, and changes you cannot take back.
The 60-second brief
At Audinate, the Software Engineer role supports the company's work in the audio-visual industry. You are not just writing code; you are building the infrastructure that powers professional AV networking globally. Your work directly impacts how audio and video data are transported, managed, and delivered across complex, high-performance systems.
State every complexity claim with the assumption sitting under it. Hash lookup is O(1) on average and only for a hash that spreads your actual keys; comparison-based sorting cannot beat n log n, though counting or radix sort can when the keys are bounded integers.
Focus areas
- Scope every query and cache key by tenant
- Build at-least-once pipelines with explicit deduplication horizons
- Evolve APIs without breaking pinned SDK clients
Overview
This position demands a blend of deep technical proficiency and a genuine passion for high-reliability software. Whether you are working on backend services, platform architecture, or specialized networking solutions, you will contribute to products that set the industry standard. Candidates report that the interviews favor engineers who handle complexity, value system stability, and can solve challenges that require both precision and architectural foresight.
The Audinate interview process
Audinate candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Initial Screening
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
Technical Deep-Dives
Input bounds are the part of the prompt most often skimmed, and they usually contain the answer. They tell you which complexity class is admissible, which narrows the search before you have thought about the problem itself. As a rough planning figure, a compiled language does on the order of 10^8 simple operations per second and an interpreted one roughly an order of magnitude less. So n up to about twenty admits enumerating subsets, a few thousand admits a quadratic pass, and a million admits neither: you need near-linear, or linear with a log factor. If the bounds are missing, ask for them.
What to demonstrate
- Whether the approach is justified by the stated input size rather than by whichever pattern you recognised first
- Whether you ask about the properties that change the algorithm: whether the input arrives sorted, whether duplicates occur, whether values are bounded integers, whether it all fits in memory
- Whether you can name the bottleneck in your own solution and what would remove it, even when you deliberately leave it in place
- Whether a claimed speedup is real, since memoising a recursion only helps when subproblems genuinely overlap and the state can be keyed cheaply
How to prepare
- For each algorithm you rely on, write down the largest n it handles in roughly a second, then check two of those figures by timing them in the language you will actually type in
- For two weeks, write one line naming your target complexity and the bound that justifies it before you write any code, then compare that line with what you ended up submitting
- Practise the conversion backwards: given a required O(n log n), list the mechanisms that get you there (sorting, a heap, an ordered map, divide and conquer) and choose by what the problem needs to query, not by what you used last
Leadership Discussion
Because the format is not fixed, the first job in the room is classification. Listen to the opening question and decide what it is: a probe into work you have already described, a fresh problem to solve now, or a conversation about how you operate. Each wants a different register, and the common failure is forcing a rehearsed structure onto a question that did not ask for it. Running a full design ritual on a ten-minute debugging question reads as not listening. When you cannot tell which it is, ask how long they want to spend and answer at that depth.
What to demonstrate
- Whether the shape of your answer matches the question, so a yes-or-no gets answered before it is justified and an open prompt gets a direction before a detour
- Whether you check how much depth is wanted instead of deciding for them, and whether you stop when the answer is complete rather than continuing until someone interrupts
- Whether you can be redirected in the middle of an answer without restarting it from the beginning
- Whether a question outside your experience gets an honest boundary followed by reasoning from what you do know, instead of a confident answer with nothing behind it
How to prepare
- Rehearse one project at three lengths, roughly thirty seconds, three minutes, and a full walkthrough at the depth of a design review, and practise switching between them when someone interrupts mid-telling
- Have someone ask you five questions of deliberately mixed type in one sitting without telling you the types, and score only whether you identified each one correctly before you started answering
- Draft the sentence you will use to check depth, along the lines of asking whether the short version is useful here or they want the detail, and use it in a real conversation this week so the day of the round is not its first outing
Where candidates lose points
Holding money in a floating-point type, or rounding it more than once
Binary floating point cannot represent 0.01 or 0.1 exactly, so sums drift and two code paths that should agree disagree by cents nobody can trace back. The fix is integer minor units or an exact decimal type end to end, with sub-cent rates expressed as scaled integers such as micro-units, because a per-request price genuinely is smaller than a cent. The second half of the trap is rounding position: rounding each line and then summing gives a different total from summing and rounding once, and half-up and half-even diverge systematically across many lines, so rounding must happen at one named place and every downstream reader must carry the rounded value rather than recompute it from quantity and rate.
Checking a quota with a select and then writing
Under read-committed isolation, two concurrent transactions both observe a count below the limit and both insert, so the limit is exceeded by exactly the concurrency. Repeatable read does not rescue it either: it provides a stable snapshot, and this is write skew, which snapshot isolation permits by design. The options are serialisable isolation, which detects the conflict and aborts one transaction with a serialisation failure and therefore obliges the caller to retry; a single statement with the predicate inside the write; or a constraint that makes the surplus insert fail outright. The reason this pattern survives review is that it is correct in every test that runs one request at a time.
Designing for a scale nobody asked for
Ask for request rate, data size, read-to-write ratio and expected growth, then size the simplest option first; one relational instance on current hardware covers a large share of real workloads. Reaching for shards, queues and a cache tier before any number has been quoted reads as pattern-matching rather than judgement.
Naming no test cases at all
State what you would test before being asked: empty input, a single element, all elements equal, the maximum permitted size, and the input that exercises the branch you just wrote. It costs thirty seconds and is much of what separates someone who has shipped code from someone who has only solved puzzles.
Questions & practice
Schedule ordered webhook retries with a heap of subscription queues
Medium · heap · scheduling · backoff · head-of-line-blocking
Design the in-memory scheduler for webhook delivery. Up to 20 million rows sit in status pending or failed_retryable across 200,000 subscriptions, each row carrying next_attempt_at and attempt_count, and each endpoint having a circuit breaker. Deliveries for one subscription must be attempted in order, so at most one attempt per subscription may be in flight. Support due(now), complete(delivery, outcome) and insert(delivery) in O(log S), where S is the subscription count rather than the delivery count. Give the backoff formula you schedule retries with.
Approach
- Key the global heap by subscription, not by delivery. Each subscription owns a FIFO of its due deliveries in event order; the heap holds one entry per eligible subscription, keyed by its head's
next_attempt_at. That is 200,000 heap entries instead of 20 million, and it makes the one-in-flight rule structural rather than a check somebody can forget. due(now): peek the minimum. If its key is in the future, sleep until then instead of spinning. Otherwise pop it, move the subscription into an in-flight set, and do not re-push it. A subscription absent from the heap cannot be dispatched twice, which is precisely how ordering is preserved.complete: on success, drop the head and re-push the subscription keyed by its new head, or leave it out when the queue empties. On a retryable failure, incrementattempt_countand setnext_attempt_at = now + uniform(0, min(cap, base * 2^attempt)), sampled uniformly across the whole interval. That is full jitter; deterministic backoff re-synchronises the herd you just created.- Circuit breaker: park the subscription in a second heap keyed by its half-open time, so an endpoint dead for six hours costs one heap entry and zero attempts rather than consuming worker slots. Admit exactly one probe at half-open and close the breaker only on its success.
- Say the price of the ordering guarantee out loud. One in-flight attempt per subscription means an endpoint answering in 10 seconds drains at 0.1 deliveries/second however many workers you run, and its backlog grows until it recovers. If the customer does not need order, allow k in flight and document delivery as unordered; that is the trade, and it is a product decision.
- All three operations are O(log S) with O(S) resident heap memory and the queues themselves backed by the store. The database-backed equivalent is a partial index on
(subscription_id, next_attempt_at) where status in ('pending','failed_retryable')claimed withFOR UPDATE SKIP LOCKED, and the write-back must be fenced onlease_tokenso a worker that stalled and resumed cannot overwrite a newer attempt.
Worked solution (30 min)
- Define the four structures explicitly:
queues: subscription_id -> deque[delivery],ready: min-heap of (next_attempt_at, subscription_id),inflight: set[subscription_id],breaker: min-heap of (half_open_at, subscription_id). - Write down the invariant you will assert after every operation: a subscription appears in at most one of
ready,inflightandbreaker, never in two. - Implement
due,completeandinsert, then simulate 200,000 subscriptions with Zipf-distributed queue depths totalling 20 million deliveries. - Add one endpoint that always times out after 10 seconds and one that always answers in 20 ms, then measure the fast endpoint's throughput with and without the per-endpoint breaker.
- Instrument heap size across the run.
Expected result: Heap size stays at or below 200,000 regardless of the 20 million deliveries. The fast endpoint's throughput is unaffected by the dead one once the breaker trips. The dead endpoint's deliveries accumulate in their own deque and cost exactly one heap entry.
Verify it yourself
- The asserted invariant never fires under concurrent workers.
- Replaying the same delivery set twice produces identical per-subscription ordering, even though the interleaving across subscriptions differs.
- Replacing full jitter with fixed backoff makes retry attempts cluster visibly in a histogram of attempt timestamps.
Follow-up
- One subscription has 4 million queued deliveries. What stops it from starving the other 199,999, and what does your heap look like under that load?
- A customer requests redelivery of last Tuesday's events. Where do those rows enter your structure, and what keeps them from reordering live traffic?
- The process restarts. How much state do you rebuild, and what stops every subscription from being attempted in the same second?
Find peak concurrent sandbox usage from run intervals
Medium · sweep-line · intervals · concurrency-caps · null-semantics
Given up to 5 million job_run rows for one tenant over one day, with run_id, started_at, finished_at, status and wall_clock_limit_seconds, report the maximum number of sandboxes running at once, the earliest instant that maximum is reached, and the first run_id that would breach a per-tenant cap of C. started_at is null while a run is queued; finished_at is null both for runs still executing and for runs in status lost. Treat a run as occupying [started_at, finished_at). Give the complexity and state how you handle each null.
Approach
- Turn each run into two sweep events,
(started_at, +1)and(end, -1), then sort the 2n events by timestamp with-1ordered before+1at equal timestamps. That tie-break is what makes the interval half-open, so a run finishing at 10:00:00 and one starting at 10:00:00 never overlap. - Decide each null out loud before sweeping, because each choice moves the answer. A null
started_atmeans queued and contributes nothing. A nullfinished_atwith statusrunningorleasedis clipped to the window end. Statuslosthas no observed end at all, so clip it atstarted_at + wall_clock_limit_secondson the grounds that the supervisor owns the timeout, and record that you did. The table'scheck (finished_at is null or started_at is not null)guarantees you never see an end without a start. - Sweep once, maintaining a running counter, the maximum, and the timestamp at which the maximum was first attained (update
peak_atonly on a strict increase, or you will report the last such instant instead of the earliest). Capture the firstrun_idwhose+1takes the counter to C+1 during the same sweep rather than in a second pass. - Complexity: O(n log n) dominated by the sort, O(n) space. If rows already arrive ordered by
started_at, a min-heap of end times gives O(n log k) time and O(k) space with k the peak concurrency, which is the better shape when the rows come from an index scan on(tenant_id, started_at). - If second resolution is acceptable, counting-sort the endpoints into an 86,400-slot delta array and prefix-sum it: O(n + T) time and O(T) space, which beats the comparison sort at 5 million rows. It answers only at second granularity, so state which resolution the cap is defined in.
Follow-up
- Now report peak concurrency per tenant for 10,000 tenants from one globally sorted stream. What changes about memory and about the sort?
- The cap has to be enforced at dispatch rather than reported afterwards. What does the admission check look like, and where does it race?
- How would you answer 'peak concurrency within any 5-minute window' without re-sorting?
Order a job dependency graph and find its critical path
Medium · topological-sort · dag-longest-path · cycle-detection · critical-path
A workspace defines up to 50,000 jobs with up to 200,000 dependency edges and an estimated duration_seconds per job. Given the edge list, reject the graph if it contains a cycle and name one cycle's nodes; otherwise return a valid execution order, the earliest possible completion time with unlimited workers, and the set of jobs whose slack is zero. Then say which single job to shorten in order to cut the completion time, and by exactly how much. State the complexity of each part.
Approach
- Kahn's algorithm for the order: compute indegrees, seed a queue with zero-indegree nodes, emit and decrement. O(V + E), which at 50,000 and 200,000 is milliseconds. If fewer than V nodes are emitted, the graph contains a cycle.
- Kahn detects a cycle but cannot name one. The nodes left with indegree above zero contain every cycle, so run one DFS restricted to that residual subgraph with three-colour marking and report the stack slice from the grey node the back edge points at. That is the difference between a usable error message and 'dependency cycle detected'.
- Earliest completion with unlimited workers is the longest path, which is NP-hard on a general graph and linear on a DAG. State the precondition, then relax in topological order:
earliest_finish[v] = duration[v] + max(earliest_finish[u] for u in preds(v)), taking the max over an empty predecessor set as zero. The makespan T is the maximum over all nodes. O(V + E). - Second pass in reverse topological order for
latest_finish, thenslack[v] = latest_finish[v] - earliest_finish[v]. Zero-slack nodes form the critical path, and there can be several disjoint critical paths, so return the set rather than one chain.slack[v] = 0is exactly the statement that some longest path runs through v; equivalently, the longest path through v has lengthT - slack[v]. - The speed-up bound is the point of the question, and the obvious form of it is wrong. Shortening a zero-slack job v by d, with 0 <= d <= duration[v], cuts the makespan by
min(d, T - L_avoid(v)), whereL_avoid(v)is the longest path in the graph with v deleted: the longest path that avoids v, not the second-longest path overall. The two coincide only when the runner-up path misses v. Counterexample: A of 10 s feeds both B of 5 s and C of 4 s, so T = 15 s and the second-longest path is 14 s, yet shortening A by 10 s leaves a makespan of 5 s. The realised gain is the full 10 s, because both paths ran through A and shrank together, whilemin(10, 15 - 14)predicts 1 s. The reason is structural: shortening v reduces every path through v by d and leaves every other path alone, so the new makespan ismax(T - d, L_avoid(v)). - Compute
L_avoid(v)the direct way: delete v and re-run the same forward relaxation, O(V + E) per candidate. The cheaper equivalent skips the deletion, sinceL_avoid(v)only ever matters through that max: setduration[v] := 0, recompute the makespan asT0(v) = max(T - duration[v], L_avoid(v)), and the gain ismin(d, T - T0(v)), which is identical for every d <= duration[v]. Only zero-slack jobs are candidates, because shortening a job with positive slack changes the completion time not at all. One relaxation is milliseconds at this size, so ranking a critical set in the hundreds costs O(k(V + E)) and is worth doing exactly; a critical set in the tens of thousands is not, and there you evaluate a shortlist, longest jobs first, and say that the answer is the best of that shortlist rather than the optimum.
Follow-up
- Only m workers are available. What happens to your answer, and what can you still promise about the schedule you produce?
- Edges arrive incrementally as the customer edits the pipeline. How do you detect a cycle at insert time without re-running Kahn over 250,000 elements?
- Durations are estimates. How would you express completion time as a distribution, and what breaks about the critical path once you do?
Rebuild an hourly rollup with deduplication and late-arrival accounting
Hard · window functions · deduplication · event time vs ingest time · watermarks
From usage_event (event_id, tenant_id, workspace_id, environment, sku, quantity numeric(20,6), idempotency_key, occurred_at, ingested_at), produce the values usage_rollup_hourly should hold for one tenant over one day: per (workspace_id, sku, hour_start) the deduplicated quantity_sum, event_count and source_max_ingested_at, bucketed by occurred_at. Duplicates share (tenant_id, idempotency_key). Also report, per hour, the running total across the day and the share of quantity that arrived more than two hours after the hour began. Write the query, and state which duplicates a daily unique index cannot catch.
Approach
- Deduplicate in its own CTE before any aggregation, because a SUM cannot be un-summed:
row_number() over (partition by tenant_id, idempotency_key order by ingested_at, event_id) = 1. Include the tiebreaker. Without it the surviving row is non-deterministic when two duplicates share an ingested_at, and a rollup described as deterministically recomputable then disagrees with itself between runs. - Bucket on occurred_at and nothing else, and pin the timezone explicitly.
date_trunc('hour', timestamptz)truncates in the session's TimeZone setting, so the same query run by a session set to a non-UTC zone buckets differently; use the three-argumentdate_trunc('hour', occurred_at, 'UTC')on PostgreSQL 16 or later, ordate_trunc('hour', occurred_at at time zone 'UTC') at time zone 'UTC'before that. Filterenvironment = 'production'explicitly, since metering covers three environments and billing covers one. - Aggregate to the grain with
sum(quantity),count(*)andmax(ingested_at). The last is not decoration: it is the watermark the row consumed up to, and without it there is no way to prove afterwards what a number did and did not include. - Compute the late share inside the dedup-and-aggregate step as a conditional aggregate,
sum(quantity) filter (where ingested_at > hour_start + interval '2 hours'), then divide by the hour's total. Compute the running total as a window over the already aggregated rows:sum(quantity_sum) over (partition by workspace_id, sku order by hour_start rows between unbounded preceding and current row). Running either over raw rows puts the duplicates back. - Answer the index question exactly. The unique constraint is on (ingested_day, tenant_id, idempotency_key), because a unique index on a partitioned table must contain the partition key. It therefore deduplicates only within one ingest day and admits a duplicate whose retry crosses midnight or whose replay runs a week later. That is why this CTE dedups across the whole window being recomputed, and why the dedup horizon is a correctness parameter rather than a retention cost.
- Keep the numeric type all the way through. quantity is numeric so the sums are exact; a cast to double precision anywhere in this pipeline reintroduces drift that surfaces only as a few unreconcilable cents per tenant per month, long after the query is out of anyone's mind.
Follow-up
- A dispute forces the same recompute over 40 days for one tenant. What changes about the dedup CTE's memory use and the chosen plan, and what would you do about it?
- Two runs a minute apart return different quantity_sum values for an hour that is already closed. Give two mechanisms that produce that, and the single query that distinguishes them.
- Express the same rollup incrementally so it does not re-scan the day each time the watermark advances. What does the incremental version stop being able to answer?
Migrate a live partitioned event table without blocking ingest
Hard · online migration · ddl locks · expand and contract · partitioning
usage_event is range-partitioned daily on ingested_at, holds roughly 250M rows per day across 400 live partitions, and is written at 10-40k rows/second. Two changes are required: quantity must move from double precision to numeric(20,6), and a new environment column must become NOT NULL with a default of 'production'. Ingest cannot stop. Give the ordered plan, naming for each step the lock it takes, what that lock blocks, and roughly how long it is held. Identify the one step that cannot be rolled back cleanly once traffic depends on it.
Approach
- Classify the two changes before planning anything. Adding a column with a non-volatile default has been metadata-only since PostgreSQL 11, so it is cheap. Changing double precision to numeric is not binary-coercible, so
alter column ... typerewrites every partition under ACCESS EXCLUSIVE and rebuilds its indexes; on this volume that is hours of blocked ingest and is simply not an option, which is why the plan is expand-and-contract rather than one statement. - Expand: add
quantity_numeric numeric(20,6)andenvironmentwith its default on the parent. Both are catalogue-only but both take a brief ACCESS EXCLUSIVE that cascades to partitions, so run each withlock_timeoutset to a second or two and retry on failure. A queued ACCESS EXCLUSIVE request blocks every reader behind it, which is how a metadata-only change turns into an outage. - Dual-write: deploy producer code that populates both columns on every insert, and leave it running before anything reads the new column. This is the step that cannot be reverted cleanly. Once readers depend on quantity_numeric, reverting the writer leaves rows with a null there, and the gap is only discoverable by re-reading the old column, which the readers have stopped doing.
- Backfill older partitions in batches keyed on the primary key, oldest first, committing every few thousand rows with a pause between batches, and skipping the partition still receiving writes until it rotates. Each batch is an ordinary UPDATE taking row locks only. The cost is bloat and WAL rather than blocking, so watch dead tuples and let autovacuum keep pace instead of wrapping 400 partitions in one transaction.
- Make NOT NULL cheap with the three-step form:
add constraint ... check (environment is not null) not valid(brief ACCESS EXCLUSIVE, no scan), thenvalidate constraint(SHARE UPDATE EXCLUSIVE, scans while reads and writes continue), thenset not null, which from PostgreSQL 12 uses the validated check and skips its own full scan. Do this per partition, then on the parent. - Switch and contract: move reads to the new column behind a flag, verify over a full period that both columns agree on freshly written rows, drop the old column (metadata-only), and only then remove the dual-write. Any index on the new column goes on with CREATE INDEX CONCURRENTLY per partition, since CIC is not supported on a partitioned parent: create the parent index with ONLY, build each child concurrently, then ALTER INDEX ... ATTACH PARTITION until the parent index becomes valid.
Worked solution (45 min)
- On a scratch cluster, build 10 partitions of 2M rows each and run a writer at a few thousand inserts/second.
- Run the naive type change and measure how long writes stall and how far ingest lag grows before killing it.
- Run the expand step with
lock_timeout = '2s'while the writer runs, and observe a clean lock timeout and retry instead of a pile-up of blocked readers. - Backfill in 5k-row batches and chart dead tuples and WAL generated per batch.
- Run the not-valid, validate, set-not-null sequence and confirm from
pg_stat_activityand timings that nothing held an exclusive lock through a full scan. - Add an index with CIC per partition plus ATTACH PARTITION and confirm the parent index reports valid only after the last attach.
Expected result: An ordered expand, dual-write, backfill, switch, contract plan with the lock named per step: metadata-only ADD COLUMN under a brief ACCESS EXCLUSIVE taken with lock_timeout; batched UPDATE backfill under row locks; CHECK NOT VALID, then VALIDATE under SHARE UPDATE EXCLUSIVE, then SET NOT NULL; CREATE INDEX CONCURRENTLY per partition with ATTACH PARTITION; and the dual-write-to-read switch identified as the step that cannot be reverted cleanly.
Verify it yourself
- No step in the plan holds ACCESS EXCLUSIVE for longer than the configured lock_timeout.
- The writer's error rate stays at zero through the rehearsal apart from the deliberate lock timeout.
- After VALIDATE, SET NOT NULL completes in milliseconds on a table where the naive form scanned every row.
- Freshly written rows agree on both columns for a full verification period before the old column is dropped.
Follow-up
- A CREATE INDEX CONCURRENTLY fails halfway through the partition list. What state is the table in, how do you detect it, and what do you run?
- The producer computes quantity itself. What happens to a request already in flight when the dual-write deploy lands, and does it matter?
- Give two queries that prove the backfill is complete: one cheap enough to run every minute, one authoritative.
Design a machine-readable error contract for the gateway
Easy · error taxonomy · retries · api contract
The edge gateway serves roughly 30k requests/second to SDKs and CI pipelines that retry automatically. Today every failure returns 500 with a prose message that clients string-match on. Design the error contract: the response body fields, and the status code for a malformed body, a revoked credential, a scope the credential lacks, a row belonging to another tenant, a reused idempotency key sent with a different body, an exceeded rate limit, and an unreachable dependency. For each, state whether the client may retry and on what schedule. Deliverable: the envelope schema plus the status-to-retry table.
Approach
- Split the envelope by audience: a stable
codestring for programs, amessagedocumented as human-only and free to change, arequest_idthat joins to gateway logs, and adetailsarray for per-field problems. The code list is an enum that only ever grows. - Assign status by who has to change something: 400/422 for the caller's bytes, 401 for a credential that no longer authenticates, 403 for a scope or entitlement, 404 rather than 403 for a row in another tenant because 403 confirms the identifier exists, 409 for an idempotency conflict, 429 for a limit, 503 for a dependency.
- Derive retryability from the method and the idempotency key rather than from the status: a 5xx or a timeout is an unknown outcome, not a failure, so GET/PUT/DELETE may be retried under HTTP semantics and POST only when it carries an idempotency key.
- Put the schedule in the response: Retry-After on 429 and 503 overrides the client's own backoff; otherwise capped exponential backoff with full jitter, sleeping uniformly in [0, min(cap, base * 2^attempt)], bounded by a total attempt budget so retries expire before the caller's deadline.
- Write the negative rules into the published contract: clients must never parse
message, must tolerate unknowncodevalues by falling back to the status class, and a code's meaning is never redefined once shipped.
Follow-up
- A customer reports they retried a 500 from POST /v1/runs and ended up with two sandboxes billed. Whose bug is it, and what in your contract permits their reading?
- You need to add a new error code next quarter without a version bump. What did the v1 contract have to say for that to be non-breaking?
Authorisation cache with a bounded revocation window
Hard · caching · invalidation · availability · authz
The edge gateway serves about 30k requests/second from roughly 120 pods across three regions and may add no more than 10 ms at p99. Each request presents an API key that must resolve to an authorisation context: tenant, workspace, scopes, entitlements and credential version. The control plane that owns those rows takes tens of writes/second. A revoked credential must stop authorising within a bound you state as a number. Design the cache - what is keyed, what invalidates it, how many tiers - and specify what the gateway does for the duration of a control-plane outage.
Approach
- Fix the entry shape before the topology: key on SHA-256 of the presented secret, value is the resolved context plus the principal's auth_version and a fetched_at. Cache negative lookups too, with a much shorter TTL and a bounded-size structure, because otherwise every sprayed invalid key is a control-plane round trip, and unbounded negative entries let a sprayer evict live ones.
- Compute the control-plane read load before choosing a TTL, and notice the multiplier is pods, not regions, when the cache is in-process: distinct_active_keys x pods / TTL. At 50,000 active credentials, 120 pods and a 60 s TTL that is 100,000 reads/second against a single-writer primary with read replicas, which is not serviceable - so the design needs two tiers, a per-region shared cache in front of the control plane with the in-process cache held to a few seconds.
- State the bound as the sum of the tiers, not as a hope: with a 10 s in-process TTL over a 60 s regional TTL, worst-case staleness absent any invalidation message is 70 s, because an in-process entry can be filled from a regional entry that was itself about to expire. Publish-subscribe invalidation on every credential and entitlement mutation makes the typical case sub-second, but it is lossy under partition, so the TTL is the only enforced bound and both tiers must subscribe.
- Make auth_version propagate through the same path: a password reset or sign-out-everywhere bumps the principal and revokes its keys with no hook of its own, so the invalidation publisher has to expand principal -> credentials and publish per key, or the cache keeps serving keys whose auth_version no longer matches.
- Decide the partition behaviour in advance and write it as two rules: on a cache hit past TTL, serve from the stale entry up to a grace ceiling (say 10 minutes); on a cache miss, refuse, because authorising something never seen converts a control-plane outage into an authorisation bypass. Worst-case revoked-key lifetime during an outage is then TTL + grace, about 11 minutes, and that number is the price of not turning a control-plane outage into a total data-plane outage.
- Protect the refill path: per-key single-flight so a mass invalidation or a cold pod does not stampede the control plane, TTL jitter so entries created together do not expire together, and a small separately replicated deny-list for compromised keys that is consulted on the hot path and survives control-plane loss.
Worked solution (35 min)
- Write the cache entry shape - key, value fields, and which of those fields a request actually reads on the hot path - and mark which field makes a password reset propagate.
- Compute control-plane reads/second for TTLs of 10 s, 60 s and 300 s using distinct_keys x cache_instances / TTL, once with cache_instances = 3 regions and once with cache_instances = 120 pods, and note which of the two the in-process design actually implies.
- Enumerate the four states a revocation can be in - published and received, published and dropped, control plane unreachable, pod started after the publish - and write which entry serves the next request in each.
- Write the outage policy as two rules (hit past TTL within grace: serve; miss: refuse) and compute worst-case revoked-key lifetime as the sum of both tier TTLs plus the grace.
Expected result: A two-tier design with a single stated number: roughly 10 s in-process over 60 s regional, so about 70 s worst-case staleness without invalidation and typically under a second with it, plus a 10-minute stale-serve grace giving about 11 minutes worst case during a control-plane outage; fail-open on a warm hit, fail-closed on a miss; a separately replicated deny-list for compromised keys; per-key single-flight and TTL jitter on the refill path.
Verify it yourself
- Recompute the read load with pods rather than regions as the multiplier. If the number exceeds what read replicas can serve, a single in-process tier is not a viable design and you need a shared tier, a longer TTL, or routing that gives each pod a slice of the key space.
- Trace a password reset end to end. If your invalidation publishes only on credential rows, the reset revokes nothing and the auth_version field in the cache entry is decorative.
- State the outage behaviour as a decision already made rather than as an alert. If the answer is a human paging someone, the availability target for the control plane has not actually been set.
Follow-up
- A key is found in a public repository and must stop working in seconds, not minutes. What changes, and what does it cost on the request path?
- One region is partitioned from the control plane while the control plane itself is healthy. What do that region's pods do, and how do you distinguish this from a control-plane outage?
- How would you measure the actual revocation bound in production rather than asserting it from the configuration?
How would you approach building a ZTNA VPN solution?
Medium · debugging and production issues
Approach
- Check the instrumentation before believing the symptom.
- Pick a bisection that eliminates candidates whichever way it turns out.
- Say what evidence would prove you wrong, then go and look for it.
Follow-up
- How would you tell a cause from a coincidence here?
- What would you look at first, and what would it rule out?
How do you structure Express endpoints for scalability and security?
Medium · debugging and production issues
Approach
- Establish what changed and when, before forming any theory.
- Pick a bisection that eliminates candidates whichever way it turns out.
- Say what evidence would prove you wrong, then go and look for it.
Follow-up
- How would you tell a cause from a coincidence here?
- What would you look at first, and what would it rule out?
Given a code snippet, can you identify and debug the performance bottleneck or logic error?
Medium · debugging and production issues
Approach
- Separate the trigger from the cause; the deploy is rarely the bug.
- Establish what changed and when, before forming any theory.
- Pick a bisection that eliminates candidates whichever way it turns out.
Follow-up
- What would you add now so this is faster to diagnose next time?
- How would you tell a cause from a coincidence here?
Can you explain the syntax and common methods used in JavaScript?
Medium · debugging and production issues
Approach
- Say what evidence would prove you wrong, then go and look for it.
- Separate the trigger from the cause; the deploy is rarely the bug.
- Check the instrumentation before believing the symptom.
Follow-up
- How would you tell a cause from a coincidence here?
- What would you add now so this is faster to diagnose next time?
Suggested practice checklist
Day one measures instead of guessing, under a fixed rubric, and the remaining hours are allocated in proportion to the gaps before any studying begins. The allocation is deliberately not renegotiated midweek, because the area that feels worst on day three is usually the one that is moving.
Day 1 — Diagnostic, scored before you study anything
- Sit a 110-minute diagnostic in four blocks: forty-five minutes on two coding problems, twenty-five on one design prompt taken to interface and data model, twenty of short-answer fundamentals, and twenty delivering two behavioural answers aloud.
- Score each block from 0 to 3 on a fixed rubric where 3 is correct and fluent, 2 is correct but slow or prompted, 1 is partially correct and 0 is stuck, grading the artifact rather than how the attempt felt.
- Allocate days two to five in proportion to 3 minus each block's score, write the allocation down, and commit to leaving it alone.
Deliverable: A scored rubric and a fixed hour allocation for the rest of the week.
Day 2 — Largest gap: find the boundary rather than the subject
- Split the weakest area into named sub-skills and rate each separately. For coding those are restating the problem, choosing the structure, stating the invariant, turning the invariant into loop bounds, handling empty and single-element input, and accounting for complexity out loud.
- Attempt three items positioned just above where the rating drops off, and for each write the first move you failed to make.
- Re-attempt one of them from blank four hours later with nothing open.
Deliverable: A sub-skill map with the two blocking sub-skills circled.
Day 3 — Drill the blocking sub-skill by repeating the shape
- Do eight short repetitions of the same shape rather than eight different problems, so what gets practised is the pattern and not the puzzle.
- State the rule you now hold in one sentence, then test it against a case built to break it, a sliding window over an array containing negative values, or a cache-aside read path whose invalidation message is dropped.
- Have someone else read your one-sentence rule and find the precondition you left out.
Deliverable: One rule statement with its preconditions attached and one counterexample that would have caught the incomplete version.
Day 4 — Second gap, plus maintenance on the strongest area
- Run the same sub-skill decomposition on the second-largest gap in half the time.
- Spend twenty-five timed minutes on the block you scored highest, choosing the hardest item you can still finish rather than a warm-up.
- Write whether each area fails you on recall, on setup, or on execution, and set the fix accordingly: repetition for recall, a written checklist for setup, timed work for execution.
Deliverable: A second sub-skill map plus a one-line failure diagnosis for each area.
Day 5 — The gap that is not a skill
- Record one technical and one behavioural answer, then count two things in the playback: seconds before your first clarifying question, and sentences you began without knowing where they would end.
- Practise saying that you do not know, followed by how you would find out, without letting it soften into a guess, and practise stating a complexity or an estimate before being asked for it.
- Redeliver one answer under a hard ninety-second cap, which forces structure ahead of detail.
Deliverable: Two recordings with a counted reduction in time-to-first-question.
Day 6 — Retest under day-one conditions
- Sit the same 110-minute structure with new prompts of comparable difficulty and score it on the identical rubric.
- For any block that did not move, change the method rather than adding hours: a block stuck at 1 usually means the practice was too varied, not too short.
- Write down which single block you would still lose the offer on.
Deliverable: A second scored rubric placed beside the first, with one named remaining risk.
Day 7 — Full loop under interview conditions
- Run a sixty-minute mock over the two blocks that moved least, with an interviewer briefed to interrupt and change direction mid-answer.
- Write the recovery script for going blank: restate the question, state your assumption, name the first thing you would check.
- Say every rule from the week aloud without reading it, and cut any you cannot state in a single sentence, since a rule you have to reconstruct mid-answer will not survive an interruption.
Deliverable: A one-page card holding the recovery script and only the rules you could state from memory.
Bounding a vague ticket before writing code
When the requirements were thin, the interesting part is how you fenced the problem off: the assumption you wrote down, who you got to confirm it, the narrow version you shipped first so the rest stayed cheap to change. Guessing and being right is luck. Guessing in writing, where someone could correct you, is method.
How do you ensure your code remains maintainable and well-documented for the rest of the team?
Medium · behavioural and engineering judgement
Approach
- Pick a story where you made the decision, not one where you watched it.
- Give the blast radius: what could have broken, and what you measured.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
Can you walk through a challenging technical project you led?
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- Give the blast radius: what could have broken, and what you measured.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
Argue against failing open when the control plane is unreachable
Hard · revocation · cache staleness · fail-open vs fail-closed · influence
The gateway caches credential-to-context decisions with a sixty-second TTL. A design proposal says that when control-plane reads fail, pods should keep serving from expired entries indefinitely so a control-plane outage never becomes a product outage. You believe that converts every revocation into an unbounded one. Describe a design you argued against while it was still a live proposal: what you measured or modelled to make the case, what you conceded, who decided, and what happened afterwards. Say what would have changed your mind before the decision, not after it.
Approach
- Reframe it from a values argument into a bounded-staleness argument. Both sides already accept the cache; the disagreement is only about the ceiling on how long a revoked credential keeps authorising. Put a number on the table — serve stale for up to fifteen minutes, then fail closed — and make the other side argue against a number rather than against a principle.
- Bring arithmetic rather than adjectives: the rate of revocations with revoked_reason in ('suspected_leak','auth_version_bump'), the observed distribution of control-plane unavailability, and the product of the two, which is expected requests served by revoked credentials per outage-hour. At 30k requests/second the unbounded version is not a subtle exposure and the number says so.
- Concede the strong half of the opposing case first, because that is what buys you the room: failing closed turns one service's outage into a total outage across three regions, and a control plane doing tens of writes per second is not engineered to the gateway's availability target. A proposal you have not steelmanned reads as reflex.
- Propose the asymmetry that usually resolves this: stale entitlements cost bounded money (a quota fifteen minutes out of date over-serves by a computable amount), while a stale revocation costs unbounded access. Split the cached decision by what it authorises, give the two halves different staleness ceilings, and let the entitlement half fail open while the revocation half fails closed.
- State the propagation dependency plainly, since it is the part that is missed: validity is also derived from the principal's auth_version, so password reset and sign-out-everywhere flow through this same cache. A design that bounds staleness for explicit revocation and not for auth_version bumps has only fixed half of it.
- Say who decided, and what you did afterwards in either outcome: write the decision down with its number and a review date, and instrument the exposure you were worried about so the next round of the argument is settled by data instead of by seniority.
Follow-up
- Publish-subscribe invalidation is lossy under a partition, and a TTL is the only hard bound. What TTL do you pick, and what does it cost you at 30k requests/second?
- The key was revoked because it was found in a public repository. Does your answer change, and where does that urgency live in the design?
- You lost the argument and six weeks later the failure you predicted happens. What do you say in the review, and what do you not say?