Interview guides · Advice Media
Advice Media Software Engineer Interview Questions & Guide 2026
System design practice for Advice Media software engineer candidates: reach a data model and an API contract, not a box diagram.
The 60-second brief
A Software Engineer at Advice Media plays a pivotal role in bridging the gap between sophisticated digital marketing technology and the practical needs of medical practices. This position is not merely about writing code; it is about crafting high-performance digital experiences that help clients grow their businesses and connect with patients. You will be responsible for building, maintaining, and optimizing the platforms that power Advice Media's client-facing solutions.
Find out what you will be typing into. A shared plain-text editor with no autocomplete, compiler or test runner changes what you have to hold in your head, and practising inside your own configured environment hides exactly that gap.
Focus areas
- Cut live latency without starving the buffer
- Expire rights across cached and offline copies
- Sessionise at-least-once heartbeats into exactly-once plays
Overview
The work is characterized by a blend of technical depth and creative problem-solving. Whether you are focusing on front-end aesthetics to improve user engagement or back-end architecture to ensure system reliability, your contributions directly influence the success of Advice Media's clients. You will often work in collaborative, cross-functional environments where your ability to communicate technical concepts to non-technical stakeholders is just as important as your ability to ship clean, efficient code.
Advice Media values practitioners who can demonstrate both technical proficiency and a genuine interest in the business outcomes of their work. Be prepared to discuss how your technical decisions positively impact end-users.
The Advice Media interview process
Advice Media candidates report 5 rounds · ≈ 4-6 weeks. The stages below are what candidates describe, not a published process.
Recruiter Conversation
The person on this call usually cannot evaluate your code and does not need to. They write a short paragraph, and that paragraph is what a hiring manager skims when deciding who to put on your loop. So the test is not whether your work was hard, it is whether a non-engineer can repeat it correctly. Name systems by what they did rather than by their internal codename, give each project a shape (what was breaking, what you changed, what happened after), and keep the whole walkthrough near ninety seconds. Depth that cannot survive a paraphrase reads as vagueness.
What to demonstrate
- Whether a non-engineer can restate your projects without distorting them, since their paraphrase is what travels to the hiring manager, not your sentences
- Whether each project has a shape rather than a stack list: the failure or constraint, the change you made, the result and how it was measured
- Whether you can say what was yours inside a team project without either inflating it or disappearing into the plural
How to prepare
- Rewrite each headline project as two sentences with no internal system names and no acronyms outside your company, then say them to someone outside engineering and have them repeat them back. Fix whatever came back wrong
- Attach one measured number to each project: the baseline, the change, and the window it was measured over. Where nothing was ever measured, say that plainly rather than reaching for a plausible percentage
- Time the background walkthrough against a clock. If it runs past two minutes, compress the earliest role to a single clause and spend the recovered time on the most recent one
Technical Evaluation
Most of the time lost in this format is not lost to thinking. It goes to a standard-library call you half-remember, an off-by-one in a loop bound, and a debugging loop that mutates code at random until something passes. When output is wrong, stop re-reading the whole function: take the smallest input that reproduces it and walk the state through by hand, printing intermediates if the environment allows. Guessing at a fix without a failing case you understand is how a five-minute bug becomes twenty, and the clock does not pause while you do it.
What to demonstrate
- Whether you reach the right structure without a detour, and can write it from memory rather than only recall that one exists
- Whether overflow is considered where the language has fixed-width integers, since a signed 32-bit value stops at 2,147,483,647 and then wraps in Java, is undefined behaviour in C++, and does not arise in Python, whose integers grow instead
- Whether recursion depth is treated as a constraint on large inputs, given that CPython's default limit is 1000 frames and a deep recursion can exhaust the stack in any language where an iterative version would not
- Whether a failing case is isolated and explained before any edit is made to the code
How to prepare
- From an empty file and with no references open, implement the pieces you lean on most: a heap push and pop, an iterative DFS with an explicit stack, and a binary search whose midpoint is written lo + (hi - lo) / 2, which avoids the overflow that (lo + hi) / 2 can hit in a fixed-width integer type
- Time yourself on the ten library calls you look up most, such as sorting with a custom comparator, splitting and joining strings, and finding the next key at or above a value in an ordered map, until the lookup is gone
- Take a solution you know is broken and, before touching it, write one sentence naming the input, the expected value and the actual value. Repeat until you do it without deciding to.
Practical Exercise
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.
Department Director Discussion
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
Final Decision-Making
A day like this is several different games in a row, and the expensive mistake is carrying the previous one into the next room. Coding rewards narrow precision and finishing inside a timer. Design rewards breadth, stated assumptions and naming what you are deliberately not building. Behavioural rewards specificity about people and decisions. Candidates who over-engineer a coding problem they were supposed to finish, or who start sketching class hierarchies before anyone has agreed what the system has to do, are usually still playing the last round. Between rooms, name out loud which game the next one is.
What to demonstrate
- Whether the coding round ends with something that runs and has been traced against a degenerate input, rather than an extensible design that was never finished
- Whether a design discussion opens by agreeing on traffic shape, read-to-write ratio and what is allowed to be stale, instead of proceeding from an architecture you arrived with
- Whether a behavioural answer names a person, a disagreement and what you did about it, rather than describing the system the story happened inside
- Whether the opening habits still appear late in the day: restating the problem, asking for constraints, saying the plan before typing
How to prepare
- Book three mocks of different types back to back on one afternoon and ask each interviewer afterwards which round you answered in the wrong mode
- Write a three-line opening script per round type — coding: restate, name the approach and its cost, then type; design: ask for scale, read-write mix and what must not break; behavioural: name the person, the stakes and the decision — and run it off a card so the switch is mechanical rather than remembered
- Practise coding with a timer you do not extend, stopping when it stops, so the trained reflex is to finish a correct solution rather than to keep improving one
Where candidates lose points
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.
Assuming one encrypted copy of the ladder serves every DRM system.
Common encryption (ISO/IEC 23001-7) defines several protection schemes, and two matter in practice: AES-CTR full-sample ('cenc') and AES-CBC pattern encryption ('cbcs'). One of the three widely deployed DRM systems accepts only cbcs, and older client versions of the other two accept only cenc, so a single-copy strategy is correct only if every device class you still support has reached cbcs. Discovering late that one legacy class has not means a second packaging run, a second set of objects in every cache, and roughly double the storage for the affected catalogue.
Answering a debugging question with a guess instead of a bisection
Give a procedure that halves the search space at each step: confirm the symptom reproduces, establish the last known-good version, input or timestamp, then bisect over commits, over the data, or over the layers of the request path. A plausible cause with no way to confirm it is the same move whether it happens to be right or wrong, which is why it scores nothing.
Tests that assert on the implementation rather than the behaviour
Assert on what a caller can observe, not on the number of internal calls or the shape of a private field. A test that breaks on every refactor but still passes when the answer is wrong costs more than it protects.
Questions & practice
Rank delivery groups by rebuffer ratio without ranking on noise
Medium · top-k · heap · aggregation
A completed hour of telemetry yields roughly 10^6 groups keyed (cdn_pop, device_class, isp), each arriving as an unordered stream of per-minute partials carrying qualified_starts and rebuffered_starts. On-call needs the 50 worst groups by rebuffer ratio, excluding groups too small to mean anything: a group with 3 starts and 1 rebuffer reads as 33%. Return the top 50 with their ratios and sample counts. Target something better than a full sort, and state both time and space.
Approach
- Two phases, forced by the fact that the ranking key is a ratio that is only final once every partial for a group has landed. Phase one is a hash aggregation of partials into per-group counters, O(n) time and O(g) space for g groups.
- Phase two selects with a size-50 min-heap keyed on the ratio: O(g log k) time and O(k) space, against O(g log g) for a sort. At g = 10^6 and k = 50 that is about 6 comparisons per group rather than 20. Quickselect is O(g) but needs every group resident and mutable, so the heap wins when phase one streams.
- Apply the minimum-sample floor before the heap, never after. At a floor of 200 qualified starts the standard error of the ratio is sqrt(p(1-p)/n), about 1.5 percentage points at p = 5%, so the ordering carries information. Filtering after the heap leaves it full of three-sample groups and returns fewer than 50 usable rows.
- Offer the alternative that removes the arbitrary cutoff: rank on the lower bound of a Wilson score interval, which shrinks small groups toward the population mean automatically. The cost is a heavier comparison per group and a figure that is no longer the raw ratio people expect on the dashboard.
- If g does not fit in memory, partition by hash of the group key, take a local top-50 per shard and merge. Top-k decomposes this way only because each group's aggregate is complete within its shard, which the hash partition guarantees.
Follow-up
- Make it a rolling one-hour top-50 recomputed every minute. What changes when groups must also be evicted as old minutes leave the window?
- Two groups tie on ratio but differ 100x in volume. Which do you page on, and what does that argue about ranking on count-weighted impact instead of on ratio?
Alert on a rolling rebuffer ratio over a time-based window
Medium · sliding window · two pointers · alerting
A session's QoE samples arrive in playback order as (sequence, wall_clock_ms, wall_delta_ms, rebuffer_ms). Find the earliest sample at which the rebuffer ratio over the trailing 120,000 ms of wall time exceeds 5%, ignoring any window covering under 30,000 ms of wall time. Sample spacing varies between 2 and 30 seconds depending on device and player version, and a long session can carry 10^5 samples. Return the sample index or none. Target O(n) time and O(1) additional space.
Approach
- Two indices over the time-ordered array with running integer sums of wall_delta_ms and rebuffer_ms. Advance the right index one sample at a time, then pull the left index forward while the window's wall span exceeds 120,000 ms, subtracting as you go. Each index only moves forward, so total work is O(n) with two accumulators and no auxiliary structure.
- Define the window by accumulated wall time, not by sample count. Spacing varies by more than 10x, so a fixed count of samples is a window somewhere between four minutes and an hour depending on which device reported it.
- Gate the predicate on a minimum covered span of 30,000 ms. Without it, the first two samples of any session containing one stall produce a window at or near 100% and every session with a startup stall pages someone.
- Name the naive alternative and its cost: recomputing the sums across the window at each step is O(n * w), roughly 6 x 10^6 operations at 10^5 samples and a 60-sample window. Acceptable for one session on demand, not acceptable when the same evaluation runs against 10^6 concurrent sessions.
- Keep both accumulators as int64 milliseconds so add-then-subtract is exact; a float accumulator drifts over 10^5 add/subtract pairs and the drift is largest on exactly the longest sessions.
Worked solution (20 min)
- Write the two-pointer loop with both accumulators and the eviction condition on wall span.
- Add the minimum-coverage gate and show which sessions it suppresses.
- Argue the O(n) bound from the fact that each index advances monotonically.
- Trace a session with one 5-second stall followed by 10 minutes of clean playback and confirm the alert clears once the stall leaves the window.
Expected result: The index of the first sample whose trailing 120,000 ms window covers at least 30,000 ms and exceeds a 5% rebuffer ratio, found in a single forward pass with two int64 accumulators and O(1) extra space.
Verify it yourself
- Doubling every wall_delta_ms without changing rebuffer_ms halves the reported ratio.
- A session with one early stall and ten clean minutes does not alert after the stall ages out.
- On a 200-sample input the answer matches a brute-force recomputation of every window.
Follow-up
- Evaluate it incrementally as samples stream in with no array retained. What state do you keep per session, and what is its memory bound at 10^6 concurrent sessions?
- A late sample arrives out of order after the window has already moved past its timestamp. Do you re-evaluate, and what does that do to the idempotency of the alert?
Recompute the downstream job set after a single rendition re-encode
Medium · dag · topological sort · invalidation
The packaging pipeline for one content version is a DAG of up to 10^5 jobs and 5 x 10^5 edges: source validation, per-chunk encodes, per-rendition concatenation, packaging, encryption, manifest generation, publish. QC fails one video rung, so its chunk encodes are re-run on a different encoder build. Given the edge list and a set of dirty job ids, return every job that must re-run plus a valid execution order for them, and report whether the graph contains a cycle. Target O(V + E) time.
Approach
- Build forward adjacency (job -> dependents) in one O(E) pass over the edge list. No sorting is required anywhere in this problem.
- Collect the affected set S by BFS or DFS from the dirty set over forward edges, marking a visited bitset. O(V + E), each node and edge touched once.
- Order S with Kahn's algorithm run on the induced subgraph, recomputing in-degrees counting only edges whose tail is also in S. Reusing whole-graph in-degrees is the failure here: clean predecessors outside S leave phantom in-degrees, the ready queue starts empty and nothing is ever emitted.
- Detect the cycle from Kahn's own termination condition: if emitted count < |S|, the residual nodes contain a cycle and everything it feeds. Report the residual set rather than asserting, because a malformed encoder profile is a data bug, not a crash.
- State the trade-off against re-running the whole version: the affected set is one rung's chunks plus concatenation, packaging, encryption, manifest and publish, so you save the 6-12x ladder fan-out. But manifest and publish are common descendants of every rung and are therefore always in S, which is precisely why re-encoding one rung is not a local change.
Follow-up
- The re-encode uses a different encoder build. Which additional nodes become dirty, and why is encoder_build_id pinned per asset rather than taken from the pipeline default?
- Two rungs fail QC concurrently and two invalidation runs overlap. What makes the recomputation idempotent so the second run does not restart jobs the first already completed?
Stop two devices winning the last concurrent stream slot
Hard · write skew · isolation · leases · lock contention
playback_session(id UUID PK, account_id, status ENUM('starting','active','ended'), last_heartbeat_at, started_at) and account_plan(account_id PK, max_concurrent_streams INT). Session start currently runs SELECT count(*) FROM playback_session WHERE account_id = :a AND status = 'active', compares the result with the plan limit, then inserts. A household on a two-stream plan regularly ends up with three. Name the anomaly, say which PostgreSQL isolation levels do and do not prevent it, then give a schema and a statement that hold the limit, plus the lease TTL and heartbeat interval you would set and why.
Approach
- Name it precisely: write skew over a phantom. Both transactions evaluate the same predicate, both see one active session, and each inserts a row the other's read would have matched. Neither writes a row the other wrote, so there is no write-write conflict for the database to block on and no constraint to violate.
- Give the isolation answer without hand-waving. READ COMMITTED does not prevent it. REPEATABLE READ in PostgreSQL is snapshot isolation and does not prevent it either — snapshot isolation stops lost updates on the same row, not write skew across a predicate. SERIALIZABLE does, via SSI predicate locks: one transaction aborts with SQLSTATE 40001 and the caller retries. Price that: a retry loop sits directly in front of the first frame, and SSI's conflict tracking runs on a path taking 10^3 to 10^4 starts per second.
- Prefer materialising the conflict to raising the isolation level. Pre-create the plan's slots as rows:
account_stream_slot(account_id, slot_no, session_id NULL, expires_at NULL, PRIMARY KEY (account_id, slot_no)). A phantom problem becomes a row-lock problem, which every isolation level already handles correctly and cheaply. - Acquire in exactly one statement:
UPDATE account_stream_slot SET session_id = :sid, expires_at = now() + interval '90 seconds' WHERE (account_id, slot_no) = (SELECT account_id, slot_no FROM account_stream_slot WHERE account_id = :a AND (session_id IS NULL OR expires_at <= now()) ORDER BY slot_no LIMIT 1 FOR UPDATE SKIP LOCKED) RETURNING slot_no. Zero rows updated means the account is at its limit — a product outcome to return to the viewer, not an exception. SKIP LOCKED makes two simultaneous starts take different slots instead of one queueing behind the other. - Reclaim by predicate rather than by a sweeper.
expires_at <= now()inside the acquire means a crashed client's slot is reusable the instant it expires, with no background job anywhere in the correctness path. Renew withUPDATE ... WHERE account_id = :a AND slot_no = :n AND session_id = :sid: thesession_idequality is what stops a late heartbeat, from a client whose slot already expired and was reassigned, from stealing it back. - Defend the numbers in both directions. At a 10-second heartbeat and a 90-second TTL a client must miss eight consecutive heartbeats before eviction, and a crashed client strands a paid slot for up to 90 seconds. Do not compute the eviction rate as a per-heartbeat loss probability raised to the eighth power — losses are correlated, a mobile handover drops a burst — so size the TTL against the measured distribution of short outages, not against an independence assumption that does not hold.
Worked solution (35 min)
- Reproduce: open two sessions, run the count in both, then insert in both, and confirm a two-stream account ends with three active rows.
- Repeat at REPEATABLE READ and confirm both still commit; repeat at SERIALIZABLE and record which transaction receives 40001.
- Create the slot table with two rows for the account and implement the single-statement acquire.
- Run two acquires concurrently and record the returned slot numbers and affected row counts, then run a third.
- Force one slot's
expires_atinto the past, acquire from a fourth client, and then run the original holder's renew.
Expected result: READ COMMITTED and REPEATABLE READ both permit three active sessions. SERIALIZABLE commits one and aborts the other with SQLSTATE 40001. With slot rows the two concurrent acquires return slots 1 and 2, the third returns zero rows, the fourth acquires the expired slot, and the stale renew updates zero rows.
Verify it yourself
- A test that starts N+1 sessions concurrently never leaves more than N slots held — run repeatedly, since a single pass can pass by luck.
- The acquire is one statement, with no SELECT preceding it anywhere in the code path.
- A renew from a client whose slot was reassigned updates zero rows and is surfaced to that client as a lost lease rather than ignored.
- Nothing in the correctness path depends on a sweeper job having run recently.
Follow-up
- The lease store is Redis and the session table is PostgreSQL. What happens when the Redis acquire succeeds and the session insert then fails?
- A viewer moves from phone to television mid-title. Should the phone's slot be released explicitly, and what failure modes does that add?
- Two starts arrive for the same device and the same title four milliseconds apart. Should they consume one slot or two, and what key decides?
Make a monthly payout scan use an index at two billion rows
Medium · sargability · covering index · partitioning · query planning
qualified_play(id BIGSERIAL PK, session_id, content_version_id, rights_holder_id, account_id, territory_code, qualified_at TIMESTAMPTZ, status ENUM('pending','counted','reversed'), source ENUM('online','offline_sync'), dedupe_key UNIQUE, counted_in_period) holds about two billion rows, roughly 150 million per month, and is physically ordered by insert time. The monthly close runs SELECT rights_holder_id, count(*) FROM qualified_play WHERE date_trunc('month', qualified_at) = DATE '2026-08-01' AND status = 'counted' GROUP BY 1 and scans the entire table. Give three separate reasons it does, and the change you would make for each.
Approach
- Reason one is the expression. Wrapping the column in
date_truncmakes the predicate non-sargable, so no b-tree onqualified_atis usable and the planner also falls back to a default selectivity guess instead of the column's histogram. Rewrite to the half-open rangequalified_at >= DATE '2026-08-01' AND qualified_at < DATE '2026-09-01', which selects the same rows. An expression index is the alternative, but in PostgreSQLdate_trunc(text, timestamptz)is STABLE rather than IMMUTABLE because it depends on the session TimeZone, so it cannot be indexed directly — you would indexdate_trunc('month', qualified_at AT TIME ZONE 'UTC')and write the predicate in exactly that form. - Reason two is selectivity, and the fixed predicate does not remove it. One month is about 7.5% of the table; at roughly 50 rows per page the table is about 40 million pages, and 150 million rows scattered across it touch essentially every page. A plain index scan would pay random I/O for the whole table while a sequential scan reads it in order — the planner is right. Make the scan index-only instead:
(status, qualified_at, rights_holder_id)covers every column the query touches, so the plan reads index pages alone. That holds only while the visibility map is current, which makes it a dependency on autovacuum keeping up with continuous inserts. - Reason three is structural.
status = 'counted'is most of the table and is not selective by itself, but a partial indexWHERE status = 'counted'keeps reversed and pending rows out of the index entirely. At two billion rows the real fix is range partitioning byqualified_atmonth: the close reads one partition, the planner prunes the rest, and the partition boundary lines up withcounted_in_periodso a period close and a partition are the same object. - Name the BRIN trap before someone reaches for it. BRIN is the obvious cheap index for an append-ordered table and it is tight on the arrival timestamp — but
qualified_atis when the threshold was crossed, andoffline_syncrows land days later, so physical order correlates with arrival rather than withqualified_at. Checkpg_stats.correlationfor the column instead of assuming; poor correlation makes every block range a candidate and the index worthless. - Settle it with EXPLAIN (ANALYZE, BUFFERS) rather than argument, and look at estimated versus actual rows for the range predicate first. A plan that goes wrong at this size usually went wrong at the estimate, and a wrong estimate has a different fix from a wrong index.
- Sequence the work by cost and risk: rewrite the predicate first because it is free and takes no lock, measure, then build the covering index with CREATE INDEX CONCURRENTLY, and only then consider partitioning, which is a migration of the whole table rather than an addition to it.
Follow-up
- The close also needs plays that qualified in August but arrived in September. Which of your changes still works, and which partition does a late row live in?
- The same aggregate is now wanted per territory as well. Do you widen the covering index or accept the scan?
- How would you prove the scan really is index-only in production rather than on a freshly vacuumed copy?
How do you approach the design and development of a website’s home page?
Medium
Approach
- State your assumptions explicitly before working the problem.
- Clarify what is being asked and what a complete answer contains.
- Say what you would check first and why it is the highest-information step.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
How do you ensure your code is scalable and maintainable?
Medium
Approach
- Clarify what is being asked and what a complete answer contains.
- State your assumptions explicitly before working the problem.
- Work from the requirement backwards to the design.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Publish a withdrawal webhook receivers can verify and replay safely
Medium · webhooks · signature-verification · ordering
When an availability_window flips to 'withdrawn', downstream consumers must react: the edge orchestrator issues a tag purge, a partner mirror hides the title, a search indexer drops the page. You are adding an outbound webhook for this. Receivers are third parties on stacks you do not control; delivery is at-least-once, retried with backoff, and can arrive out of order. Design the contract: the payload, how a receiver verifies the request came from you, how it orders two edits to the same window, what it returns for success, and what your sender does with a receiver failing for six hours.
Approach
- Decide thin event versus fat event and say why. A thin event (window id, content_version_id, territory_code, new status, version) forces a read-back but is never stale on arrival and leaks nothing to a receiver whose signing key later turns out to be compromised; a fat event saves the round trip but must be versioned and signed as carefully as any API response and can describe a state that has already changed again. The defensible middle is thin plus enough denormalised fields to route without a fetch.
- Sign the exact bytes you transmit, never a re-serialisation. HMAC-SHA256 over timestamp + '.' + raw_body, with the timestamp in its own header, verified before JSON parsing and compared in constant time. The timestamp must be inside the signed string or the signature is replayable forever; the receiver rejects anything older than a stated tolerance, and that tolerance is also the receiver's clock-skew budget, so it belongs in the documentation. Send two signatures during a key rotation so neither side has to deploy in the same minute as the other.
- Make ordering explicit rather than assumed. Retries and parallel senders mean a withdrawal can arrive before the edit that preceded it, so every event carries a monotonic per-row version and the receiver applies only when the incoming version exceeds the one it holds. A wall-clock updated_at is not a version: two edits inside one tick, or a writer whose clock stepped backwards, are both silently mis-ordered by it.
- Define success narrowly. Any 2xx means accepted for processing and requires the receiver to have made it durable first; everything else is retried with exponential backoff and jitter to a bounded budget, then parked on a dead-letter queue with an operator alert. Receivers must deduplicate on event_id, because a duplicate after a lost response is normal traffic under at-least-once rather than an incident.
- Keep the invariant off this channel. A withdrawal must take effect even when every delivery fails, so the manifest TTL stays shorter than the window granularity and the persistent-licence expiry was already set conservatively at issue time. The webhook makes the purge prompt; it is not what makes it correct, and designing as though it were puts rights compliance behind a third party's uptime.
Worked solution (20 min)
- Write the envelope in full: event_id, type, occurred_at, version, and a data object carrying window id, content_version_id, territory_code and new status.
- Write the signed string and the header set, then write the receiver's verify routine in order — read raw bytes, check timestamp tolerance, compute HMAC, constant-time compare, then parse.
- Write the apply step with the version comparison and the event_id idempotency check, and trace two events delivered in reverse order through it.
- Write the sender's retry schedule with a stated budget and the dead-letter behaviour at the end of it.
Expected result: A signed envelope carrying a monotonic version; a receiver routine that verifies raw bytes before parsing and applies last-writer-wins by version rather than by arrival; an at-least-once contract with deduplication on event_id; and a bounded sender retry ending in a dead-letter queue plus an alert.
Verify it yourself
- Replaying a captured request an hour later fails verification, and you can point at the field in the signed string that causes the failure.
- Delivering two edits out of order leaves the receiver in the same final state as delivering them in order.
- Removing the webhook entirely still leaves the withdrawal correct eventually, and you can name the two mechanisms that carry it.
Follow-up
- A receiver is down for six hours and returns. Do you replay, and what would let it catch up without replay?
- Your sender retries a delivery whose response was lost, and the receiver had already processed it. Which side pays, and what in the payload makes that cheap?
Session service leaks two gigabytes a day, restarts nightly
Medium · memory leak · retention · ttl eviction
The playback session service holds about 400,000 concurrent sessions per instance and is restarted nightly because RSS grows roughly 2 GB a day; instances OOM within three days if left alone. Old-generation pauses lengthen through the day while throughput is unaffected until the end. Sessions are created at start, renewed by 10-second heartbeats, and removed on an explicit stop; playback_session.ended_at is null for every session whose client simply stopped reporting. Give the ordered checklist and name the leak.
Approach
- Split the growth before choosing a tool: plot RSS minus heap used. A rising heap live set is a reachability leak; RSS rising against a flat heap sends you to off-heap buffers, native TLS state, thread stacks or a pool that never shrinks.
- Read GC logs next. In steady state the live set after each old collection should be flat. A monotonically rising post-collection floor is the leak signature; lengthening pauses with a flat floor is fragmentation or sizing, which is a different fix.
- Take two heap dumps hours apart and diff the dominator tree rather than the class histogram. The histogram names what grew; the dominator names what is holding it, which is the only actionable answer.
- Expect a map keyed by session id holding per-session state, plus a scheduled renewal task per entry. Cross-check its size against the count of sessions with status 'active' whose last_heartbeat_at is older than the lease TTL. A client that stops reporting never sends a stop, so a removal path conditioned on an explicit end never runs for it, and the entry count tracks exactly that population.
- Fix by evicting on the same TTL that governs the lease rather than on an explicit end: time-based expiry plus a sweeper, with the scheduled task cancelled on eviction, since an orphaned timer keeps its captured state alive by itself and will survive a fix that only clears the map.
- Restate the invariant so it does not regress: in-process session state must not outlive the lease it mirrors. The lease store already expires these entries; the in-process copy diverged from it.
Follow-up
- How do you bound the map so that exhaustion becomes a rejected session start instead of an OOM?
- Eviction now also removes sessions whose client is briefly offline - what does the viewer experience, and how does that interact with the lease TTL?
- Which metric would have caught this at 5% of heap rather than at OOM?
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.
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 handle feedback or constructive criticism on your technical work?
Medium · behavioural and engineering judgement
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Close with what you would do differently, concretely.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
Where do you see yourself in your career in a few years?
Medium · behavioural and engineering judgement
Approach
- Name the disagreement and how you resolved it with evidence.
- Give the blast radius: what could have broken, and what you measured.
- Close with what you would do differently, concretely.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
What motivates you to work in a collaborative, team-based environment?
Medium · behavioural and engineering judgement
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- 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.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
Can you describe your experience with specific software programs and development environments?
Medium · behavioural and engineering judgement
Approach
- Name the disagreement and how you resolved it with evidence.
- State the situation in two sentences and spend the rest on the reasoning.
- Close with what you would do differently, concretely.
Follow-up
- How did you know your change caused the improvement?
- What did you decide not to do, and why?