cswork / libraryOpen practice workspace

Interview guides · Aquatic Capital Management

Aquatic Capital Management Software Engineer Interview Questions & Guide 2026

Software engineer prep for Aquatic Capital Management: design exercises weighted toward the constraints of streaming and media work.

The 60-second brief

As a Software Engineer at Aquatic Capital Management, you are at the intersection of high-performance computing and quantitative finance. The role is critical to the firm’s ability to process massive datasets, execute complex trading strategies, and maintain the low-latency infrastructure that defines the firm's competitive edge in the market. You will not just be writing code; you will be architecting systems that must remain robust, scalable, and efficient under extreme pressure.

Scope in the operational half of the job when the seat carries on-call. Rollout, rollback and what you would put on a dashboard belong inside a design answer rather than after it, and an answer that never reaches them reads as someone who has built systems but not run them.

Focus areas

  • Cut live latency without starving the buffer
  • Design a bitrate ladder under stated constraints
  • Expire rights across cached and offline copies

Overview

This position demands a high level of technical rigor and an ability to solve problems that are often ambiguous or computationally intensive. You will work closely with researchers, traders, and other engineers to translate mathematical models into production-grade systems. Because Aquatic Capital Management operates in a fast-paced environment where milliseconds translate to material outcomes, your work directly influences the firm’s bottom line and operational stability.

The Aquatic Capital Management interview process

Aquatic Capital Management candidates report 5 rounds · ≈ 4-6 weeks. The stages below are what candidates describe, not a published process.

Screening Stage

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

Take-Home Assignment

Nobody is in the room when this is read, so the submission stands in for what an ordinary pull request from you would cost to review. That makes navigability the first thing being judged. A reader should reach the code that does the actual work within a minute of cloning, without tracing three layers of indirection to get there. What most separates a strong submission from a weak one is separation itself: the logic that implements the prompt kept apart from file reading, argument parsing and printing, so each piece can be read, and tested, on its own.

What to demonstrate

  • Whether the entry point makes the shape of the program obvious: what is read, what is computed, what is written, in that order
  • Whether the core logic can be called without touching the filesystem, a socket or stdin, since that is what makes it testable at all
  • Whether names hold up on a second reading; a function called parse that also writes, or a total that means something different from the README, costs the reader more than a longer name would have
  • Whether the commit history reads as a sequence of steps someone could follow, rather than one commit containing everything

How to prepare

  • Clone your own finished submission into an empty directory and read it in the order a stranger would, README then entry point then downward, noting every place you rely on something the code never told you, and fix those places
  • Take an exercise you have already solved and restructure it so the module holding the rules imports no file, CLI or network library, leaving input parsing and output as a thin shell around it
  • Build the history while you work, committing at points where the tests pass, so the log shows the order you actually solved the problem in

Technical Discussions

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.

Follow-Up Sessions

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 Rounds

Coding rounds mostly set a floor. They decide whether you clear the bar, not where you land on the ladder. Level tends to come out of the design discussion and the ownership stories, so the question worth auditing beforehand is whether the scope you describe matches the scope of the job. Work that stops at your own service, or a story whose hard part was writing the code rather than getting several people to agree on an interface, reads a level below where you think you are interviewing, and that gap is usually resolved downwards.

What to demonstrate

  • Whether the largest thing you describe owning ran end to end — the decision, the migration path, the rollout, and what you did when it went wrong — or stopped at the change you merged
  • Whether design answers include what you would not build, what you would defer, and what you would measure before committing, rather than only what the boxes are
  • Whether a disagreement in a story was settled with something checkable — a benchmark, a prototype, a written proposal — instead of by seniority or by waiting it out
  • Whether you can say which calls you made alone and which you escalated, and why the line sat where it did

How to prepare

  • Write your largest piece of owned work as a timeline of decisions — who decided what, when, and what you did when the plan broke — then delete every sentence whose subject is "we" and see how much survives
  • Take one system you know well and drill the migration answer: how old and new paths run side by side under live traffic, how you compare their outputs, what the rollback is once writes are going to both, and which step you would not automate
  • Map each line of the ladder in the job posting to a specific thing you have done, find the line you cannot support, and prepare the closest evidence you have plus an honest account of the gap

Where candidates lose points

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.

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.

Trusting input because it came from your own front end

Anything crossing a trust boundary is hostile: parameterise queries instead of building SQL by concatenation, validate against an allow-list rather than a deny-list, and bound the size of anything you allocate from a request. Raising this unprompted in an API or design question is a cheap and unusually strong signal.

Starting work without saying what you are about to spend time on

State the plan before executing it: the approach, roughly how long it will take, and what you intend to leave hand-waved. That gives the interviewer a chance to redirect you in ten seconds rather than watching you spend fifteen minutes on the wrong sub-problem.

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

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?

Deduplicate at-least-once heartbeats into per-session watch time

Easy · idempotency · hash aggregation · streaming

You receive a batch of up to 10^7 heartbeat records, each (session_id UUID, sequence INT, played_ms BIGINT, position_ms BIGINT, wall_clock). Delivery is at-least-once and out of order: a client that crashed and resumed replays earlier sequences, and an offline device can deliver days of buffered records in one burst. played_ms is a monotonically non-decreasing counter reported by the client; position_ms is the playhead. The batch spans roughly 10^6 sessions. For each session emit accumulated watch time and the count of distinct sequences received. Target O(n) time, and state your memory bound and what you do when it is exceeded.

Approach

  1. Split the output by idempotency before writing any code: max(played_ms) is idempotent under duplicate delivery, so watch time needs no deduplication at all. The distinct-sequence count is not idempotent, and it is the only figure that forces you to remember what you have already seen.
  2. One hash map keyed by session_id holding {max_played_ms, seen-sequence structure}, filled in a single pass. O(n) expected time, O(g) space for g distinct sessions.
  3. Bound the memory with real numbers rather than an asymptote: sequences are dense per session and start at zero, so a compressed bitmap per session costs order 100-200 bytes, and 10^6 sessions is roughly 100-200 MB. A bounded-reorder window would be cheaper but is wrong here, because a bulk offline sync arrives arbitrarily late.
  4. If the map does not fit, partition on hash(session_id) mod P and process shards independently. Every aggregate is per-session, so the partition is exact and no reduce pass is needed to merge across shards.
  5. Emit max_played_ms as watch time and the bitmap cardinality as the distinct count; a gap between cardinality and max_seq + 1 is a loss signal worth emitting alongside, not a reason to reject the session.

Worked solution (20 min)

  1. Classify each required output as idempotent or not under duplicate delivery, and write down which ones therefore need stored state.
  2. Write the single-pass loop: map lookup, max update, bitmap set, no sorting anywhere.
  3. Compute the memory figure out loud: 10^6 sessions times the per-session structure, and compare it against the box you expect to run on.
  4. Describe the hash-partition fallback and argue why it needs no cross-shard merge.
  5. Construct a seek-heavy session by hand (forward 40 minutes, backward 10, forward 5) and show that the maximum is unchanged while a delta-based sum is not.

Expected result: A per-session map of {max played_ms, exact distinct-sequence count} produced in one O(n) pass with O(1) amortised work per record, a stated memory figure of order 100-200 MB at 10^6 sessions, and a hash-partition fallback that requires no second pass.

Verify it yourself

  • Feeding the same batch twice produces byte-identical output.
  • A synthetic session that seeks backwards ten times reports the same watch time as one that plays the same content straight through.
  • Distinct-sequence count never exceeds max_seq + 1 for any session.

Follow-up

  • A session's records arrive split across two batches a day apart. What has to persist for the per-session maximum to stay correct, and what does that cost per open session?
  • played_ms decreases for 0.1% of sessions. Do you clamp, drop the record, or quarantine the session, and what does each choice do to payout counting downstream?

Audit segment boundary alignment across the catalogue without pairwise comparison

Hard · hashing · binary search · batch verification

Every video rendition of a content version must share identical segment boundaries. Per content version you have 6-12 renditions, each with a sorted int64 list of segment start times in milliseconds, up to 3,600 entries for a two-hour title. The audit runs over 10^4 content versions a day. The obvious check compares every rendition against every other element by element, and it is correct. Give a method fast enough for the daily run, localise the first divergence when a version fails, and state the complexity of both.

Approach

  1. Price the naive version first, because it is correct and the argument has to be about cost: C(12,2) = 66 pairs times 3,600 elements is about 2.4 x 10^5 comparisons per version, so 2.4 x 10^9 a day. Embarrassingly parallel, and still CPU you do not need to spend.
  2. Replace pairwise equality with a canonical fingerprint: one streaming 64-bit hash per rendition over the boundary sequence, O(R * S) per version, then group renditions by fingerprint. Do the arithmetic rather than asserting a win: 12 x 3,600 = 4.32 x 10^4 element hashes per version, so 4.3 x 10^8 a day against 2.4 x 10^9 comparisons. That is 5.5x, not an order of magnitude, and 5.5 is not a remembered constant - it is (R - 1) / 2, the R^2 to R factor, which falls to 2.5x on a six-rung ladder. Per element a hash mix also costs more than an int64 compare, so treat 5.5x as an upper bound on element touches, not on wall clock.
  3. The durable reasons to prefer the fingerprint are structural rather than that ratio: each list is read exactly once so renditions never need to be resident together, and 8 bytes per rendition persists, which is what lets the check run incrementally as rungs become ready instead of as a batch. Say which direction can lie: a mismatch is always a real difference, while equality is equality up to collision probability, which is what you are trading away.
  4. Localise a failure with prefix hashes: build a prefix-hash array per rendition in O(S), then binary search for the lowest index at which two renditions' prefix hashes differ. O(log S), about 12 probes instead of a 3,600-element scan.
  5. Check the stronger property at publish time, since equal lists do not prove the boundaries are legal. Frames per segment = segment_duration_ms * frame_rate / 1000, and gop_size_frames must divide it. At exactly 25 fps a 6,000 ms segment is 150 frames and GOP sizes 25, 50 or 75 divide it cleanly. Do the NTSC case in the rational rate, not in the stored column: the true rate is 24000/1001 fps, so a 6,000 ms segment is 144000/1001 = 143.86 frames and can never align, while 6,006 ms is exactly 144 frames. The frame_rate_milli_fps column stores 23976, a rounding that drifts about 1 ms per 1,000 s (roughly 7 ms over a two-hour title) and computes that same aligned segment as 143.999856 frames.

Follow-up

  • The audit is green but one player stalls switching from rung 4 to rung 5 at 00:41:12. What do you check that list equality cannot tell you?
  • Run the audit incrementally as each rendition reaches status ready rather than as a nightly batch. What do you persist per content version, and what does a rung that arrives a day late cost you?

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.

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?

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.

Worked solution (40 min)

  1. Build a scaled copy with a known duplicate population and a writer process inserting continuously throughout.
  2. Hold a long SELECT open, then run the ADD COLUMN with lock_timeout set, and observe what queues behind what.
  3. Backfill in batches, recording dead tuples and table size after each batch.
  4. Enumerate duplicates with ROW_NUMBER and write reversing rows for every row past the first — leaving one duplicate in place deliberately.
  5. Run CREATE UNIQUE INDEX CONCURRENTLY, observe the failure, and inspect pg_index.indisvalid for the leftover.
  6. Drop the invalid index, reverse the last duplicate, rebuild, promote to a constraint, and set NOT NULL through the NOT VALID check path.

Expected result: No writer statement blocks for longer than the lock_timeout at any step. The first concurrent build fails and leaves an index with indisvalid = false that must be dropped rather than retried over. After the duplicates are reversed the build succeeds, promotion adds the constraint without a rebuild, and NOT NULL is set without a second full scan.

Verify it yourself

  • pg_index.indisvalid is true for the final index and no invalid index remains behind.
  • The writer process logs no errors and no statement waiting longer than the configured lock_timeout.
  • Every pre-existing duplicate has a reversing row, and the count of rows in status pending or counted per dedupe_key is exactly 1.
  • The SET NOT NULL step finishes in far less time than a full scan of the table takes, confirming it used the validated check.

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?

How would you design a system from scratch?

Medium

Approach

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

Follow-up

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

What are the strengths and weaknesses of Python?

Medium

Approach

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

Follow-up

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

Turn at-least-once heartbeats into exactly-once qualified plays

Medium · idempotency · partitioning · backpressure · sessionisation

Heartbeats arrive at 100,000 per second carrying (session_id, seq, played_ms, position_ms, client_ts). Delivery is at-least-once and out of order, a crashed player resumes under the same session_id, and an offline client can deliver days of buffered events in one burst. qualified_play carries dedupe_key UNIQUE, status ENUM('pending','counted','reversed') and counted_in_period, and money is paid against it. Give the stream partition key, how watch time is accumulated, the sessionisation rule including sessions that never report an end, the dedupe key, the period-close policy, and the backpressure for bulk replay.

Approach

  1. Partition by session_id and say why the alternative loses. The dedupe grain is the session, so partitioning by session_id makes the seen-sequence state partition-local and gives per-session ordering from per-partition ordering alone; client-generated UUIDs hash evenly, so no partition goes hot. Partitioning by account_id colocates an account's sessions and buys nothing, because no rule in this pipeline spans sessions.
  2. Order and deduplicate on (session_id, seq), never on client_ts. Client clocks are skewed and untrusted, so a timestamp is a data field rather than an ordering key; the per-session sequence number is the only monotone thing the client produces. Keep the seen-sequence set per session with a TTL past the idle gap so the state does not grow with lifetime session count.
  3. Accumulate watch time as MAX(played_ms) over the session rather than a sum of position deltas. Seeking backwards makes deltas negative and seeking forwards makes them enormous, so delta accounting turns a viewer scrubbing a title into a negative or multi-hour watch. MAX is also idempotent under duplicate and out-of-order delivery, which is exactly the property at-least-once transport demands, and it is the reason the naive version passes every test written from a happy-path trace.
  4. Sessionise as gaps-and-islands with an explicit idle gap, and close the sessions nobody closed. Order by (session_id, seq), open a new island when the gap since the previous heartbeat exceeds three heartbeat intervals plus margin, and run a reaper that sets ended_at = last_heartbeat_at and end_reason = 'unknown' for sessions past the gap, because a client that simply stopped reporting will never send an end. The query is O(n log n) dominated by the sort unless the store already clusters by (session_id, ts).
  5. Derive dedupe_key deterministically from session_id, content_version_id and the qualification-threshold version, so a crashed player regenerating it collides on the unique index. Insert with ON CONFLICT DO NOTHING and never update in place; a correction is a reversing row so the period close stays reproducible from the table alone. Hold the period open for at least the maximum offline licence life, then close, and let anything later land in the next period with counted_in_period set to that close.
  6. Give bulk replay its own path. A days-long offline sync is not proportional to current concurrency and cannot be capacity-planned from it, so route it to a separate topic with its own consumer group and lower priority, keeping operational alerting on startup time and rebuffer ratio out from behind a replay. Rate-limit per device at ingest with 429 and Retry-After, and require a durable client-side queue with jittered retry, because these events are payable and dropping them loses a rights holder money.

Worked solution (40 min)

  1. Choose the partition key and state the ordering and dedupe guarantee it gives you, plus the one it does not.
  2. Write the gaps-and-islands query with the idle-gap constant, and add the reaper that closes never-ended sessions.
  3. Define dedupe_key's inputs and argue it survives client retry, a player crash and a bulk offline sync.
  4. Write the period-close rule and the shape of a reversing row for an arrival after close.
  5. Size the replay path: separate topic, per-device limit, and the consumer-lag signal you alert on.

Expected result: session_id as the partition key with dedupe state kept local and TTLed, ordering by sequence rather than timestamp, watch time as MAX(played_ms), a gaps-and-islands rule with an idle gap of three heartbeat intervals plus a reaper for sessions with no end, a deterministic dedupe_key inserted with ON CONFLICT DO NOTHING, a close held at least as long as the maximum offline licence with reversing-row corrections, and a separate lower-priority replay path with per-device rate limiting.

Verify it yourself

  • Replaying an entire session's heartbeats a second time changes no qualified_play row and no watch-time total.
  • A session containing backward seeks produces a watch time no greater than its wall-clock duration.
  • An offline sync arriving after period close produces a row in the next period, and the earlier close still reproduces its original total.
  • A crashed player resuming with the same session_id yields exactly one qualified_play for that content version.

Follow-up

  • A device ships with a bug that resets seq to zero every 65,536 events. What breaks, and how do you repair the affected days?
  • Finance needs a period closed on the third working day, but offline licences last 30 days. Reconcile those two requirements concretely.
  • How would you detect that a client is inflating played_ms, and what would you do with the sessions you flag?

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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 who has spent the last few years shipping features and reading other people's code, and who has not solved a timed problem from a blank file in a long time. Five days rebuild the primitives and the patterns that sit on them, working from invariants rather than remembered solutions, and the last two attach that back to the rest of the loop.

Day 1 — Rebuild the primitives by implementing them

  • Implement a dynamic array with doubling growth and an operation counter, then change the growth rule to add a fixed sixteen slots instead, and time both for n of ten thousand, a hundred thousand and a million. The fixed-increment version resizes n/16 times at O(n) each, so its total work is quadratic; doubling is what makes append amortised constant.
  • Implement a hash map with separate chaining and a load-factor resize, then insert ten thousand keys engineered to land in one bucket and record what happens to lookup time, so that average-case O(1) becomes a claim with a stated precondition rather than a reflex.
  • For dynamic-array append and hash-map insert, write down which cost is amortised rather than worst-case, which single operation pays the whole bill, and what a system with a hard per-operation deadline would have to do instead.

Deliverable: Two working implementations plus a timing table showing the input at which each structure's advertised complexity stops holding.

Day 2 — Arrays under an invariant: two pointers, sliding window, binary search

  • Solve longest-subarray-with-sum-at-most-K using a sliding window, then run it on an input containing negative numbers and watch it return the wrong answer: extending the window only moves the sum monotonically when every element is non-negative, and that precondition is the whole reason the technique works.
  • Write the binary search that finds the first index satisfying a predicate rather than an exact value, put the loop invariant above the loop in a comment, and verify termination on the two inputs that break careless versions: the empty range, and a range where every element satisfies the predicate.
  • Compute the midpoint as lo + (hi - lo) / 2 and write one line on why the obvious (lo + hi) / 2 is a genuine defect in a fixed-width integer type and a non-issue in a language with arbitrary-precision integers.

Deliverable: Three solved problems, each with its invariant written above the loop, plus one recorded input on which the sliding window is provably wrong.

Day 3 — Sorting, heaps, and the greedy argument that has to be proved

  • Solve one top-k problem three ways, by full sort, by a size-k heap, and by quickselect, then write the values of n and k at which each becomes the right choice, along with quickselect's quadratic worst case and why a randomised pivot makes that unlikely rather than impossible.
  • Implement bottom-up heapify and count sift-down steps to confirm it does linear work rather than n log n, because most nodes sit near the bottom of the tree and therefore move only a short distance.
  • Take interval scheduling by earliest finishing time and write the exchange argument out in full: given any optimal schedule, swapping in the earliest-finishing interval keeps it feasible and no smaller. Then construct the weighted variant where that same greedy fails and name what has to replace it.

Deliverable: A three-way top-k comparison with measured crossover points, one written exchange argument, and one counterexample to a greedy rule that looks almost identical.

Day 4 — Recursion, memoisation, and the step to a table

  • Take one problem with overlapping subproblems, such as edit distance or coin change, instrument the plain recursion with a call counter to show the blow-up, then add memoisation and re-count.
  • Convert the memoised version to a bottom-up table and state the two properties you relied on: each subproblem's result depends only on its arguments, and the dependencies form a DAG you can enumerate in order.
  • Rewrite one deep recursion with an explicit stack, then find the input length at which the original hits the interpreter's frame limit, which defaults to about a thousand frames in CPython, so you know when the rewrite is required rather than decorative.

Deliverable: One problem in three forms, naive, memoised and tabulated, with call counts for each and the input length at which recursion depth becomes the binding constraint.

Day 5 — Graphs, where most of the work is choosing the traversal

  • Implement BFS and DFS over one adjacency list, then answer for each which finds a shortest path in an unweighted graph and which you would use to detect a cycle in a directed graph, including why the in-progress versus finished distinction matters for the second.
  • Implement topological sort by in-degree, feed it a graph containing a cycle, and confirm the failure signature is that fewer than V nodes come out rather than an exception, then note that the order it produces is one of several valid ones.
  • Run a shortest-path search on a graph with a single negative edge weight and show the wrong answer, then write the precondition Dijkstra actually needs, non-negative weights, because it finalises a node's distance the first time that node is popped, and name the algorithm you would switch to and its own limit.

Deliverable: A small graph library with BFS, DFS and topological sort, plus two inputs that produce documented wrong answers under the wrong algorithm choice.

Day 6 — One day for everything that is not an algorithm

  • Sketch one system only to the depth a coding-heavy loop tends to reach: the endpoints, what the service stores, and the single query pattern that decides the schema. Stop at twenty-five minutes.
  • Prepare the project answer for an interviewer who codes, which means rehearsing the two levels they push to: the specific thing you built, and why you chose that approach over the alternative they will name. Open with a number and be ready to say what it excludes.
  • Prepare the answer to what you would do differently, choosing a real technical mistake with a specific fix rather than a complaint about process or staffing.

Deliverable: One design sketch at endpoint-and-schema depth, plus a project answer rehearsed to two levels of follow-up.

Day 7 — Solve out loud, under time

  • Do three timed problems at twenty-five minutes each in a plain editor with no autocomplete and no execution until the end, then tally separately the failures that were syntax and the ones that were approach, because those two numbers call for different fixes.
  • Narrate one solution from the first sentence, stating the approach and its complexity before writing any code, and rehearse the sentence you will use when you realise mid-solution that the approach is wrong.
  • Re-solve from blank the two problems you were slowest on this week and compare the times against the day they first appeared.

Deliverable: A recording of one fully narrated solution and a tally that separates syntax failures from approach failures.

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.

What excites you about the quantitative finance space?

Medium · behavioural and engineering judgement

Approach

  1. Give the blast radius: what could have broken, and what you measured.
  2. State the situation in two sentences and spend the rest on the reasoning.
  3. Close with what you would do differently, concretely.

Follow-up

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

Tell me about a time you had to defend a technical decision.

Medium · behavioural and engineering judgement

Approach

  1. Close with what you would do differently, concretely.
  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 would you do differently if you ran that again?
  • How did you know your change caused the improvement?

How do you approach learning new technologies under tight deadlines?

Medium · behavioural and engineering judgement

Approach

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

Follow-up

  • How did you know your change caused the improvement?
  • What would you do differently if you ran that again?

Why Aquatic Capital Management?

Medium · behavioural and engineering judgement

Approach

  1. Close with what you would do differently, concretely.
  2. Name the disagreement and how you resolved it with evidence.
  3. Give the blast radius: what could have broken, and what you measured.

Follow-up

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