Interview guides · Immediate Media
Immediate Media Software Engineer Interview Questions & Guide 2026
A prep plan for Immediate Media software engineer candidates: answer first, read the solution second, repeat the misses later.
The 60-second brief
A Software Engineer at Immediate Media plays a pivotal role in maintaining and evolving the digital platforms that support the company's diverse portfolio of high-profile brands. You are not just writing code; you are building the infrastructure that delivers engaging content to millions of users. This role involves balancing high-performance technical requirements with the creative needs of editorial teams, ensuring that the technology stack remains robust, scalable, and efficient.
The title spans product, platform and infrastructure work, and which of those the seat actually is decides whether design or algorithms carries more weight in your preparation. The posting rarely settles it; what the team is on call for usually does.
Focus areas
- Cut live latency without starving the buffer
- Key CDN caches without sharding on tokens
- Enforce entitlement at the licence boundary
Overview
You will contribute to complex problem spaces, such as managing Digital Asset Management (DAM) systems and optimizing resource distribution across web platforms. Because Immediate Media operates at the intersection of media and technology, engineers here are expected to be both technically proficient in core languages like C# and thoughtful about system architecture. It is an environment that values collaborative problem-solving and a human-centric approach to software delivery.
The Immediate Media interview process
Immediate Media candidates report 2 rounds · ≈ 2-4 weeks. The stages below are what candidates describe, not a published process.
Technical Skills Assessment
The same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.
What to demonstrate
- In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
- In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
- Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes
How to prepare
- Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
- Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
- For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first
Face-to-Face Discussions
Because the format is not fixed, the first job in the room is classification. Listen to the opening question and decide what it is: a probe into work you have already described, a fresh problem to solve now, or a conversation about how you operate. Each wants a different register, and the common failure is forcing a rehearsed structure onto a question that did not ask for it. Running a full design ritual on a ten-minute debugging question reads as not listening. When you cannot tell which it is, ask how long they want to spend and answer at that depth.
What to demonstrate
- Whether the shape of your answer matches the question, so a yes-or-no gets answered before it is justified and an open prompt gets a direction before a detour
- Whether you check how much depth is wanted instead of deciding for them, and whether you stop when the answer is complete rather than continuing until someone interrupts
- Whether you can be redirected in the middle of an answer without restarting it from the beginning
- Whether a question outside your experience gets an honest boundary followed by reasoning from what you do know, instead of a confident answer with nothing behind it
How to prepare
- Rehearse one project at three lengths, roughly thirty seconds, three minutes, and a full walkthrough at the depth of a design review, and practise switching between them when someone interrupts mid-telling
- Have someone ask you five questions of deliberately mixed type in one sitting without telling you the types, and score only whether you identified each one correctly before you started answering
- Draft the sentence you will use to check depth, along the lines of asking whether the short version is useful here or they want the detail, and use it in a real conversation this week so the day of the round is not its first outing
Where candidates lose points
Purging a withdrawn title by enumerating its URLs.
A content version is 6-12 video renditions plus audio and subtitles, each split into thousands of segments, so a per-URL purge of one title is tens of thousands of requests that must reach every POP and will partially fail. Cache tags reduce it to one purge, but only if the tag was attached at publish time - a packaging decision made long before anyone asks for a withdrawal. And nothing reaches an offline copy at all, so the persistent licence's policy expiry has to be set conservatively when it is issued, because it cannot be shortened later.
Including the delivery token, session id or any per-user parameter in the CDN cache key.
It looks like the conservative choice, and it behaves perfectly in staging where three people are watching. In production every viewer becomes a distinct cache object, hit ratio collapses toward zero, and the origin absorbs the entire read load at peak - a cost and availability event rather than a slow page. The correct shape is for the edge to verify the token's signature and then strip it from the key, which means the token has to be verifiable at the edge without a callback to any origin service.
Choosing a schema before the access patterns are known
Write the queries first, with their filters, sort orders, cardinalities and which ones sit on the latency-critical path, then design tables and indexes to serve them. An index nothing queries still costs write throughput and storage, and a hot query with no supporting index becomes a full scan that only hurts once the table is big.
A cache with no invalidation story
Say how an entry goes stale, how long you can serve it stale, and what happens when many requests miss the same key at the same instant. One popular key expiring under load sends every concurrent request to the origin together; single-flight coalescing, jittered expiry, or serving stale while revalidating are the standard answers.
Questions & practice
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?
Parse a live media playlist for seek offsets and deltas
Medium · parsing · prefix sums · binary search
You are given two successive snapshots of an HLS media playlist as text: #EXT-X-TARGETDURATION, #EXT-X-MEDIA-SEQUENCE, then repeated #EXTINF:<seconds>, lines each followed by a segment URI, with #EXT-X-DISCONTINUITY markers possible between entries. A four-hour DVR window at two-second segments is 7,200 entries. Build a structure that answers which segment contains playback position P in better than linear time, and compute which segments were added and removed between the two snapshots. State the complexity of both the build and each query.
Approach
- Single-pass parse into an array of (media_sequence_number, duration_ms, uri, discontinuity_flag), converting each #EXTINF decimal-seconds value into integer milliseconds at parse time. Accumulating 7,200 floating-point seconds drifts, and every comparison downstream wants exact integers.
- Build prefix sums of duration_ms alongside the array, O(n) time and O(n) space, and answer a position query by binary search over the prefix array: O(log n), about 13 probes at 7,200 entries.
- Reset the accumulator at each #EXT-X-DISCONTINUITY and keep a per-discontinuity base. A discontinuity is a timeline break, so a prefix sum that runs straight through one maps every position after it to the wrong segment. The query then returns (discontinuity index, offset within it) rather than a single scalar.
- Diff by media sequence number, not by URI or by position. The first entry's number is #EXT-X-MEDIA-SEQUENCE and each subsequent entry is one greater, so added = numbers in B above A's maximum and removed = numbers in A below B's first. That is O(added + removed) and survives URI reuse across a discontinuity.
- Tie it back to delivery: every viewer refetches this document about once per target duration, so the delta is the thing you would actually transmit, and it is the reason a 7,200-entry playlist is a bandwidth problem rather than a parsing problem.
Follow-up
- Add #EXT-X-BYTERANGE so several segments share one resource. What changes in the structure, and what does a seek now have to return?
- The playlist is served as a delta against a client-held version. What must the server retain, for how long, and what happens to a client that has fallen further behind than that?
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?
Put segment duration where alignment cannot drift
Medium · normalisation · check constraints · composite foreign key
content_version(id, title_id, duration_ms, encoder_build_id, packaging_status) and rendition(id, content_version_id, stream_kind ENUM('video','audio','subtitle'), width, height, frame_rate_milli_fps, segment_duration_ms, gop_size_frames, status). Every video rendition of a content version must have identical segment boundaries, and each segment must begin at an IDR frame, so gop_size_frames has to divide the segment length in frames. Both facts currently live per rendition and nothing stops a re-encoded rung from disagreeing. Restructure so the database rejects drift, and say what breaks at a frame rate of 24000/1001 with a 2,000 ms segment.
Approach
- Identify the functional dependency. For video rows,
segment_duration_msis determined bycontent_version_id, not byrendition_id. A dependency stored at the wrong grain is exactly what lets two rows disagree, and the invariant is presently a sentence in a comment rather than anything the database can refuse. - Move the authoritative value to the parent as
content_version.segment_frames. Ask whether the child copy earns its keep: the manifest build already loads the content_version row to resolve the URL, so the join costs nothing and the denormalised copy buys no read. If it is kept anyway for write-path convenience, guard it with a composite foreign key —UNIQUE (id, segment_duration_ms)on the parent andFOREIGN KEY (content_version_id, segment_duration_ms) REFERENCES content_version(id, segment_duration_ms)on the child. Under the default MATCH SIMPLE semantics a NULL in either child column satisfies the constraint, which is the escape hatch that lets audio and subtitle rows carry no video segment length. - Fix the representation before writing any CHECK. 24000/1001 is 23.976023976… and is not representable in integer thousandths, so
frame_rate_milli_fps = 23976is already a different number from the real rate and every derived value drifts from it. Store the exact rational asframe_rate_num INTEGER, frame_rate_den INTEGER— 24000 and 1001 — and store the segment length in frames rather than milliseconds. - With frames and an exact rational, both invariants become integer CHECKs that cannot round:
CHECK (segment_frames % gop_size_frames = 0)for IDR alignment, and the millisecond duration becomes derived —segment_frames * 1000 * den / num— rather than a second stored copy to keep in sync. - Work the stated case: at 24000/1001, 2,000 ms is 48000/1001 frames, about 47.952, so no segment of that target length contains a whole number of frames and no integer GOP size divides it. 96 frames is exactly 4.004 s and admits any GOP size dividing 96. The encoder has to round a millisecond target to whole frames, and that rounding is where two rungs encoded at different times end up on different boundaries.
- Accept what the schema still cannot catch. Rate control can relocate an IDR without changing any stored column, so add a post-packaging QC step that compares segment counts and first-sample timestamps across the video renditions of a version and fails the publish on a mismatch, and pin
encoder_build_idper rendition so a rung re-encoded on a newer build is a visible difference rather than an inference.
Worked solution (30 min)
- Work the frame arithmetic for 25, 30000/1001 and 24000/1001 at 2,000 ms and at 4,004 ms, marking which combinations yield a whole number of frames.
- Draft both schemas — milliseconds on the child with a composite FK, frames on the parent — and write the CHECK constraints each one admits.
- Insert two video renditions of one content version with different segment lengths and confirm the second is rejected.
- Insert an audio rendition with a null video segment length and confirm it is accepted.
- Write the post-packaging QC query that compares segment counts across the video renditions of one version and returns the offending rendition ids.
Expected result: At 24000/1001 a 2,000 ms target is about 47.952 frames and no integer GOP divides it; 4,004 ms is exactly 96 frames and admits any GOP size dividing 96. Both schemas reject a second video rendition with a different segment length, and both accept the audio row — the composite-FK version only because its FK column is null.
Verify it yourself
- No two video renditions of one content version can be inserted with different segment lengths, in either insertion order.
- An audio rendition inserts cleanly with no video segment length.
segment_frames % gop_size_frames = 0holds for every video row, and the millisecond duration is recomputable rather than stored twice.- The QC query flags a rendition whose segment count differs from its siblings even when every stored column matches.
Follow-up
- One rung fails QC and is re-encoded alone. What do you verify before republishing the manifest, and what is the rollback if boundaries moved?
- Audio renditions legitimately use a different segment length from video. Which of your constraints still applies and which has to be weakened?
- How do you migrate the existing millisecond column to frames without a window in which neither constraint holds?
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.
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?
Explain the advantages of utilizing specific Design Patterns in your code.
Medium
Approach
- Work from the requirement backwards to the design.
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Asked to draft an architectural design for repurposing resources in a DAM (Digital Asset Management) system.
Medium
Approach
- State your assumptions explicitly before working the problem.
- Work from the requirement backwards to the design.
- 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?
Rate limit licence issuance through a scheduled live start
Medium · rate-limiting · thundering-herd · backpressure
The DRM licence service handles one request per playback start plus one per key rotation. Baseline tracks session starts at 10^3 to 10^4 QPS, but a scheduled live event concentrates the entire audience into a 60-second window and drives a 10x spike with no ramp. Callers are player SDKs on devices you cannot patch, each with its own retry loop. Licences cannot be cached or pre-issued, because this endpoint is the only place entitlement is enforced. Design the rate-limiting contract: the dimension you limit on, the refusal, the client's required behaviour, and what you provision instead.
Approach
- Separate the two jobs that get conflated into one limiter. Per-caller quota is a fairness and abuse control, keyed on account or device, a token bucket with a small burst allowance. Global admission control is a capacity control that sheds any caller once in-flight concurrency exceeds what the service can serve inside its latency budget. A per-account limit tight enough to protect the service refuses legitimate households during the spike; a global shed with no per-account limit lets one abusive key consume the headroom you provisioned for everyone.
- Do not key the limiter on session_id or key_id. Live channels rotate keys, so a long legitimate session makes many licence requests over hours, and limiting on the rotating dimension punishes exactly the sessions the event consists of.
- Refuse with 429 and an explicit Retry-After, chosen per response and drawn over a window rather than fixed. A constant Retry-After against a synchronised audience reassembles the identical spike one interval later, which is the failure the limiter was installed to prevent. Spreading arrivals is the whole job, so the server hands each refused caller a different deadline instead of hoping clients disagree by accident.
- Pin the client's obligations in the SDK specification, because the devices cannot be patched later: honour Retry-After when present, otherwise exponential backoff with full jitter, a bounded attempt budget, and a user-visible failure at the end rather than an indefinite loop. Add a per-attempt deadline — a client holding a request open for thirty seconds occupies a server slot it will never use.
- Name what you provision rather than shed. The spike is set by a clock, so it is forecastable: scale ahead of the scheduled start. If you must reduce requests, lengthen that channel's key-rotation interval, which trades a known amount of protection for a known amount of load. Never reach for caching or pre-issuing a licence: that deletes the only enforcement point in the system while every latency and error dashboard improves.
Worked solution (25 min)
- Compute the spike: take a baseline QPS, apply the 10x multiplier over a 60-second start window, then add the steady rotation rate for the duration of the event.
- Write the two limiters as separate rules with separate keys, and state the intended victim of each in one line.
- Write the 429 response with its headers, then compute the arrival distribution for 500,000 refused clients under Retry-After of exactly 1 second versus a value drawn uniformly over 10 seconds.
- Write the client retry pseudocode with full jitter and a bounded budget, then state the residual peak you must still provision for.
Expected result: Two independent limiters with the dimension and target of each; a 429 carrying a server-chosen jittered Retry-After; a client retry specification with bounded attempts, full jitter and a per-attempt deadline; and an explicit statement that the residual spike is provisioned rather than shed because entitlement cannot move off this path.
Verify it yourself
- No proposed mitigation caches a licence, pre-issues one, or extends a licence's life beyond what the entitlement check at issue time justifies.
- You can state the peak arrival rate your Retry-After distribution produces as a number — 500,000 at one instant versus 50,000 per second across ten — not as an assertion that it 'spreads load'.
- No limiter key includes session_id or key_id, so a three-hour live session that rotates keys is never refused for being long.
Follow-up
- The limiter is itself a shared counter. What happens to it at 10x, and what accuracy do you give up to keep it off the hot path?
- An entire ISP sits behind a handful of egress addresses. Which of your dimensions breaks, and what do you key on instead?
One partition stops dead while the other sixty-three drain
Medium · poison message · dead letter queue · at-least-once
Consumer lag on one partition of the heartbeat topic climbs steadily while the other 63 partitions are healthy: throughput on it is zero, its committed offset has not moved in 40 minutes, and the consumer process restarts every few seconds. Events are (session_id, sequence, played_ms) delivered at-least-once and deduped on (session_id, sequence). Give the ordered checklist, what you do in the next ten minutes, and the permanent fix.
Approach
- Separate stalled from slow using three metrics before opening a log: committed offset (stationary versus advancing), per-partition throughput (zero versus reduced) and the consumer restart counter. Stationary offset plus zero throughput plus rising restarts is a poison record; a slow consumer advances its offset and shows raised per-record processing time.
- Confirm with the crash fingerprint. The same exception at the same offset on every restart is definitive. Note why lag is linear rather than bursty: at-least-once delivery plus unbounded in-process retry is an infinite loop by construction.
- Fetch the record at that offset with a separate consumer group so the live group is untouched, and classify it: a deserialisation failure, a value outside a column domain (played_ms beyond the content duration, a sequence beyond the column's integer width), or a code path only that key reaches.
- Mitigate within ten minutes: bounded retries, then route the record to a dead-letter topic carrying key, partition, offset and exception, and commit past it. The right to skip comes from the dedupe key: replaying the DLQ later is safe precisely because (session_id, sequence) makes reprocessing idempotent.
- Do not hand-commit a later offset to unstick it. That discards the whole batch, which contains hundreds of healthy sessions, and the loss is invisible afterwards.
- Permanent fix: validate at the consumer boundary with a schema plus range checks and treat an invalid record as an expected input class with a defined output, not as an exception. A poison record is data; if the handler has no branch for it, the queue has no way forward.
Follow-up
- What bounds the damage if the poison is a whole batch from a bad producer release rather than one record?
- What alert would have paged before lag was visible, and why is consumer lag alone a weak signal here?
- How does the diagnosis change if the record is valid but the downstream write is the thing failing?
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.
'We rewrote it in a better framework' is not a story
A migration is a cost you chose to pay, not an achievement. The story is what the old system made expensive, what you measured before committing, what kept serving traffic during the cutover, and what you would have done if the numbers had come back flat. Without those, a rewrite reads as taste.
Describe a challenging technical problem you solved and how you communicated the solution to non-technical stakeholders.
Medium · behavioural and engineering judgement
Approach
- Give the blast radius: what could have broken, and what you measured.
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
Tell us about a time you had to learn a new technology or framework under a deadline.
Medium · behavioural and engineering judgement
Approach
- Pick a story where you made the decision, not one where you watched it.
- 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 would you do differently if you ran that again?
- How did you know your change caused the improvement?
Discuss your experience with databases and how you optimize queries for performance.
Medium · behavioural and engineering judgement
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Give the blast radius: what could have broken, and what you measured.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
What motivates you to work in a media-focused engineering environment?
Medium · behavioural and engineering judgement
Approach
- Give the blast radius: what could have broken, and what you measured.
- State the situation in two sentences and spend the rest on the reasoning.
- 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?
- What did you decide not to do, and why?