Interview guides · Decagon
Decagon Software Engineer Interview Guide
Decagon Software Engineer interview guide: reported coding, concurrency and system design questions, what each round covers, and a 7-day prep plan.
Interviewing at Decagon
- Practice bank: 23+ questions
- Rounds: 2
- Typical prep: 1–2 weeks
- Interview reports: 3
Interview rounds
| Round | Questions |
|---|---|
| Technical Screen | 16 |
| Onsite | 7 |
How hard is the Decagon Software Engineer interview?
| Difficulty | Share | Questions |
|---|---|---|
| Easy | 9% | 2 |
| Medium | 74% | 17 |
| Hard | 17% | 4 |
What Decagon actually tests for
| Topic | Share | Questions |
|---|---|---|
| Coding & Algorithms | 43% | 10 |
| Software Engineering Fundamentals | 22% | 5 |
| System Design | 17% | 4 |
| Behavioral & Leadership | 9% | 2 |
| ML System Design | 9% | 2 |
The 60-second brief
Decagon builds a conversational AI platform: AI agents that handle customer inquiries for brands across chat, email and voice. Software Engineers work on the systems behind those agents. The role material names core infrastructure, data pipelines, ML serving, agent orchestration and real-time telephony as problem spaces, along with keeping p95 and p99 latency low for LLM inference.
Decagon candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
What a Software Engineer does at Decagon
The listed responsibilities include designing execution engines that coordinate model reasoning and actions, working with research, product and machine learning teams, building evaluation tooling for agent behavior, taking part in on-call rotations, setting SLOs and writing runbooks, and managing infrastructure as code with Terraform and GitOps. The role is described as in-office, and Python, TypeScript and JavaScript are the languages it names.
The reported questions follow the same themes. Coding leans toward stateful classes, such as a CSAT tracker over a time window and a recipe shopping cart with undo and redo. There are also concurrency and async questions, plus system design prompts on real-time messaging, multi-provider LLM routing, telemetry streaming and customer-managed air-gapped deployments. Prepare for those categories before you prepare for generic puzzle volume.
The Decagon interview process
Decagon candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Recruiter Screening
Half of this call is the part candidates treat as small talk: start date, notice period, work authorisation and its timing, location and time zone, on-call, and the number. Those are what kill offers late, after several engineers have each spent a day. Surfacing a hard constraint now costs you nothing and occasionally buys you something, since a loop compressed to fit a competing deadline can usually only be arranged if it is asked for early. The common failure is deflecting the compensation question twice, then discovering at offer stage that the band never reached your number.
What it evaluates
- Whether your hard constraints are compatible with the role before a loop gets booked: earliest start, notice period, what authorisation you hold and when it needs action, days on site, willingness to carry a pager
- Whether you give a compensation range with something behind it, such as current total compensation or a competing timeline, rather than leaving the band untested
- Whether your stated timeline is real, since a competing deadline raised now is something scheduling can sometimes work around and the same deadline raised at offer stage usually is not
How to prepare
- Write each constraint down in one line before the call and state them as facts rather than negotiating them live under a question you were not expecting
- Set your range from two or three current data points for that level and location, and name the structure you are quoting in, so the number is comparable to the one they are holding
- If another process is running, say where it stands and by when, and ask directly whether this loop can be scheduled inside that window
Technical Assessments
Candidates describe this stage as a series of coding rounds covering data structures and algorithms, threading, and language internals, done in a shared CoderPad-style editor. Many of the reported coding questions are stateful class designs rather than one-shot puzzles. Examples include a CSAT tracker that records unique conversation IDs and scores with strictly increasing timestamps and reports averages or percentiles over an active window, and a recipe cart with discounts and undo/redo. Get a correct, simple version running first. Then state its cost and improve the operation that dominates. Before you call it finished, test boundary cases such as an empty window, an entry exactly at the window edge and a repeated ID.
What it evaluates
- Whether each class operation stays cheap as state grows, for example evicting expired entries from the front of a window instead of rescanning all history on every query
- Whether you can say what state is shared between threads, where the critical section is, and which synchronization primitive closes the race in your chosen language
- Whether async code in Python, TypeScript or JavaScript avoids blocking the event loop and limits how much work runs concurrently
- Whether you resolve ambiguous rules (inclusive window edges, duplicate IDs, empty results) by stating a decision rather than leaving them implicit
How to prepare
- Implement the CSAT tracker and the recipe cart from scratch in a plain editor with no autocomplete, then extend the cart with nested begin, commit and abort as the bank variant asks
- Write a lost-update race in your language and fix it. Then explain aloud what the event loop does when a coroutine or promise awaits, and what happens when a synchronous call never yields
- For every class you write, keep a fixed list of test calls (empty, single element, boundary timestamp, repeated ID, undo after redo) and run through it before saying you are done
Onsite Interview Loop
Candidates describe the onsite as covering system design and domain-specific challenges. The reported design questions are a real-time messaging app similar to Slack, a high-throughput multi-provider routing system for LLM inference, a telemetry and streaming data platform for customer-facing analytics and batch processing, and support for customer-managed air-gapped deployments. The reports do not tie these to particular onsite interviews, so prepare them as one pool. Behavioral questions in the reports cover owning a large project end-to-end, prioritizing competing demands, and why you should be hired.
What it evaluates
- Whether you fix requirements, traffic shape and latency targets before drawing components, and name where p95 and p99 are measured
- Whether the design covers failure paths: a provider that slows down rather than fails, a client that reconnects mid-conversation, a stream consumer that falls behind, an upgrade on a network with no outbound access
- Whether the scale, team size and timeline you give for a project stay the same when that project comes up again in a later conversation
How to prepare
- Design multi-provider LLM routing with per-provider timeouts, health tracking, a fallback order and a retry budget, and explain how one slow provider is kept from using up the whole latency budget
- Design the Slack-like messaging system with connection management, per-channel ordering, persisting a message before acknowledging it, and resuming from a last-seen message ID after reconnect
- Write a one-page sheet for the project you will use in behavioral answers, with its figures, your decisions and what broke, and say it aloud until the numbers come out the same every time
Questions Decagon candidates report
Data structures and algorithms
One structure rarely covers the whole operation set, and the interesting questions in this category are the ones where it does not. When you need constant-time lookup and constant-time reordering at once, or the minimum plus deletion of an arbitrary element, the answer is two structures that index each other: a map from key to the node holding it, beside the list or heap that maintains the order. What separates a strong answer is the invariant between them, stated explicitly. Every operation has to leave both halves consistent, and the bug people ship is a removal from one that leaves a stale entry in the other.
- Implement a shopping cart system for a cooking application with methods to add recipes, remove recipes, calculate total discounts based on ingredient quantities, and support undoing and redoing cart operations.
- Design a Customer Satisfaction (CSAT) tracking class with a fixed score range of 1 to 5, including a time window size, a method to record unique conversation IDs and scores with strictly increasing timestamps, and a way to calculate average scores or specified percentiles within active windows.
- Write production-ready code in Python, TypeScript, or JavaScript that handles asynchronous operations cleanly.
System design and architecture
At any point in a design you should be able to name the one resource that gives out first and what it costs to move it. Most follow-ups push on exactly that: traffic rises tenfold, a region is lost, a stricter consistency requirement appears. Answers hold up when the assumptions were said out loud early, because the design can then be re-derived from them rather than defended. The weak move under pressure is to add a component. The strong one is to say which part absorbs the change, which part has to be rebuilt, and roughly what the rebuild costs.
- Design a real-time messaging application akin to Slack, detailing message routing, data storage, and connection management.
- Design a telemetry and streaming data platform that handles customer-facing analytics and batch processing efficiently.
- Discuss how to support customer-managed, air-gapped deployments with robust observability and upgrade paths.
Languages, concurrency and fundamentals
Before choosing threads, processes or async, classify the work by which resource is saturated. CPU-bound work needs real parallelism, so it needs cores, and a runtime that serialises bytecode execution will not provide any however many threads you start. I/O-bound work is waiting rather than computing, and concurrency alone fixes it: one thread or one event loop can hold thousands of open sockets. Weak answers reach for threads by default. Strong ones name the bottleneck first, then the model, then what the model costs, which is either shared memory and data races or isolated memory and serialisation on every hand-off.
- Describe how asynchronous programming patterns behave under high load in Python or TypeScript.
- Tests your deep understanding of language runtimes, threading models, and low-level system performance.
Behavioural and engineering judgement
Ownership questions here usually point at something with no owner and no deadline: an alert people mute, a suite everyone reruns until it passes, a service whose on-call is a rota of volunteers. Nothing forces action, so what you did says how you spend discretionary time. The strongest answers are not the ones where you fixed everything. They are the ones where you sized the problem first, chose between fixing it, containing it and writing it down for whoever comes next, told someone who could have overruled you, and can still say why the other two options were worse.
- Tell me about a time you owned a large technical project end-to-end and handled unexpected roadblocks.
Practice exercises with worked solutions
The prompts above tell you what gets asked.
Rebuild an hourly rollup with deduplication and late-arrival accounting
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.
How a strong candidate works it
- 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.
Expected result (40 min): One query: a dedup CTE using row_number() over (partition by tenant_id, idempotency_key order by ingested_at, event_id); aggregation by (workspace_id, sku, hour_start) producing quantity_sum, event_count and max(ingested_at) plus a filtered late-quantity aggregate; a running total as a window over the aggregated rows; and an explicit statement that the partition-scoped daily unique index cannot catch a duplicate that crosses the ingest-day boundary or arrives in a later replay.
Check yourself
- Row count after dedup equals raw count minus injected duplicates, exactly.
- The running total at the final hour equals the ungrouped day total for that (workspace, sku).
- Two consecutive runs over static data return byte-identical rows.
- Every hour_start is hour-aligned in UTC and unchanged by the session's TimeZone setting.
Follow-ups
- 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?
Decide which facts an invoice line copies instead of joining
invoice_line_item already denormalises tenant_id, which is reachable through invoice_id, and stores amount_minor even though quantity times unit_price_micros would recompute it. A reviewer asks you to normalise both away, and separately asks whether the tenant's legal name and billing address should be copied onto the invoice header. Decide each case. For every field you keep denormalised, name the read pattern or the invariant that justifies it, the anomaly the copy can develop, and the mechanism that prevents that anomaly here.
How a strong candidate works it
- Split the question into two kinds of copy, because they fail differently. A copy of a currently mutable fact is a cache: it drifts and needs invalidation. A copy of a fact frozen at write time is not a cache at all, it is the record of what happened, and normalising it away destroys information the source no longer holds.
- Keep tenant_id on the line. It costs 8 bytes, it leads every index on the table so no read is ever accidentally cross-tenant, and it turns a wrong join into an empty result rather than another tenant's money. Prevent the drift structurally: a unique constraint on invoice (invoice_id, tenant_id) plus a composite foreign key from the line on (invoice_id, tenant_id) makes a mismatched pair impossible, so the database enforces agreement instead of a code review.
- Keep amount_minor. Rounding must happen exactly once, at a named site, with a stated mode (half-even here). If readers recompute from quantity and unit_price_micros, every reader owns a rounding decision, and half-up and half-even diverge systematically across thousands of lines rather than cancelling out. A check constraint can bound the stored value but deliberately cannot re-derive it.
- Copy the legal name and billing address onto the invoice header, written once and never updated. The statement must show what was true when it was sealed, and the tenant record will change afterwards. This is a snapshot for the same reason
source_rollup_watermarkis stored per line: without it, nobody can reconstruct what the customer was told. - Name the read pattern that pays for all of it. Rendering, dispute response and export are per-tenant, per-period reads over thousands of lines that would otherwise join back to slowly changing dimensions that no longer hold the historical value. The write side is a once-per-period batch, so the extra columns cost nothing that matters.
- Concede the case where the reviewer is right: a mutable operational attribute such as the tenant's current plan name has no business on a line. If a report wants it, join. If a statement needs the plan as of the period, that is another snapshot and it belongs on the header with the rest.
Follow-ups
- Write the composite foreign key and the unique constraint it requires on the parent. What does it cost on every line insert, and what does it do to a bulk load?
- A tenant is renamed after being invoiced. Which rows change, and what does the customer see on last quarter's PDF?
- Where does currency live, and what breaks if a tenant's billing currency changes between two periods?
Seal an hour under late data with bounded memory
Metering ingest reads 256 partitions at 10,000 to 40,000 events/second. Events carry occurred_at and ingested_at, and during a producer replay the gap between them is hours. Seal each UTC hour once no more than 50 parts per million of that hour's eventual quantity can still arrive, using memory that does not grow with the size of the replay. Define the watermark, the lateness parameter and how you measure it, the structure holding open hours, and the write that performs the seal. State what an idle partition does to your watermark.
How a strong candidate works it
- Two clocks, two jobs. Bucket by
occurred_at, because that is the hour the customer is billed for, and advance the watermark oningested_at, because that is what the fold has consumed and whatsource_max_ingested_atrecords. Conflating them is what makes late data invisible. - The global watermark is the min over partitions of each partition's committed
ingested_at, not the max: the fold is trustworthy only as far as the slowest partition. The consequence is that one idle partition pins the watermark forever and nothing seals, so an idle partition must promote its watermark to wall clock after a stated idle timeout, and that timeout becomes a correctness parameter, because a partition that is slow rather than idle gets sealed past. - Choose the lateness L from the measured distribution of
ingested_at - occurred_at, weighted by quantity rather than by event count. The target is 50 ppm of the hour's quantity, and a replay is rare in events while carrying disproportionate mass, so an event-weighted quantile picks an L that is comfortably wrong at exactly the moment it matters. - Measure that quantile in bounded memory. A Greenwald-Khanna summary gives epsilon-approximate quantiles in O((1/epsilon) log(epsilon n)) space; a t-digest costs more per merge but has relative error that tightens at the tails, which is the half of the distribution you are reading at p99.99. Keep a separate summary per tenant class, because one tenant's batch importer is not the population.
- Hold open hours in a min-heap keyed by
hour_start. When the watermark advances, pop every hour withhour_end + L < Wand seal it: O(log H_open) per advance and O(1) amortised per event to touch its bucket. Memory is open hours multiplied by distinct(tenant, workspace, sku)keys, so cap the number of simultaneously open hours and spill the oldest intousage_rollup_hourlyasstatus='open'with arevisionbump. While an hour is open the row is upsertable, so the store is your overflow. - The seal itself is a conditional write:
update ... set status='sealed', sealed_at=now() where status='open' returning .... Two sealers race on every restart, and the loser must see zero rows and stop rather than write a second value. After the seal, an event for that hour is not an upsert but an adjustment, andsource_max_ingested_atis what proves it arrived afterwards.
Expected result (40 min): The quantity-weighted p99.99 is hours larger than the event-weighted one. Choosing L from the event-weighted number lets roughly the tail's 3% of quantity land after the seal, 600 times the 50 ppm target. With the min watermark and no idle timeout, the stalled partition blocks all sealing; with a 60-second timeout, the stall is sealed past and its events arrive late.
Check yourself
- Post-seal quantity as a fraction of the hour's total is at or below 50 ppm at the chosen L, measured rather than assumed.
- The second sealer's update reports zero rows affected and writes nothing.
- Heap size equals the number of open hours and does not grow with the replay's event count.
Follow-ups
- A replay starts during the sealing window for a period you are about to close. What do you do, and what is the customer-visible consequence of each option?
- Your measured quantity-weighted p99.99 lateness is six hours and the invoice must be issued at 02:00 UTC on the first. How do you reconcile those two numbers?
- How would you detect that L has drifted before it costs you an hour's quantity?
An idempotent create endpoint that returns a one-time secret
POST /v1/api-keys inserts a tenant_api_key row (tenant_id, workspace_id, name, key_prefix, secret_hash, scopes, status, auth_version, created_at, expires_at) and returns the plaintext secret exactly once, since only its SHA-256 is stored. Write volume is tens per second and clients retry on timeout. Design the idempotency mechanism: what the key is scoped by, where the record lives, its retention, what happens when the same key arrives with a different body, what happens when a retry arrives while the first request is still in flight, and what a replay returns for the secret.
How a strong candidate works it
- Scope the key by tenant, not globally: uniqueness is on (tenant_id, idempotency_key), or one tenant's key collides with another's and the second caller receives a stored response for a request it never sent. Store a fingerprint of the request alongside it - method, path and a hash of the canonicalised body - so a mismatch is detectable.
- Let the unique constraint decide the race instead of application logic. In the same transaction as the credential insert, run INSERT INTO idempotency_record (tenant_id, key, request_fingerprint, status) VALUES (...) ON CONFLICT DO NOTHING RETURNING id; no returned row means this is a replay, and the existing record is then read. A select-then-insert here loses to itself under concurrency in exactly the way this endpoint is meant to prevent.
- Write the three replay outcomes as a decision table rather than as prose: same fingerprint and completed returns the stored response; same fingerprint and still in flight returns 409 with Retry-After, without blocking and without executing; different fingerprint returns 422, because replaying a stored response for a mutated body would tell the caller a credential was created for parameters it never sent.
- Handle the secret as the part that makes this endpoint different from an ordinary idempotent create. The plaintext cannot be regenerated from secret_hash, so either the stored response holds it - making the idempotency record a secret at rest whose retention is now the secret's exposure window - or a replay returns the key metadata without the secret and the documentation says a lost response is resolved by listing keys and revoking the orphan. The second is the safer default precisely because credentials are listable and revocable.
- Set retention from the client's retry budget, not from a round number: the record must outlive the SDK's maximum total retry duration, so twenty-four hours is defensible if that budget is minutes. After expiry the key is reusable and a very late retry creates a second credential, which is acceptable here only because the object is listable and revocable, and would not be for an unlistable side effect. Expire with a scheduled delete on an index over created_at.
Expected result (20 min): Unique (tenant_id, idempotency_key); the record inserted in the same transaction as the credential with ON CONFLICT DO NOTHING deciding the winner; 200 with the stored response, 409 for in-flight, 422 for a fingerprint mismatch; a documented rule that the plaintext secret is not replayed and a lost response is resolved by listing and revoking; retention exceeding the SDK retry budget, for example twenty-four hours.
Check yourself
- Two concurrent requests with the same key must leave exactly one row in tenant_api_key. If the logic is select-then-insert, they will not.
- A retry with a mutated body must not return 200. If it does, the fingerprint is not being compared or is not covering the fields that matter.
- The retention you chose must be strictly longer than the client library's maximum total retry duration. Look that number up rather than assuming it.
Follow-ups
- Two requests with the same key arrive concurrently. Show the exact statements and say which one loses, and how it finds out.
- The client receives a timeout, retries, and gets 409 in-flight. What should the client library do next, and for how long?
- How does the design change if the created object is not listable - a one-off payout, say - so an orphan cannot be found afterwards?
A rare job-run overwrite that logging makes disappear
About one job run in fifty thousand records billable_seconds matching no observed sandbox lifetime, and a few rows show worker_id changing after finished_at was already set. It does not reproduce: debug logging around the terminal write made it vanish for two weeks before it returned. Runs last from 200 ms to 30 minutes, the lease is 60 seconds and is renewed while a run executes. Give an ordered checklist, the mechanism, and a fix that makes the illegal write impossible rather than merely rarer.
How a strong candidate works it
- Mine the evidence instead of chasing a repro: select rows where updated_at is later than finished_at, or where a terminal status was written twice, and join them to attempt history to recover both writer identities. The defect has already happened tens of times and the rows are the recording.
- State the signature before measuring it. If the cause is a lease that expired while the original worker was stalled, affected runs should cluster where the gap between the last renewal and the terminal write exceeds the lease, and should correlate with worker pause metrics rather than with workload shape.
- Read the disappearance honestly. Logging inside the window changed the timing and lowered the probability; it is evidence about how narrow the window is, not a fix. Reproduce by widening the window on purpose, shortening the lease and injecting a pause between sandbox exit and the terminal write, rather than by adding more instrumentation.
- Name the mechanism precisely: the lease expires during a stall such as a long garbage-collection pause or a brief partition, the run is re-dispatched, and the original worker then wakes and writes its terminal state over the new attempt's row. A lease alone cannot stop this, because the check and the write are separated by the stall.
- Fix by fencing the write itself: UPDATE job_run SET status = $2, finished_at = $3, billable_seconds = $4 WHERE run_id = $1 AND status = 'running' AND lease_token = $5, with zero rows affected interpreted as having been fenced rather than as success. The token lives on the row so the store arbitrates, not the worker's memory.
- Keep the state machine honest: a retry inserts a new row pointing at parent_run_id rather than resetting the old one, and a run whose worker vanished terminates as lost with billable_seconds null, because recording failure asserts an outcome nobody observed and then bills and retries on that assertion.
Follow-ups
- The supervisor also emits a usage event on completion. What does the fenced worker do about the event it already emitted, and how does metering absorb it?
- Lease renewal is itself a network call. What happens when a renewal times out, and how does the worker decide whether it still holds the lease?
- Why is lengthening the lease past the longest legitimate run the wrong lever, and what breaks if you do it anyway?
Estimate a tenant-leading index migration you have never run
Someone needs a date. usage_event carries an index on (occurred_at) and needs (tenant_id, occurred_at); the largest tenant holds roughly a hundred times the median tenant's rows, the table is partitioned daily with years of retention, and you have never run a migration on a table this large. Give an estimate you would defend: how you decompose the work, the two or three numbers you would go and measure first, the range and confidence you state, and what you commit to when the person asking needs a single date today.
How a strong candidate works it
- Refuse the bare number and then give one anyway, in the form that is actually useful: a range plus the measurement that collapses it. 'Four to eleven days; one afternoon building this index on a restored copy of the largest partition takes that to within a day' is an answer, while 'it depends' is not.
- Decompose by failure mode rather than into equal chunks, because that is where estimates go wrong. On a partitioned parent you create the index ON ONLY the parent, build each partition's index with CREATE INDEX CONCURRENTLY, then ALTER INDEX ... ATTACH PARTITION, at which point the parent index becomes valid. CONCURRENTLY does not block writes but scans each partition twice, waits out older transactions, cannot run inside a transaction block, and on failure leaves an invalid index you must drop concurrently and retry.
- Name the two unknowns that dominate and price them: build time on one restored partition of realistic size, and whether the planner actually chooses the new index for the skewed tenant, since selectivity for a tenant holding most of the rows is a different question from selectivity for the median tenant. Both are half-day measurements against a replica, and both are cheaper than being wrong by a week.
- State the assumptions the range is conditional on, because that is what makes a slip a re-estimate instead of a credibility event: no partition above a stated row count, one concurrent build at a time so it does not compete with ingest for I/O, and an ingest backlog that can absorb the added write amplification while both indexes exist.
- Budget the step nobody budgets: verification and the old index's removal. Dropping the old index is fast, but deciding it is safe to drop means confirming no plan still uses it, and that confirmation waits on real traffic across a full weekly cycle rather than on your patience.
- Answer the single-date request honestly. Commit to a date for the first checkpoint — the measured build number from the replica — and to re-estimating on that date, and say plainly what you are not committing to yet. A date with a scheduled re-estimate is worth more to the asker than a confident wrong one, and you should say why in those words.
Follow-ups
- The concurrent build fails half way through the largest partition. What is the state of the database and what do you do next?
- Your estimate slips by sixty percent. Which assumption broke, and at what point would you have known?
- The person asking needs the date for a customer commitment. Does your answer change?
Argue against failing open when the control plane is unreachable
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.
How a strong candidate works it
- 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-ups
- 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?
Where candidates lose points
Computing the CSAT tracker's average or percentile by scanning every stored score on each query
Timestamps arrive strictly increasing, so keep a FIFO queue of (timestamp, conversation_id, score) and evict expired entries from the front, updating a running sum and count as entries enter and leave. The prompt gives a fixed score range of 1 to 5 but does not say scores are integers, so ask. If they are integers, keep a five-bucket count array: a percentile is then a walk over five buckets. If they are not, keep the active scores in an order-statistic structure such as a sorted multiset or an indexed balanced tree, with O(log n) insert, delete on eviction and rank lookup. State your rules out loud: whether an entry exactly at the window edge is still active, what a repeated conversation ID does, and what an empty window returns instead of dividing by zero.
Letting redo survive a new cart operation, or undoing a removal without the quantities it removed
Record each cart mutation as a command with its inverse. A remove must store what it removed so undo can restore it exactly. Push commands onto an undo stack and clear the redo stack on any new mutation. Compute discounts from current cart state rather than from a cached total that undo does not reverse. Expect the bank follow-ups: nested begin, commit and abort (commit merges the inner change log into its parent, abort replays the inner log's inverses), and a space-efficient versioned cart, where you should compare full snapshots with a change log.
Treating async as parallelism and calling blocking code inside an async handler
In Python's asyncio and in the JavaScript event loop, a task runs on one thread until it awaits. A synchronous CPU loop or blocking I/O call stalls every other task, and the stall shows up as tail latency. Say how you would move that work off the loop (a thread or process pool, or worker threads) and how you would cap concurrency with a semaphore or queue so a traffic spike does not open unbounded connections. For threading questions, name the shared state and the critical section before choosing a lock.
Designing LLM routing or real-time messaging only for the case where every dependency works
Start from failure. For multi-provider LLM routing, cover per-provider timeouts, what marks a provider unhealthy and what brings it back, the fallback order, and a retry budget, because a slow provider is harder to handle than one that is down. For messaging, cover what happens to messages in flight when a connection drops, how a client resumes, and whether ordering is guaranteed per channel or globally. For air-gapped deployments, cover how upgrades and observability work without outbound network access.
Answering 'why should you be hired' with enthusiasm and no evidence
Tie the answer to one project you can walk through in depth. The bank includes a 'walk through one past project' question, so expect follow-ups on architecture, the decisions you made and what broke. Use the same figures every time the project comes up, and connect it to a problem space in the role material, such as low-latency serving, data pipelines or on-call reliability, without claiming experience you do not have.
Debugging and fundamentals, for someone arriving from a scripting background
For someone fluent in a dynamic language who has shipped real work but has never had to say what the runtime is doing underneath. The week is built on measuring and deliberately breaking things, because the questions that expose this background are the ones where the interviewer asks why a second time.
Day 1 — Recruiter screen prep and a plain-editor baseline
- Write your constraints for the recruiter screen in one line each: start date, location and days on site (the role is described as in-office), work authorization, and a compensation range with a basis behind it
- Pick one of Python, TypeScript or JavaScript and use it for every coding task this week
- Set up a plain editor with no autocomplete and solve two bank warm-ups: first non-repeating character and maximum-sum contiguous subarray, stating the complexity before you write code
Deliverable: A one-page constraints sheet and two warm-ups solved in a plain editor, each with complexity written above the code.
Day 2 — Time-windowed trackers
- Implement the CSAT tracker: record(conversation_id, score, timestamp), average over the active window, and a requested percentile, using a FIFO queue for eviction plus a running sum and count
- Before choosing the percentile structure, confirm with your interviewer (or decide and state) whether scores are integers in the fixed 1-to-5 range. If they are, use a five-bucket count array; if not, use a sorted multiset or order-statistic tree
- Write down your boundary rules before testing: inclusive or exclusive window edge, rejecting or replacing a repeated conversation ID, and the return value for an empty window
- Repeat the pattern on the bank variants: a sliding-window average tracker, a rolling-window conversation score tracker, and CSAT tracking with call timing
- Test each one with an empty window, a single entry, all-equal scores and an entry exactly at the window edge
Deliverable: One tracker class with amortized O(1) average, a percentile structure that matches the score type you confirmed (five buckets for integer scores, an order-statistic structure otherwise), and a written list of its boundary rules.
Day 3 — Stateful classes: undo, redo and transactions
- Implement the recipe cart: add and remove recipes, compute discounts from ingredient quantities, and support undo and redo through a command stack that clears redo on any new mutation
- Extend it with nested begin, commit and abort: a stack of change logs, where commit merges the innermost log into its parent and abort replays that log's inverses
- Write a short comparison of full snapshots against a change log for the space-efficient versioned cart, covering memory per version and the cost of restoring an old version
Deliverable: A cart class that passes the sequence add, add, undo, redo, new add, redo (no-op), plus nested begin, abort and commit tests.
Day 4 — Algorithms under time pressure
- Solve exclusive CPU time from nested logs with a stack of open calls: on a start event, pause the function on top of the stack; on an end event, credit the elapsed time and resume the caller. Test nested calls and back-to-back calls
- Count tic-tac-toe game sequences with backtracking that stops at a win or a full board, then memoize on board state and explain why the count from a position depends only on the board
- Work a skyline variation with a sweep over start and end events and a max-heap of active heights, writing the brute force first
- Time each problem and write down where you lost minutes
Deliverable: Three solved problems, each with its brute force, final complexity and the edge cases you tested.
Day 5 — Concurrency, async and runtime fundamentals
- Build a lost update with two threads incrementing a shared counter, show the wrong total, fix it with a lock, and explain why an increment is not atomic
- Block an asyncio or Node event loop with a synchronous call, measure the delay it adds to other tasks, then move the work off the loop and cap concurrency with a semaphore
- Explain the JavaScript event loop aloud, including when promise callbacks run relative to timers
- List the usual causes of memory leaks in a long-running Node or Python service (unbounded caches, listeners never removed, lingering global references) and how a heap snapshot confirms one
- Stretch: work through the debugging question 'A rare job-run overwrite that logging makes disappear' and write the fenced update
Deliverable: A race demonstration and fix, a measured event-loop stall and fix, and a one-page note on leak causes and how to diagnose them.
Day 6 — System design: the reported pool
- Design multi-provider LLM inference routing (or the bank's AI gateway) with requirements, per-provider timeouts and health, fallback order, a retry budget and where p95 and p99 are measured
- Design the Slack-like messaging app with connection management, per-channel ordering, persistence before acknowledgement, and reconnect and resume
- Sketch the telemetry and streaming platform, then check your event-time and late-data reasoning against the worked exercises 'Seal an hour under late data with bounded memory' and 'Rebuild an hourly rollup with deduplication and late-arrival accounting'
- For customer-managed air-gapped deployments, write the upgrade path and how observability data leaves the environment, if at all. Use the worked exercise 'An idempotent create endpoint that returns a one-time secret' to review retry-safe writes
Deliverable: Two full designs written up, each with a failure table listing the failure, how it is detected and how the system recovers.
Day 7 — Behavioral answers and a full mock
- Fill in a project sheet for 'walk through one past project' and 'a large project you owned end-to-end', with figures, your decisions and what broke
- Prepare answers to why you should be hired and how you prioritize competing engineering demands, each anchored to a specific example
- Run a mock: one coding class (the tracker or the cart) followed by one design prompt, with someone asking the project question twice to check the answers match
Deliverable: A recorded mock with notes on one coding bug you caught yourself, one design trade-off you named, and any project figure that changed between tellings.