cswork / libraryOpen practice workspace

Interview guides · Celstream Technologies

Celstream Technologies Software Engineer Interview Questions & Guide 2026

Software engineer prep for Celstream Technologies: design exercises weighted toward the constraints of streaming and media work.

The 60-second brief

A Software Engineer at Celstream Technologies serves as a core technical contributor responsible for designing, developing, and maintaining high-quality software solutions. You will work within a dynamic project-based environment, often collaborating with cross-functional teams to deliver robust applications that meet specific client requirements. Your work directly impacts the efficiency and functionality of the systems Celstream Technologies deploys for its partners.

Ask how many rounds there are and what each one is before you plan your weeks, because the composition is what your hours should follow. A loop with three coding rounds and one short design conversation deserves a different split from the reverse, and whoever schedules it will usually say plainly which it is.

Focus areas

  • Cut live latency without starving the buffer
  • Sessionise at-least-once heartbeats into exactly-once plays
  • Design a bitrate ladder under stated constraints

Overview

This role is both challenging and rewarding, demanding a strong grasp of fundamental computer science concepts and the ability to apply them to real-world engineering problems. Whether you are working on embedded systems, web services, or application development, you will be expected to demonstrate technical rigor and a proactive approach to problem-solving. Success in this position requires not only technical proficiency but also the ability to communicate complex ideas clearly and work effectively within a team-oriented culture.

The Celstream Technologies interview process

Celstream Technologies candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.

Assessment Phase

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.

Technical Evaluation

What this round decides is narrow: whether you can produce code that runs and is correct on inputs nobody showed you. An elegant solution that does not compile scores below a plain one that does, so write a correct brute force first, say out loud that you know its cost, and improve it with the working version still on screen. What separates strong answers is who finds the broken case. Trace your own code against an empty input, a single element, and duplicate keys before you say you are finished, because being told is far more expensive than noticing.

What to demonstrate

  • Whether degenerate inputs get checked without being asked for: an empty collection, one element, every element equal, and the extreme value the input type allows
  • Whether the complexity you state matches the code you actually wrote, including a sort or a copy sitting inside a loop
  • Whether the finished answer is verified against the worked examples before you call it done, rather than assumed correct because the code reads correctly

How to prepare

  • Take five problems you have already solved and, without running anything, write down what each returns for empty input, a single element, and all-duplicates. Then run them and count how many you predicted wrong.
  • Drill the brute force as its own skill: on ten problems, write only the obviously-correct slow version and time how long it takes to get it passing. If that is more than a few minutes, that is what to practise, not the optimal version.
  • Add a fixed last step before you submit anything, reading only the loop bounds and the initial value of each accumulator, which is where most off-by-one errors live

HR Interview

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.

Where candidates lose points

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.

Shortening segment duration to cut live latency.

In classic segmented delivery a compliant client starts roughly three target durations back from the live edge, so halving the segment duration does halve that component - and that is the whole of the appeal. It also doubles the request rate for both segments and playlists, and doubles the number of IDR frames, which cost far more bits than the predicted frames around them, so quality at a fixed bitrate drops. Low-latency extensions exist precisely to break this coupling: they deliver sub-second parts within a segment that stays long enough to encode efficiently.

Retrying a write that is not safe to repeat

A timeout tells you nothing about whether the server applied the write, so a blind retry of a create or a charge can duplicate it. Either make the operation idempotent, with a caller-supplied key the server deduplicates on or a conditional update, or do not retry it; and use exponential backoff with jitter so the retries of many clients do not synchronise into a second outage.

Saying 'eventually consistent' without naming the anomaly a user would see

Describe the concrete symptom you are choosing to accept: the author reloads and their own comment is missing for two seconds, or two devices show different balances for a minute. The class of consistency model is a technical label; the tolerable anomaly is the actual product decision.

Questions & practice

How do you approach solving logic puzzles?

Medium · data structures and algorithms

Approach

  1. State the target complexity and say which constraint rules the naive version out.
  2. Choose the data structure from the access pattern, not from familiarity.
  3. Walk one small example through your approach before writing the whole thing.

Follow-up

  • Which test case would catch an off-by-one here?
  • How does this change if the input no longer fits in memory?

Write a program to identify even numbers in an array.

Medium · data structures and algorithms

Approach

  1. State the target complexity and say which constraint rules the naive version out.
  2. Restate the input: its shape, its size, and what is guaranteed about it.
  3. Name the brute-force solution and its complexity before improving on it.

Follow-up

  • What is the worst case, and how likely is it on real data?
  • How does this change if the input no longer fits in memory?

Explain how you would implement a linked list or a tree structure.

Medium · data structures and algorithms

Approach

  1. Restate the input: its shape, its size, and what is guaranteed about it.
  2. Choose the data structure from the access pattern, not from familiarity.
  3. Name the brute-force solution and its complexity before improving on it.

Follow-up

  • Which test case would catch an off-by-one here?
  • How does this change if the input no longer fits in memory?

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

  1. Build forward adjacency (job -> dependents) in one O(E) pass over the edge list. No sorting is required anywhere in this problem.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Worked solution (25 min)

  1. Sketch a 12-node pipeline with three rungs so that manifest generation has three predecessors at different depths.
  2. Mark one rung's chunk encode dirty and hand-compute the forward-reachable set.
  3. Show a BFS order on that sketch that violates a dependency, then run Kahn on the induced subgraph and show it does not.
  4. Recompute in-degrees restricted to S and state exactly which edges are excluded.
  5. Give the complexity and the saving versus a full re-run in terms of the ladder fan-out.

Expected result: The affected set equals the dirty set union its forward-reachable closure, emitted in a Kahn order computed over the induced subgraph, in O(V + E); a cycle is reported when emitted count is less than |S|.

Verify it yourself

  • Manifest generation and publish appear in every non-empty affected set.
  • Every node in the emitted order has all of its in-S predecessors emitted earlier.
  • On acyclic input the emitted count equals |S| exactly.

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?

Add a dedupe constraint to a live plays table

Hard · online migration · lock queueing · backfill · unique constraints

qualified_play holds about two billion rows, takes continuous inserts, and was created without the dedupe_key column the design calls for. Add the column, backfill it deterministically from session_id and content_version_id, and make it unique and NOT NULL without taking the table offline. Duplicate plays already exist and money was paid against them, and corrections are written as reversing rows that share the same logical key. Give the ordered plan, name the lock each step takes, and say what the uniqueness is actually on.

Approach

  1. Deploy the write path before the backfill. Ship the code that populates dedupe_key on new rows first so the backfill covers a finite set — everything older than that deploy — instead of chasing a table that keeps growing new null rows behind it.
  2. Add the column as nullable with no default. A nullable column with no default has never rewritten the table; since PostgreSQL 11 a constant default does not either, but a volatile default still does. The statement takes ACCESS EXCLUSIVE briefly, and the real hazard is the queue: the ALTER waits behind a long analytics scan of this table, and every query arriving after it waits behind the ALTER. Take it with SET lock_timeout = '2s' and retry, so a failure to get the lock is a no-op rather than a stall of the read path.
  3. Backfill in bounded batches over the primary key range, committing per batch. Each UPDATE writes a new row version, so the backfill produces roughly a table's worth of dead tuples before autovacuum reclaims them; throttle to the rate autovacuum can keep up with and watch the dead-tuple count rather than wall-clock progress. Never hold one long transaction over the whole backfill — it pins the transaction horizon and blocks cleanup everywhere.
  4. Decide what the uniqueness is on before building anything. A reversing row carries the same logical key as the row it reverses, so a plain unique on dedupe_key would make corrections impossible. Either make the index partial — unique on dedupe_key WHERE status IN ('pending','counted') — or fold a revision into the stored key. Say which, and say what a re-count after a reversal then looks like under your choice.
  5. Resolve the pre-existing duplicates with a window function rather than a DELETE: ROW_NUMBER() OVER (PARTITION BY dedupe_key ORDER BY qualified_at, id) identifies every row past the first, and each gets a reversing row. Deleting them would erase rows a payout already ran against; the ledger has to show the correction, not lose the evidence.
  6. Build with CREATE UNIQUE INDEX CONCURRENTLY, which takes SHARE UPDATE EXCLUSIVE and does not block reads or writes, at the cost of two table passes and waiting on open transactions. It fails if any duplicate remains, and a failed build leaves an index with indisvalid = false that must be dropped before retrying. Promote it with ALTER TABLE ... ADD CONSTRAINT ... UNIQUE USING INDEX, a brief ACCESS EXCLUSIVE that does not rebuild. For NOT NULL, add CHECK (dedupe_key IS NOT NULL) NOT VALID, run VALIDATE CONSTRAINT under SHARE UPDATE EXCLUSIVE, then SET NOT NULL, which from PostgreSQL 12 uses the validated check instead of scanning the table again.

Follow-up

  • The backfill is half complete when a bug in the key derivation is found. What is the recovery, and does it need another full pass?
  • A duplicate surfaces after the constraint is live and the period is already closed. Which table changes and which does not?
  • How do you prove the key is deterministic enough that a client regenerating it after a crash produces the identical string?

Two child tables that quietly inflate a watch-time report sixfold

Medium · join fan-out · pre-aggregation · distinct aggregates

playback_session(id UUID PK, account_id, content_version_id, played_ms BIGINT, started_at, cdn_pop). license_grant(id, session_id, key_id, issued_at, status) has several rows per session because live channels rotate keys. qualified_play(id, session_id, content_version_id, status ENUM('pending','counted','reversed'), qualified_at) records a correction as a second row rather than an update. A daily report joins all three and reports, per content version: sessions, total played_ms, licences issued and counted plays. Total watch time comes out roughly six times too high. Explain the mechanism, write the corrected query, and say why COUNT(DISTINCT ...) is not a complete fix.

Approach

  1. Count the rows the join actually produces: one per (session x grant x play). A live session with six key rotations whose play was reversed and re-counted contributes 6 x 2 = 12 rows, so SUM(ps.played_ms) adds that session's watch time twelve times. The multiplier varies per session, which is why the result is 'roughly six times' rather than exactly anything — there is no constant to divide out.
  2. Fix the counts with COUNT(DISTINCT lg.id) and COUNT(DISTINCT qp.id) FILTER (WHERE qp.status = 'counted'), and price it: distinct aggregates sort or hash per group, so the planner gives up the cheap hash aggregate it would otherwise use.
  3. State why the sum cannot be rescued the same way. SUM(DISTINCT ps.played_ms) deduplicates by value, so two different sessions that each played exactly 1,800,000 ms collapse into one contribution. Deduplicating a measure by its value is wrong in general; the only correct move is to reduce each session to one row before summing.
  4. Write the corrected shape: aggregate each child to session grain independently, then join. WITH g AS (SELECT session_id, count(*) AS grants FROM license_grant GROUP BY 1), p AS (SELECT session_id, count(*) FILTER (WHERE status = 'counted') AS counted FROM qualified_play GROUP BY 1) SELECT ps.content_version_id, count(*) AS sessions, sum(ps.played_ms) AS played_ms, coalesce(sum(g.grants), 0) AS grants, coalesce(sum(p.counted), 0) AS counted FROM playback_session ps LEFT JOIN g ON g.session_id = ps.id LEFT JOIN p ON p.session_id = ps.id GROUP BY 1. Every join is now one-to-at-most-one, so no row is duplicated.
  5. Compare the cost: two hash aggregates over the child tables plus two hash joins, each linear in rows scanned, against a plan that first materialises the product of the two children and then aggregates the larger intermediate. Keep the joins LEFT with COALESCE so a session that started but never reached a licence stays in the session count — an INNER join drops it silently and changes what the report means.
  6. Say which column carries the highest correctness bar: played_ms feeds reporting, the counted play feeds payout, and a duplicate there is money moved from one rights holder's share to another's. The bar differs by column inside a single report.

Worked solution (25 min)

  1. Build a fixture on one content version: session A with 6 licence grants and 2 qualified_play rows (one counted, one reversed), session B with 1 grant and 1 counted play, session C with no grants and no plays.
  2. Run the naive three-way LEFT JOIN and record the output row count and SUM(played_ms).
  3. Isolate session A in the join output and confirm it occupies grants x plays rows.
  4. Rewrite with per-child CTEs at session grain and re-run.
  5. Switch the LEFT JOINs to INNER and note which session disappears.

Expected result: The naive join emits 12 rows for A, 1 for B and 1 for C, and its watch time equals 12xA + B + C. The corrected query emits one row for the content version with watch time A + B + C, grants 7, counted plays 2, and session C present with grants 0. Under INNER joins session C vanishes and the session count drops to 2.

Verify it yourself

  • The corrected SUM(played_ms) equals the same sum taken from playback_session alone with no join at all.
  • Result rows equal the number of distinct content versions, and each child count is independent of the other child's row count.
  • Deleting the reversed qualified_play row changes the counted total by zero.
  • Adding a seventh licence grant to session A changes grants by one and played_ms by nothing.

Follow-up

  • The same report is wanted broken down by CDN POP and device class. Does the pre-aggregation survive, or does each CTE need the extra key?
  • A play was reversed and later re-counted. Should the report show one counted play or zero, and stamped at which timestamp?
  • How would you catch this class of bug in CI, given the naive query returns a plausible number rather than an error?

Explain the four fundamental OOP concepts in detail.

Medium

Approach

  1. State your assumptions explicitly before working the problem.
  2. Say what you would check first and why it is the highest-information step.
  3. Clarify what is being asked and what a complete answer contains.

Follow-up

  • How would you know your answer was wrong?
  • What assumption would you test first?

What are the differences between asynchronous and synchronous timers?

Medium

Approach

  1. Work from the requirement backwards to the design.
  2. State your assumptions explicitly before working the problem.
  3. Clarify what is being asked and what a complete answer contains.

Follow-up

  • How would you know your answer was wrong?
  • What assumption would you test first?

Absorb a clock-driven licence spike without deleting enforcement

Hard · availability-vs-consistency · admission-control · cache-ttl-as-policy · enforcement-boundary

A scheduled live event starts at 20:00. DRM licence requests baseline at 5,000 per second, and the start concentrates the audience into a 60-second window at ten times that with no ramp. Entitlement must be read live from the subscription store, which fails over at 19:58 and serves no reads for 90 seconds. Give the capacity plan, exactly what may be cached and what may not, the behaviour during the outage for accounts you hold evidence about and accounts you do not, and the order in which you shed load.

Approach

  1. Plan capacity from the calendar, not from a metric. A ten-times step with no ramp gives reactive autoscaling nothing to react to until the queue has already built, and instance start plus TLS and process warmup is tens of seconds against a spike that lasts sixty. Pre-scale on the event schedule, hold capacity across the window, and size against the total: 50,000 requests per second for 60 seconds is 3,000,000 licence issuances in the minute, which is the number the load test has to reproduce.
  2. Draw the enforcement line precisely, because this is the whole problem. The content key and its usage policy are minted per client per key ID and cannot be cached or pre-issued without deleting the only per-user checkpoint in the system, at which point every dashboard improves. The entitlement read, meaning whether this account is in good standing and whether its plan covers this event, is a separate lookup and may be cached for a bounded TTL. That TTL is the enforcement lag in seconds, and you state it as a number rather than leaving it implicit.
  3. Convert the outage from a dilemma into a capacity problem by pre-warming. At a live start most of the audience is making its first licence request of the evening, so treating a cache miss as unknown would make fail-closed mean a dark event. Bulk-load entitlement snapshots for eligible subscribers in the eligible territories hours ahead; at a few hundred bytes per account, five million accounts is about a gigabyte, so the live-start case becomes a cache hit and the subscription store is off the critical path before it ever fails.
  4. Split the outage behaviour by whether you have evidence. For an account with a cached snapshot, serve on it and extend the TTL to a hard ceiling for the declared outage, recording entitlement_snapshot on the grant so a later dispute is adjudicable. For an account with no snapshot at all, fail closed, because a false allow with no evidence is an unbounded grant to an unknown party, while a false allow on a stale snapshot is bounded by the ceiling you chose. Name the asymmetry that justifies the split rather than applying one rule to both.
  5. Shed by class instead of uniformly. Licence renewals for sessions already playing come first, because a failed rotation is a black screen for a viewer who is already watching; new starts come next, where a refusal is a spinner and a retry; everything else yields. Return a retryable status with a Retry-After that spreads clients rather than a bare error that invites a synchronised retry storm on top of the spike.
  6. Stagger key rotation so you do not build a second spike. If every live session rotates on the same schedule, the rotation wave arrives synchronised and re-creates the peak minutes later; jitter each session's rotation deadline across the rotation period so rotation load is flat, and verify the flatness in the load test rather than assuming it.

Worked solution (45 min)

  1. Compute peak requests per second and total issuances in the window, then state the pre-scale trigger and the hold period.
  2. Draw the request path and mark exactly one point as cacheable, writing the TTL beside it and what that TTL means in plain language.
  3. Size the pre-warm set and its memory, and specify when it runs and how its success is verified.
  4. Write the outage decision table for snapshot-present and snapshot-absent accounts, including the stale ceiling.
  5. Order the shed classes and pick the status code and Retry-After distribution for each.
  6. Specify the rotation jitter and show, arithmetically, that rotation load stays flat.

Expected result: Scheduled pre-scaling sized to 3,000,000 issuances in 60 seconds, licences never cached while entitlement reads are cached under a stated TTL that is explicitly the enforcement lag, a pre-warmed entitlement set that removes the subscription store from the critical path, fail-open on a stale snapshot up to a hard ceiling and fail-closed on no snapshot with the asymmetry argued, renewals shed last, and jittered key rotation.

Verify it yourself

  • No cached object anywhere in the design contains a content key.
  • An account cancelled at 19:00 stops being able to start a stream by 19:00 plus the stated TTL, and the design says so before the event rather than after.
  • With the subscription store returning errors for 90 seconds, pre-warmed accounts start playback and unknown accounts receive a refusal rather than a key.
  • A load test at 50,000 requests per second for 60 seconds shows no queue growth after the window and no synchronised second peak at the first rotation.

Follow-up

  • Legal asks how long a cancelled account can still start a stream. Give them a number and show where it comes from in your design.
  • The pre-warm job fails silently at 18:00 and nobody notices until 20:00. What does the system do, and how would you have known?
  • Halve the entitlement TTL. What changes in load, and what changes in the guarantee you can state?

Concurrency limit over-admits only during live-start bursts

Hard · race condition · leases · atomicity · idempotency

Support reports accounts playing four concurrent streams on a two-stream plan, a few hundred times a day, clustered in the minutes after a scheduled live start. It never reproduces locally or in load tests below 500 starts per second. Acquire reads the account's active-lease count from the lease store and, if it is under the limit, adds a member with a TTL; heartbeats renew by removing and re-adding the member; clients retry a start that times out. Give the ordered checklist, the acquire primitive you would write, and the test that proves it.

Approach

  1. Rank hypotheses against the timing evidence before instrumenting anything. Read-then-write is a time-of-check-to-time-of-use race and explains clustering at bursts. A count read from a replica explains cases correlated with one region. A retry that mints a fresh session id explains excess leases that share a device but differ in session id. All three can be live at once, which is why one fix often fails to close it.
  2. Instrument the decision inputs rather than the outcome: observed count, limit, whether the member was added, which lease-store node served the read, and the client attempt number. One burst of these logs settles which mechanisms are firing and in what proportion.
  3. Close the check-then-act window first, because it is the one that scales with load. Make acquire a single server-side atomic operation over a sorted set keyed by account, with score equal to the lease expiry instant: in one script, ZREMRANGEBYSCORE up to now, then ZCARD, then ZADD only if the count is below the limit, returning acquired or refused. Cost is O(log N + M) for M expired members, and it cannot interleave because the script runs to completion on a single node.
  4. Close the renew hole second. Remove-then-add is two steps with a window in which the member is absent, so a concurrent acquire sees a free slot; under a burst that window is hit constantly. Renew must be one ZADD XX that updates the score of an existing member only, and must fail rather than re-create a member that has already expired.
  5. Close the retry hole third. The session id is client-generated, so a retry must reuse it and acquire must be idempotent on (account_id, session_id): re-acquiring an id already held refreshes the score and returns success without consuming a second slot. Without this, every timeout at a live start manufactures a second lease.
  6. Prove it with a test that fires limit + K concurrent acquires against one real lease-store instance and asserts exactly limit successes, repeated enough times that interleavings are likely, plus a test that renewing after expiry fails instead of resurrecting the member. A test built on a process-local mutex proves nothing, because the race is across service replicas.

Follow-up

  • What lease TTL do you pick against a 10-second heartbeat, and what are the false-eviction and stranded-slot numbers that follow?
  • How does the slot come back when a client crashes without sending a stop?
  • The lease store fails over mid-burst: do you fail open or fail closed, and who pays for that choice?

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.

What the behavioural round is actually measuring

Every story you tell gets read for blast radius and judgement: what could have broken, who else it touched, what you knew at the moment you decided. Nobody can audit your code in an hour, so they audit your reasoning instead. Pick work where the call was genuinely yours and the consequences were real enough to remember.

How do you handle working within a team-based project structure?

Medium · behavioural and engineering judgement

Approach

  1. Pick a story where you made the decision, not one where you watched it.
  2. Name the disagreement and how you resolved it with evidence.
  3. Close with what you would do differently, concretely.

Follow-up

  • What did you decide not to do, and why?
  • How did you know your change caused the improvement?

What are your professional weaknesses?

Medium · behavioural and engineering judgement

Approach

  1. Pick a story where you made the decision, not one where you watched it.
  2. Name the disagreement and how you resolved it with evidence.
  3. 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?

Discuss your experience with C and C++ programming.

Medium · behavioural and engineering judgement

Approach

  1. Pick a story where you made the decision, not one where you watched it.
  2. State the situation in two sentences and spend the rest on the reasoning.
  3. Give the blast radius: what could have broken, and what you measured.

Follow-up

  • What did you decide not to do, and why?
  • What would you do differently if you ran that again?