Interview guides · 100ms
100ms Software Engineer Interview Questions & Guide 2026
A prep plan for 100ms software engineer candidates: answer first, read the solution second, repeat the misses later.
The 60-second brief
A Software Engineer at 100ms is at the heart of building the infrastructure that powers live video and audio experiences for millions of users. You are not just writing code; you are solving complex challenges related to real-time communication, low-latency streaming, and massive scalability. Whether you are working on the Core Infrastructure or building out new product features, your contributions directly impact how businesses connect their communities through high-quality, reliable video.
Treat capacity estimation as a conversion skill rather than a table to memorise: turn a user count and an action rate into requests per second and bytes per day, then name the component that number breaks first. The figure only matters if it changes the design.
Focus areas
- Key CDN caches without sharding on tokens
- Design a bitrate ladder under stated constraints
- Enforce entitlement at the licence boundary
Overview
This role is critical because 100ms operates in a high-stakes environment where every millisecond of latency matters. You will be expected to tackle distributed systems problems, optimize performance, and design robust architectures that can handle sudden spikes in traffic. It is an environment for engineers who thrive on deep technical problem-solving and want to see their work deployed in production environments that support global scale.
100ms values candidates who demonstrate a balance between strong technical fundamentals and the ability to apply those skills to real-world product constraints.
The 100ms interview process
100ms candidates report 5 rounds · ≈ 4-6 weeks. The stages below are what candidates describe, not a published process.
Initial Conversation
An unlabelled round is first an information problem, and the cheapest information is free. Whoever schedules it can usually tell you how long it runs, who will be in the room and what they work on, whether you will be writing code and in what environment, and whether anything is being sent beforehand. Ask in writing so the answer is on record, then prepare for the two or three formats those answers still leave open instead of betting on one. What separates a strong candidate is not guessing right; it is having an opening that works whichever one it turns out to be.
What to demonstrate
- Whether you can start work from an ambiguous brief, since tolerating a vague scope without stalling is the same thing the job asks for
- Whether the questions you asked beforehand were ones that change your preparation, such as duration, medium and who is joining, rather than ones whose answers you could not have acted on
- Whether you adapt when the round turns out to be something other than what you were told, instead of spending the first ten minutes visibly recalibrating
How to prepare
- Send one short scheduling message asking four things: how long, who is joining and what they work on, whether you will be writing code and where, and whether to prepare anything in advance. Treat a vague reply as real information, since it means the round is loosely structured and you will be shaping it yourself.
- Write one opening that works in any of the formats still open: restate in your own words what you have been asked to do, then ask which of two directions is more useful to them. Say it aloud until it stops sounding recited.
- Set up for the two most likely formats before the call starts, with a blank editor in the language you would choose and a shared document you can type into, so a format surprise costs you nothing in the first minutes
Data Structures and Algorithms
When a round has no standard shape, it is often there because something is still open: an area no earlier conversation reached, a round where the signal came out mixed, or a decision someone is not ready to make alone. Work out which by going back over what each earlier round actually covered rather than how it felt, and arrive able to give evidence on that point without being asked twice. Weak answers replay the loop's earlier material at the same depth. Strong ones go a level deeper and stay consistent with what you already said.
What to demonstrate
- Whether your account of a project matches the one you gave earlier in the loop, since what you said before may be available to whoever runs this round
- Whether you can go a level deeper on something already covered, reaching the decision and its alternatives rather than repeating the summary
- Whether you state your own uncertainty accurately, including parts of a system you did not build and decisions you inherited, instead of claiming even ownership across all of it
- Whether you can answer a question you handled poorly earlier by naming what you missed, rather than delivering a polished second version as if the first had not happened
How to prepare
- Reconstruct the loop on one page: for each round, the questions you were asked and the answer you actually gave, not the better one you thought of afterwards. The gaps on that page are your best available guess at why this round exists.
- Take the two claims you made earlier that carry the most weight and assemble the backing for each: the measurement, the date, what broke, the decision you would make differently now.
- Write down the three facts about your work that must not drift between tellings, such as team size, timeline and your own role, and check your stories against that list rather than trusting recall under pressure
Low-Level Design
You cannot drill a format you do not know, so put the preparation into material that travels. Three pieces of your own work, each rehearsed until you can take a follow-up you did not anticipate, will carry a conversation or a code walkthrough equally well. Specificity is what separates that from filler. A number needs its definition before it means anything: a p99 is over some window and measured at some hop, and a server-side figure excludes the queueing and network time a client would see. The number you cannot qualify is the one to leave out.
What to demonstrate
- Whether your examples carry detail only someone who did the work would hold, such as what the binding constraint actually was, which alternative you rejected and why it was worse, and what you measured on each side of the change
- Whether a number survives one follow-up, meaning you can say what it was measured over and whether it moved because of your change or merely alongside it
- Whether a failure is described with the specific change that followed it, rather than a lesson stated in general terms
- Whether your part in a team effort is stated accurately, including what other people did
How to prepare
- Write a page on each of three projects covering the constraint, the option you rejected, the measurement before and after, and what went wrong. Cut any line you cannot take a follow-up on, since you are writing the parts you will be pressed on rather than a summary.
- Recover the real figures while you still have access: request volume, data size, latency with its percentile and window, team size, timeline. Note where each came from, whether a dashboard, a design document or memory, and mark the estimates so you can say which they are out loud.
- Take your weakest project story to someone who works in a different area and have them ask why four times in succession. The point where you run out of answer is the part to go and re-read before the round.
High-Level System Design
This round has no published label, so start by asking the person who schedules it, in writing, how long it runs, who the interviewers are and what they work on, whether you will write code and in what environment, and whether any material is sent beforehand. Use those answers to narrow the format to two or three possibilities and prepare for each instead of betting on one. What separates a strong candidate is not guessing right; it is having an opening that works whichever one it turns out to be.
What to demonstrate
- Whether you can start work from an ambiguous brief, since tolerating a vague scope without stalling is the same thing the job asks for
- Whether the questions you asked beforehand were ones that change your preparation, such as duration, medium and who is joining, rather than ones whose answers you could not have acted on
- Whether you adapt when the round turns out to be something other than what you were told, instead of spending the first ten minutes visibly recalibrating
How to prepare
- Send one short scheduling message asking four things: how long, who is joining and what they work on, whether you will be writing code and where, and whether to prepare anything in advance. Treat a vague reply as real information, since it means the round is loosely structured and you will be shaping it yourself.
- Write one opening that works in any of the formats still open: restate in your own words what you have been asked to do, then ask which of two directions is more useful to them. Say it aloud until it stops sounding recited.
- Set up for the two most likely formats before the call starts, with a blank editor in the language you would choose and a shared document you can type into, so a format surprise costs you nothing in the first minutes
Take-Home Project
Nobody is in the room when this is read, so the submission stands in for what an ordinary pull request from you would cost to review. That makes navigability the first thing being judged. A reader should reach the code that does the actual work within a minute of cloning, without tracing three layers of indirection to get there. What most separates a strong submission from a weak one is separation itself: the logic that implements the prompt kept apart from file reading, argument parsing and printing, so each piece can be read, and tested, on its own.
What to demonstrate
- Whether the entry point makes the shape of the program obvious: what is read, what is computed, what is written, in that order
- Whether the core logic can be called without touching the filesystem, a socket or stdin, since that is what makes it testable at all
- Whether names hold up on a second reading; a function called parse that also writes, or a total that means something different from the README, costs the reader more than a longer name would have
- Whether the commit history reads as a sequence of steps someone could follow, rather than one commit containing everything
How to prepare
- Clone your own finished submission into an empty directory and read it in the order a stranger would, README then entry point then downward, noting every place you rely on something the code never told you, and fix those places
- Take an exercise you have already solved and restructure it so the module holding the rules imports no file, CLI or network library, leaving input parsing and output as a thin shell around it
- Build the history while you work, committing at points where the tests pass, so the log shows the order you actually solved the problem in
Where candidates lose points
Letting the live playlist grow with the DVR window.
A sliding-window playlist carries one entry per segment, so a four-hour DVR at two-second segments is 7,200 entries - at roughly 100 bytes per entry, about 720,000 bytes - and every viewer refetches the document about once per target duration. At 100,000 concurrent viewers that is 720 kB x 100,000 every two seconds, roughly 288 Gbit/s of playlist text that is nearly identical between fetches. Delta playlist updates and byte-range addressing solve it, but they have to be designed in, not discovered once the playlist has become the largest object being served.
Treating the manifest as the authorisation boundary.
Checking entitlement when the manifest is requested feels natural, because that is where the session visibly begins. But the manifest is a cacheable document and the segment URLs inside it carry no identity, so anyone who obtains it plays until the delivery token expires, and a subscriber who cancels mid-title finishes the title. Authorisation has to sit at the licence request, the only thing on the path that is per-user and uncacheable by construction.
Issuing one query per row of a result set
Fetch related rows in a single batched query keyed by the ids you already hold, or join them into the original query. A per-row round trip multiplies network latency by the row count, and it looks perfectly fine against the ten rows in your development database.
Reading the constraints as preamble rather than as part of the problem
The bounds are usually there to eliminate the obvious approach: n up to 10^5 makes an O(n^2) scan roughly 10^10 operations, far outside any per-test time budget, and an input larger than memory rules out loading it at all. When a bound is not given, ask for it, then say out loud which approach it kills.
Questions & practice
Problems involving binary search.
Medium · data structures and algorithms
Approach
- Name the brute-force solution and its complexity before improving on it.
- Walk one small example through your approach before writing the whole thing.
- Choose the data structure from the access pattern, not from familiarity.
Follow-up
- Which test case would catch an off-by-one here?
- How does this change if the input no longer fits in memory?
Trapping rainwater problem.
Medium · data structures and algorithms
Approach
- Name the brute-force solution and its complexity before improving on it.
- Walk one small example through your approach before writing the whole thing.
- Restate the input: its shape, its size, and what is guaranteed about it.
Follow-up
- How does this change if the input no longer fits in memory?
- Which test case would catch an off-by-one here?
Choose ladder rungs against a measured bandwidth distribution
Hard · dynamic programming · optimisation · cost modelling
You have 60 candidate encoding rungs sorted ascending by bitrate, and an audience bandwidth histogram of 1,000 buckets measured from delivered throughput. A viewer plays the highest rung whose bitrate is at most 80% of their sustained throughput, and cannot play at all if no rung qualifies. Choose exactly 12 rungs, always including the lowest candidate, maximising total delivered bitrate across the audience. Return the chosen rungs and the objective value, state the complexity, and say why a log-spaced ladder is not automatically the answer.
Approach
- Precompute for each candidate i the bucket at which viewers can first afford it, threshold_i = bitrate_i / 0.8, by binary search over the bucket edges: O(n log m). Build a prefix sum over bucket counts so any viewer-count range is O(1).
- Formulate the contribution: with the selected set sorted, if rung i is selected and j is the next selected rung, every viewer whose threshold falls in [threshold_i, threshold_j) plays rung i, contributing count * bitrate_i. Call it value(i, j), computable in O(1) from the prefix sums.
- Run the DP over (last selected rung, rungs used): f[j][t] = max over i < j of f[i][t-1] + value(i, j), with the lowest rung forced selected and a sentinel j = n+1 closing the final interval. O(n^2 k) = 60^2 * 12, about 43,000 transitions. The point is that the ladder is measured, not that the algorithm is clever.
- Say when the structure buys more: if value satisfies the concave quadrangle inequality, the inner maximisation admits divide-and-conquer optimisation at O(n k log n). That condition depends on the histogram's shape and must be tested on the real distribution rather than assumed.
- Name what the objective leaves out, because the interview point is the modelling: storage and cache footprint scale with rung count, device decode limits make some rungs unusable by whole client classes, and delivered bitrate is a crude proxy for perceived quality. Log spacing is a prior about a smooth unimodal distribution, and measured throughput is usually bimodal, which is exactly where the prior loses.
Worked solution (40 min)
- Write threshold_i and the prefix-sum lookup, and confirm the 80% headroom appears in exactly one place.
- Derive value(i, j) and check it by hand on a three-rung example against a two-bucket histogram.
- Write the DP recurrence with the forced lowest rung and the closing sentinel, and count the transitions at n = 60, k = 12.
- Compare the DP's chosen rungs against a log-spaced ladder on a deliberately bimodal histogram and quantify the difference.
- List the constraints the objective omits and say where each would enter the formulation.
Expected result: Twelve rungs plus the objective value, from an O(n^2 k) DP over sorted candidates with O(1) range counts (about 43,000 transitions at n = 60, k = 12) and the lowest candidate forced into the selection.
Verify it yourself
- Setting k = n reproduces the full candidate set and the maximum achievable objective.
- Deleting a candidate the DP did not select leaves the objective unchanged.
- The objective is non-decreasing as k increases from 1 to n.
Follow-up
- Add a per-device decode cap so one client class cannot use rungs above 1080p. Does the DP still decompose, and along which dimension?
- Throughput varies during playback and the player switches rungs. What does that do to the highest-affordable-rung model, and what would you measure instead?
- Storage and cache footprint become a hard budget rather than a fixed rung count. Which dimension of the DP state changes?
Serve a rights window where a null end means perpetual
Easy · null semantics · range predicates · index column order
availability_window(id, content_version_id, territory_code CHAR(2), licence_type ENUM('subscription','ad_supported','rental','purchase'), starts_at, ends_at NULL, max_resolution_cap NULL, status ENUM('draft','active','withdrawn')) carries INDEX (content_version_id, territory_code, starts_at, ends_at). A null ends_at means perpetual. Playback start asks: may this content version be served in this territory, on this licence type, at this instant, and under what resolution cap? Write that query. Then say which index columns actually narrow the scan and which are only filters, and what should happen when one row's ends_at equals another row's starts_at.
Approach
- Name the failure in the naive predicate first.
:now BETWEEN starts_at AND ends_atexpands to:now >= starts_at AND :now <= ends_at, and on a perpetual row the second comparison is:now <= NULL, which is unknown rather than true. Three-valued logic drops the row with no error, so every perpetual grant becomes unplayable and nothing in the system complains. - Write it as a half-open interval with an explicit null branch:
status = 'active' AND content_version_id = :cv AND territory_code = :terr AND licence_type = :lt AND starts_at <= :now AND (ends_at IS NULL OR ends_at > :now). Half-open means a renewal row starting exactly when the previous row ends matches exactly one row at the boundary instant rather than two or zero. - Read the index honestly.
content_version_idandterritory_codeare equalities and set the scan range;starts_at <= :nowis the first inequality and closes it.ends_atsits after a range column, so in a b-tree it cannot narrow the scanned range at all — it is evaluated against each index tuple in the range.licence_typeis not in the index, so it is a filter after the fetch; putting it beforestarts_atwould make it a third equality. - Put the index in proportion: a single (content version, territory) pair has a handful of window rows, so the range is tiny whatever the column order. The index exists so the lookup does not touch other titles, not to make the range tight — say that rather than over-tuning a predicate whose selectivity is already a few rows.
- Decide the multi-match case deliberately. Subscription and rental windows for the same content version can both be active, which is legitimate; two active rows for the same triple are the overlap data bug. The serve path should take the most restrictive
max_resolution_capand raise, because picking arbitrarily means two viewers in one territory get different caps from the same data.
Worked solution (20 min)
- Insert four rows for one content version: an active perpetual subscription row, an active rental row that ended yesterday, a draft row covering today, and an active row for a different territory.
- Run the BETWEEN form and record which rows come back.
- Rewrite with the half-open predicate and the explicit null branch, re-run, and confirm only the perpetual subscription row matches for the subscription licence type.
- Add a renewal row whose
starts_atequals the expired rental'sends_at, then query at exactly that instant for the rental licence type. - Run EXPLAIN on both forms and note which columns appear as index conditions and which as filters.
Expected result: The BETWEEN form returns zero rows for the perpetual grant. The half-open form returns exactly one row for the requested licence type, and at the boundary instant exactly one of the expired and renewal rows matches. EXPLAIN shows the two equality columns plus starts_at bounding the scan, ends_at listed as an index condition but not narrowing it, and licence_type applied after the fetch.
Verify it yourself
- The perpetual row is returned by the rewritten query and by no BETWEEN variant.
- At the boundary instant, the count of matching active rows for one (content version, territory, licence type) is exactly 1 — not 0 and not 2.
- A
status = 'draft'row never appears, whatever its dates say. - EXPLAIN (ANALYZE, BUFFERS) shows the scan reading only this content version's index entries, not the whole index.
Follow-up
- What constraint would stop two overlapping active rows for the same (content_version_id, territory_code, licence_type) from being written at all?
- The contract says the window closes at midnight local to the territory, but
starts_atandends_atare instants. Where does the local-time-to-instant conversion live, and what happens in a territory that shifts its clocks? - How stale is a cached answer to this query allowed to be, given that the cache TTL is the exposure window for a rights change?
Collapse heartbeats into plays with a gaps-and-islands query
Medium · window functions · gaps and islands · deduplication
heartbeat(session_id UUID, sequence INT, received_at TIMESTAMPTZ, played_ms BIGINT, playhead_ms BIGINT, profile_id, device_id, content_version_id) is delivered at-least-once and out of order. played_ms is a monotonic counter the client restarts at zero for each new session, and a crashed player resumes under a fresh session_id. Collapse one day of heartbeats into one row per continuous viewing island — same profile, device and content version, no gap longer than 60 seconds — with start, end and watch time. Write the query and state its cost.
Approach
- Deduplicate before any window function runs. A heartbeat's identity is
(session_id, sequence), so takeDISTINCT ON (session_id, sequence)ordered byreceived_at, or enforce it at ingest with a unique index andON CONFLICT DO NOTHING. Duplicates leave a MAX-minus-MIN watch time unharmed but corrupt any per-row delta and inflate row counts used as liveness signals. - Island on
(profile_id, device_id, content_version_id)ordered byreceived_at, not onsession_id. The crash-and-resume case is precisely the one the report has to merge, andsession_idchanges across it — partitioning by it guarantees the split you are trying to avoid. - Flag island starts with LAG —
CASE WHEN prev_at IS NULL OR received_at - prev_at > interval '60 seconds' THEN 1 ELSE 0 END— then number islands with a runningSUM(...) OVER (PARTITION BY profile_id, device_id, content_version_id ORDER BY received_at, sequence ROWS UNBOUNDED PRECEDING). Specify ROWS: with an ORDER BY and no frame clause the default frame is RANGE UNBOUNDED PRECEDING, which includes every peer row sharing the ordering value, so two heartbeats landing on the same instant would each absorb the other's flag. - Accumulate watch time from the counter, grouped by session inside the island: take
max(played_ms) - min(played_ms)per (island, session_id) and sum those. Never fromplayhead_msdeltas — a backward seek makes them negative and a forward seek makes them enormous. The naive version is correct for every session that never seeks, which is most of them, so it survives any test written from a happy-path trace. - Give the cost as O(n log n), dominated by the sort behind the partition and ordering. A b-tree on
(profile_id, device_id, content_version_id, received_at)lets the planner feed the window from an ordered index scan and drop the Sort node; without it, a day of heartbeats sorts, and at 10^5 events per second that sort spills to disk. - Handle the open island at the query's upper bound: an island whose last heartbeat falls within the gap of the boundary is still running, and should be emitted with a null end rather than truncated, or the next day's run counts the same viewing a second time.
Follow-up
- A device uploads four days of buffered offline heartbeats in one burst. Which islands are wrong, and does ordering by client timestamp instead of
received_atfix it or make it worse? - Make the 60-second gap configurable per device class. Does the query shape survive, and what happens to the index you relied on?
- How do you turn islands into
qualified_playrows so that re-running the job writes nothing new?
Design a real-time communication system component.
Medium · system design and architecture
Approach
- State the consistency you need, and where you are willing to be stale.
- Name the read and write paths separately; they rarely have the same bottleneck.
- Name the failure you are designing for, then the recovery path.
Follow-up
- What breaks first when traffic grows ten times?
- What would you drop to keep the system up under load?
Semi-system design scenarios involving pseudo-code implementation.
Medium · system design and architecture
Approach
- Fix the scope first: who calls this, how often, and what they do when it fails.
- Name the read and write paths separately; they rarely have the same bottleneck.
- Name the failure you are designing for, then the recovery path.
Follow-up
- What breaks first when traffic grows ten times?
- What would you drop to keep the system up under load?
Page a rights-change feed without skipping withdrawn windows
Easy · pagination · change-feed · at-least-once
A partner's catalogue mirror polls GET /availability-windows every minute to keep its copy current. The endpoint pages with limit and offset ordered by updated_at, rights operators edit rows constantly, and the mirror is demonstrably missing withdrawals. The table is availability_window(id, content_version_id, territory_code, licence_type, starts_at, ends_at NULL, status ENUM('draft','active','withdrawn'), max_resolution_cap NULL, created_at, updated_at, updated_by), indexed on (content_version_id, territory_code, starts_at, ends_at). Design the pagination and change-feed contract: the cursor, the ordering, how a withdrawal reaches the mirror, and what the caller does when a page fails.
Approach
- Name the failure mechanically. OFFSET is a position into a result set recomputed on every request. A row edited mid-scan gets a fresh updated_at, sorts behind the cursor, and shifts every later row one position forward — so the row that slid into the vacated offset is never returned. It also scans offset+k rows per page and degrades linearly with depth, where a keyset seek is O(log n + k).
- Replace it with a keyset cursor on (updated_at, id) and a strict row comparison,
WHERE (updated_at, id) > (:ts, :id) ORDER BY updated_at, id LIMIT :n. The id is not decoration: without a unique tiebreak, rows sharing a timestamp straddle a page boundary and are dropped or repeated forever. This needs a new index on (updated_at, id) — the existing one is built for the serve-time range predicate and cannot order this scan. - Close the visibility gap, which is the part most candidates miss. Do not serve up to now(): a transaction that began before your high-water mark can commit after you have read past it, carrying an updated_at that now sits behind the cursor, so that row is never seen again. Either hold the watermark back by more than the longest write transaction, or order by a commit-ordered sequence instead of a wall-clock column.
- Make withdrawal a row rather than an absence. status flips to 'withdrawn' and updated_at bumps, so it travels the feed like any other edit. Hard deletion would leave the mirror able to learn about a withdrawal only by diffing the entire catalogue, which is the diff you built the feed to avoid.
- State the delivery guarantee out loud. Keyset-by-updated_at is at-least-once — a row edited during a scan is re-delivered — so the contract must require the consumer to apply rows idempotently keyed on id and must forbid treating a repeat as a new event. A failed page is simply refetched from the last committed cursor, and the cursor advances only after the page is durably applied.
Worked solution (15 min)
- Build a ten-row table by hand, page it with limit=3 at offsets 0, 3 and 6, and edit row two's updated_at between pages. Write down the id that is never returned.
- Redo the same sequence with a keyset cursor and write down which id is returned twice instead.
- Write the cursor encoding (opaque, so the sort key can change later) and the exact WHERE clause with the strict tuple comparison.
- Write the consumer's apply step and confirm it is a per-id upsert that absorbs the repeat from step two.
Expected result: A worked example naming the id dropped by offset paging and the id repeated by keyset paging; a strict tuple comparison with a matching (updated_at, id) index; and a stated at-least-once contract whose consumer upserts by id and advances the cursor only after applying.
Verify it yourself
- The predicate compares the pair strictly, not
updated_at >= :tswith a separate id filter — that variant either loops on a timestamp cluster or skips past it. - A withdrawal is observable as a row with status='withdrawn', never as a missing row.
- You can state which transaction-visibility case your watermark rule covers, and how far behind now() it holds.
Follow-up
- The mirror is down for six hours and resumes from an old cursor. Does it converge, and what will it never learn?
- An operator edits 40,000 windows in one bulk job. What does the feed look like to a consumer polling every minute, and what do you add to the contract?
Payout close disagrees with the dashboard by a constant sliver
Medium · timezone · period close · reconciliation
Every monthly close, one territory's payout total disagrees with the operational dashboard by roughly 0.3%, always in the same direction, and the gap widens in March and November. qualified_play holds qualified_at, territory_code, source ENUM('online','offline_sync'), counted_in_period and a UNIQUE dedupe_key; the close job selects rows by a qualified_at range. Give an ordered checklist that separates a timezone bug from late offline arrivals, and state the fix and the backfill.
Approach
- Bucket the disputed rows by day of month and by hour. A boundary-offset bug puts all of them on the first or last day of the period inside one contiguous band of hours; late arrival scatters them across the whole month by qualified_at. Do this before reading any code.
- Check types rather than intent: is qualified_at an absolute instant or a naive local timestamp, and is the close boundary built from a calendar date in a reporting zone? A naive column compared against a zone-converted boundary shifts the window by that zone's offset, which is why only a few hours at each end move.
- Size it. A five-hour shift at a month boundary misattributes the plays in five hours out of roughly 730, about 0.7% before weighting; if the shifted hours are low-traffic overnight hours, 0.3% is what you see. The magnitude is evidence, not a coincidence.
- Explain the seasonal term, because it is the discriminator. A hardcoded fixed offset does not change size across the year. An offset that moves at a daylight-saving transition does, which points at a zone conversion rather than a constant.
- Separate the offline path explicitly: for source = 'offline_sync', qualified_at is when the threshold was crossed on a device, by an untrusted clock, and the row may arrive days later. Plot arrival time minus qualified_at. Late arrival produces rows landing after a close, which is a policy problem, not a boundary one, and it is additive to the timezone gap rather than an alternative explanation.
- Fix: store qualified_at as an absolute instant, construct close boundaries as instants from the reporting zone explicitly, and keep territory_code for reporting rather than deriving the boundary from it. Correct the history with paired reversing and replacement rows, never by updating counted_in_period in place, because money was already paid against the closed period and it must stay reconstructible.
Follow-up
- What close policy do you adopt for offline syncs arriving days late, and what does the rights holder see on their statement?
- Does a dedupe_key regenerated by a client after a crash still hold through this correction?
- How do you prove the already-closed period can still be reproduced after the backfill?
Suggested practice checklist
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 — Measure before reasoning
- Take a slow piece of your own code, write down in advance where you believe the time goes, then profile it and record how wrong the guess was. The cost is usually an allocation you did not notice or an accidental quadratic membership test.
- Replace one list membership test inside a loop with a set and measure at a thousand, ten thousand and a hundred thousand elements, confirming the shape of the curve rather than only that it got faster.
- Write down the three quantities you can now measure instead of assert: wall time, peak memory, and call count for the function you suspected.
Deliverable: A before-and-after profile of real code plus a written note on the size of the gap between the guess and the measurement.
Day 2 — References, copies, and the bugs they produce
- Write the function with a mutable default argument, call it three times, and explain the accumulating result: the default is evaluated once when the function is defined, so every call shares one object.
- Build a nested structure, take a shallow copy, mutate an inner element, and show that both views changed, because a shallow copy duplicates the container and not the elements. Then fix it with a deep copy and state the cost you just accepted.
- Write two functions, one mutating its argument in place and one rebinding the local name, and predict the caller's view of each before running it. That single distinction produces most of the bugs that pass their tests.
Deliverable: Three small programs whose output you predicted correctly before running, each with a one-line statement of the rule underneath.
Day 3 — Types, once, in a language that checks them
- Port one module you have already written, roughly a hundred lines, into a statically typed language, and record every place the compiler demanded an answer your original had left implicit: a value that can be absent, a numeric width, a case never handled.
- Write the same signature in both languages and state what the static one guarantees before the program runs and what it does not, since it will not save you from a wrong algorithm or an index out of range.
- Write the difference between an interface satisfied by declaration and one satisfied structurally, with one case each where the other approach would miss the mistake.
Deliverable: One module in two languages plus a list of the questions the type checker forced you to answer.
Day 4 — Concurrency, starting with what actually runs at the same time
- Run the same CPU-bound function across four threads and four processes and measure both. Under the default CPython build the threaded version will not speed up, because only one thread executes bytecode at a time; the process version will. Check which build you are on first, since free-threaded builds remove that lock and change the result.
- Then run a blocking I/O workload across four threads and measure it speeding up, because the interpreter releases that lock around blocking calls, which is why treating threads as useless is wrong as a general claim.
- Build the lost update: two threads each incrementing a shared counter a hundred thousand times, and show a final value below the expected sum, because an increment is a load, an add and a store and the thread can be suspended between them. Fix it with a lock and then measure what the lock costs.
Deliverable: Three measurements, threads against processes on CPU work, threads on I/O work, and a demonstrated lost update, each with the mechanism written underneath.
Day 5 — Debugging as a procedure rather than an instinct
- Work one real failure as a bisection: find a revision or an input size where it is good and one where it is bad, halve repeatedly, and state the two assumptions bisection needs, that the property changes exactly once across the range and that the test is reliable.
- Minimise one failing input to the smallest version that still fails, and record how many rounds it took.
- Keep a hypothesis log for one bug in three columns, what I believe, what would disprove it, what I observed, and stop yourself the first time you are about to change two things at once.
Deliverable: One bug worked to root cause with a written hypothesis log and a minimised reproducing input.
Day 6 — Tests that catch the bug you are about to write
- Implement an LRU cache with a capacity bound, then write the three test cases that would catch an off-by-one in eviction: insert exactly capacity items and assert nothing was evicted, insert one more and assert the least recently used key is the one gone, and read an old key just before that insert so the eviction victim changes.
- Add a property test comparing your implementation against a deliberately slow reference, an ordered list scanned linearly, over a few thousand random operation sequences, because a slow reference finds the cases you would not have thought to write.
- Write one numeric test that fails under exact equality and passes with a tolerance, and state why the tolerance has to be relative rather than absolute once the magnitudes grow.
Deliverable: An LRU implementation with three boundary tests, one property test against a slow reference, and one tolerance-based numeric test.
Day 7 — Debug something broken, out loud
- Have someone plant three defects in a two-hundred-line program, an off-by-one, a shared mutable state bug, and a wrong error-handling path, then find them while narrating, under a fixed rule: state the hypothesis before touching anything.
- Time each one and record which tool found it, reading, a printed value, a debugger, or a test, because the question asked in interviews is how you would find it rather than what it was.
- Write the sentence you will use when you do not yet know the cause, one that names the next measurement instead of offering a guess.
Deliverable: A recorded debugging session with time-to-find per defect and the method that found each.
Know where your numbers came from
Bring the two or three numbers the story rests on and know how they were collected. A p99 whose timer starts inside your handler excludes the time a request spent queued, so it can sit flat while users wait longer. Give the window, the percentile and what the measurement left out, or drop the number.
Tell me a little about yourself and your background.
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- What did you decide not to do, and why?
- How did you know your change caused the improvement?
What challenges did you face while working on your technical task, and how did you overcome them?
Medium · behavioural and engineering judgement
Approach
- Name the disagreement and how you resolved it with evidence.
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
Argue against pre-issuing licences to survive a live spike
Hard · entitlement · drm · load spike · design review
A scheduled live event concentrates an audience into a 60-second window, driving a 10x licence-service spike with no ramp. A proposal on your team: pre-issue licences at manifest fetch and cache the entitlement decision for the event's duration. Every dashboard improves. Describe a design you argued against where the cost was invisible in metrics. State the property the proposal removed, the alternative you offered with its own cost, how you made the risk concrete, and what happened when the decision went the other way.
Approach
- The probe is whether you can name the property being deleted instead of arguing taste. State it exactly: manifests and segments carry no user identity and are cached so they can be shared, which leaves the licence request as the only per-user, uncacheable point on the path. Moving authorisation to manifest fetch moves it onto a cacheable document, and a cacheable document is a shared one.
- Make the consequence a scenario with a number. A viewer whose payment fails mid-event keeps playing until the licence policy expiry, which is fixed at issue and cannot be shortened; with a three-hour event and a three-hour policy, the exposure is the whole event per account, with nothing left to revoke against.
- Split the two changes the proposal bundles, because they are not equally bad. Caching the entitlement lookup for 60 seconds keeps a per-request check and bounds the error to 60 seconds of staleness. Pre-issuing the licence removes the check and hands out a key with a long policy life. Attack the second and concede the first - conceding it is usually what gets the rest of the argument heard.
- Price the alternative rather than only objecting. This is the one load spike in the system set by a clock rather than by demand, so it is the one you can capacity-plan: pre-warm from the event schedule, shard by key ID, and keep the policy expiry short enough that key rotation re-checks entitlement during the event. The costs are idle capacity bought before the event and a higher steady-state request rate from rotation.
- Make it reviewable instead of a debate. Write the exposure as one sentence with a figure in it, in a decision record with an owner and a date, so the team is choosing the risk rather than drifting into it.
- Say what you did after losing. A strong answer instruments the thing it argued against - compare entitlement_snapshot on issued grants against live subscription state and count the issued-but-unentitled - so the next conversation has evidence rather than a rerun.
Follow-up
- The event ships with pre-issued licences. What do you measure during it so the post-mortem is not opinion again?
- Specify the 60-second entitlement cache: what is the key, what is the staleness bound, and which decision must never read it?
- How short can the licence policy expiry go before key rotation becomes the new spike?