cswork / libraryOpen practice workspace

Interview guides · Chicagotrading

Chicagotrading Software Engineer Interview Guide

Code reading practice for Chicagotrading software engineer prep: work out what unfamiliar code does before judging it.

The 60-second brief

As a Software Engineer at Chicagotrading, you are at the intersection of high-performance computing, financial markets, and rigorous engineering excellence. The role is central to building and maintaining the systems that power Chicagotrading's trading infrastructure. You will be responsible for designing low-latency solutions, optimizing core data structures, and ensuring the reliability of Chicagotrading's microservices and trading platforms.

If the seat owns a service boundary, scope your preparation toward failure behaviour rather than topology. Retrying over an at-least-once channel produces duplicates by construction, so a retry policy is only as safe as the idempotency key underneath it.

Focus areas

  • Fold execution reports idempotently on venue exec id
  • Treat a send timeout as unknown, not failed
  • Budget and measure latency at the tail

Overview

This position demands a unique blend of technical precision and strategic thinking. You will be expected to solve complex problems under strict performance constraints, often working on systems where efficiency is measured in microseconds. Whether you are debugging order book templates, implementing custom pointers, or designing scalable scheduling systems, your work directly influences the firm's ability to operate effectively in fast-paced, competitive market environments.

The Chicagotrading interview process

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

Initial Screening

Half of this call is the part candidates treat as small talk: start date, notice period, work authorisation and its timing, location and time zone, on-call, and the number. Those are what kill offers late, after several engineers have each spent a day. Surfacing a hard constraint now costs you nothing and occasionally buys you something, since a loop compressed to fit a competing deadline can usually only be arranged if it is asked for early. The common failure is deflecting the compensation question twice, then discovering at offer stage that the band never reached your number.

What to demonstrate

  • Whether your hard constraints are compatible with the role before a loop gets booked: earliest start, notice period, what authorisation you hold and when it needs action, days on site, willingness to carry a pager
  • Whether you give a compensation range with something behind it, such as current total compensation or a competing timeline, rather than leaving the band untested
  • Whether your stated timeline is real, since a competing deadline raised now is something scheduling can sometimes work around and the same deadline raised at offer stage usually is not

How to prepare

  • Write each constraint down in one line before the call and state them as facts rather than negotiating them live under a question you were not expecting
  • Set your range from two or three current data points for that level and location, and name the structure you are quoting in, so the number is comparable to the one they are holding
  • If another process is running, say where it stands and by when, and ask directly whether this loop can be scheduled inside that window

Technical Assessments

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.

Behavioral Sessions

This round is deciding whether a change you make without supervision can be allowed to reach production. It is scored on what you knew at the moment you decided, not on how it turned out, so a story that opens with the result and works backwards reads as luck retold as judgement. Say what the options were, what you did not know, what you did to shrink the unknown before committing, and what you accepted as the worst plausible case. The detail that separates answers is a bound: how many users, how much data, and for how long, if you had been wrong.

What to demonstrate

  • Whether the reasoning you give was available at the time you decided rather than after the result came in, since a story whose deciding evidence arrived later describes an outcome and not a judgement
  • Whether you can put units on the exposure (users, rows, minutes of degraded service) and whether the containment you chose actually bounded it: a canary bounds the request path it fronts, while a background job writing to a shared table reaches every user regardless of which version served their requests
  • Whether the reversal path existed before you shipped or was improvised during the incident, and whether it restores state or only stops further damage

How to prepare

  • For your three largest changes, write down the one thing you would have had to be wrong about for it to fail, and what your best estimate of it was on the day you shipped. If you never held an estimate, that is the gap the follow-up questions will find
  • Write the undo procedure for one of those changes as it existed at the time, then mark which steps restore data and which only stop new damage. Turning a flag off or reverting a deploy ends the new writes; rows already written come back only from a copy you kept, and a dropped column comes back empty unless something outside the schema holds the values
  • Rehearse one story from the decision point forward and stop before the outcome, then have someone ask what you would do next. If the story only works with the ending attached, it is an anecdote rather than a decision you can defend

Final Onsite Rounds

Where the day includes a partner from product, design or data, that conversation is weighted like the technical ones and prepared for least. They are deciding one thing: whether having you in the room makes their decisions cheaper. That means options with costs attached, not implementation detail and not "it depends". An estimate someone can plan against — a range, the assumption that would push it to the high end, and what you would drop to hit the low one — is worth more than a confident single number, which everyone present already knows is wrong.

What to demonstrate

  • Whether an estimate comes as a range with the assumption most likely to break it, and states what a specific scope cut would actually buy
  • Whether a technical constraint is handed over as a choice with consequences on their side, rather than as a verdict they have no standing to argue with
  • Whether you establish what decision is on the table before proposing anything
  • Whether risk is raised while it can still change the plan, with the trigger that would confirm it, instead of reported afterwards as a slip

How to prepare

  • Take a project that shipped late and write the two-sentence warning you could have given three weeks earlier, naming what you would have needed decided at that point
  • Rehearse one estimate out loud until it arrives in three parts: the range, the single assumption that would blow it, and the smallest thing you would cut to protect the date
  • Rewrite an objection you have actually made — the "we can't do that" version — as two options with their costs, so the choice ends up with the person who owns it

Where candidates lose points

Allocating, taking a lock, or logging synchronously on the order path.

Each injects a delay whose magnitude depends on state you do not control: an allocation can fault in a new page, a lock can hand the core to another thread, a synchronous write can block on the filesystem. They also fail together, because all three are likeliest under load, which is when a burst is arriving. The hot path should preallocate, hand work to a logging thread over a single-producer single-consumer ring, and avoid any call that can enter the kernel. Verify by measurement rather than by reputation: a lock-free queue whose producer and consumer counters share a cache line can be slower than the mutex it replaced, because every update invalidates the other core's copy.

Retrying an order send after a timeout, on the assumption that the send failed.

A timeout says nothing about whether the venue received, matched and acknowledged the order - only that a reply did not arrive in time. Retrying turns an unknown into a duplicate: two live orders, twice the intended position, and a hedge computed against a position record that is now wrong. The correct move is to treat the client order id as an idempotency key, persist it before the send, and on recovery query state (order status request, or the drop-copy stream) rather than resend. It is the same shape as a double-captured payment, except the second order can move the price against you while you work out what happened.

Finishing a solution without stating its complexity

Give time and space in the same breath as the code, and define n explicitly when there are two sizes, since n nodes and m edges are not interchangeable. Space is the half that gets skipped: count the auxiliary structures you allocate and the recursion stack at its deepest, not only the answer you hand back.

Sorting when the problem never required a total order

Match the algorithm to the guarantee actually needed: the top k comes from a size-k heap in O(n log k) time and O(k) space, distinctness needs a set rather than an ordering, and a small bounded integer key range admits a linear counting pass. A full O(n log n) sort is the right default only when you genuinely need everything in order.

Questions & practice

Implement a sliding window technique to solve a frequency or sequence constraint problem.

Medium · data structures and algorithms

Approach

  1. Name the brute-force solution and its complexity before improving on it.
  2. Walk one small example through your approach before writing the whole thing.
  3. Restate the input: its shape, its size, and what is guaranteed about it.

Follow-up

  • What is the worst case, and how likely is it on real data?
  • Which test case would catch an off-by-one here?

Given an array of integers, return the smallest missing positive integer.

Medium · data structures and algorithms

Approach

  1. Choose the data structure from the access pattern, not from familiarity.
  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?
  • Which test case would catch an off-by-one here?

Given two arrays representing time and position, calculate the interpolated position at a specific time using linear fitting.

Medium · data structures and algorithms

Approach

  1. Name the brute-force solution and its complexity before improving on it.
  2. Choose the data structure from the access pattern, not from familiarity.
  3. State the target complexity and say which constraint rules the naive version out.

Follow-up

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

Find the minimum cost to delete characters from a string so that no two identical characters are adjacent.

Medium · data structures and algorithms

Approach

  1. Name the brute-force solution and its complexity before improving on it.
  2. Choose the data structure from the access pattern, not from familiarity.
  3. State the target complexity and say which constraint rules the naive version out.

Follow-up

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

Implement a custom unique_pointer or shared_pointer from scratch.

Medium · languages, concurrency and fundamentals

Approach

  1. Distinguish a value from a reference to it, and say which one you handed out.
  2. Identify the window where an invariant is briefly untrue.
  3. Name what is shared across threads and what owns each piece of state.

Follow-up

  • Where could this allocate more than you expect?
  • What happens if two callers reach this at the same time?

Discuss the concepts of locks, read-modify-write (RMW) operations, and atomic variables.

Medium · languages, concurrency and fundamentals

Approach

  1. Distinguish a value from a reference to it, and say which one you handed out.
  2. Say what the runtime actually does before reasoning about the code.
  3. Reach for the cheapest primitive that closes the race, not the broadest lock.

Follow-up

  • How would you prove the race exists rather than suspect it?
  • What happens if two callers reach this at the same time?

Publish top instruments by traded notional every second

Medium · top-k · heap · integer overflow · streaming

Fills stream in as (instrument_id, last_qty, last_px, contract_multiplier), with last_px an integer at the instrument's price_scale. Roughly 10^7 fills a day across 10^5 instruments. Publish the fifty instruments with the highest cumulative traded notional for the session, refreshed once a second, using exact integer arithmetic. Per-fill work must be O(1), and the per-second emit must not rescan all 10^5 instruments in the common case. Trade cancels and corrections can reduce an instrument's total. Give the accumulator width you need, and the argument that your pruning cannot drop a qualifying instrument.

Approach

  1. Size the accumulator before choosing the algorithm. A single fill of 10^6 lots at a price of 100 carried at price_scale 9 is 10^6 x 10^11 = 10^17 in scaled units, so an int64 session total overflows after about 92 such fills (9.22e18 / 1e17). Accumulate in int128, or rescale to a coarser notional unit at ingest and document the rounding; a double is out on the usual grounds, since it is exact only to 2^53.
  2. Per fill: one open-addressed hash map lookup on instrument_id, one 128-bit add, and an insert into a dirty set — O(1) expected, no allocation, no ordering work at all between emits.
  3. Per emit: maintain tau, the current fiftieth-largest total. Candidates are the previous top fifty plus the dirty instruments whose total exceeds tau. Select with a bounded min-heap of size 50, which is O(c log 50) for c candidates, and c is a few thousand per second rather than 10^5.
  4. State the precondition that makes the pruning sound: while totals only increase, an untouched instrument below tau cannot have crossed it, because nothing changed its value. Cancels and corrections break exactly that precondition, so a corrected instrument must enter the dirty set and, if it falls out of the top fifty, the replacement may be an instrument the pruning skipped.
  5. Close that hole rather than ignoring it: keep a reserve of the top 200 and refill from it when a member drops, falling back to a full O(m) quickselect over all 10^5 totals when the reserve is exhausted. The fallback runs rarely, is bounded, and is the reason the output is correct rather than usually correct.
  6. Report both costs: amortised O(1) per fill, O(c log k) per emit with an O(m) worst case, and note that the emit runs on its own thread off a snapshot so it never sits in the fill path.

Worked solution (30 min)

  1. Compute the overflow point by hand for the worst realistic fill (qty 10^6, px 100 at price_scale 9) and confirm the count at which an int64 total wraps.
  2. Build a fixture of 20 instruments with k = 3, feed fills so totals are 100, 90, 80, 70, ... and record tau after the first emit.
  3. Send a fill of size 5 to the instrument at 70, taking its running total to 75, and confirm it is pruned without entering the heap because 75 is still below tau; then send a further fill of size 20 to that same instrument, taking it to 95, and confirm it now enters the heap and displaces the instrument at 80.
  4. Send a trade cancel that removes 30 from the instrument at 100 and confirm the reserve supplies the replacement rather than the pruned set being consulted.
  5. Drain the reserve deliberately with a run of cancels and confirm the quickselect fallback fires and returns the same answer as a brute-force sort.

Expected result: An int64 total wraps after about 92 worst-case fills, so the accumulator is int128. With k = 3 the first emit gives {100, 90, 80} and tau = 80. The +5 fill carries that instrument from 70 to 75, still below tau, so the top three are unchanged; the further +20 fill carries the same instrument to 95, which enters the heap, displaces 80, and yields {100, 95, 90} with tau now 90. The cancel then takes the leader from 100 to 70, and the top three become {95, 90, 80} sourced from the reserve, identical to a full re-sort.

Verify it yourself

  • For a random 10^6-fill fixture, the incremental top-k matches a full sort of all totals at every emit, byte for byte.
  • The per-emit candidate count stays far below 10^5 in the steady state, and every fallback to quickselect is counted and explainable by a cancel or correction.
  • Replacing int128 with int64 on the worst-case fixture produces a negative total, which is the observable symptom of the overflow you sized for.

Follow-up

  • Make it a rolling five-minute window rather than a session total. What state does each instrument now need, and what does that do to memory at 10^5 instruments?
  • Two instruments tie exactly at the fiftieth place. What is your tie-break, and why does an unstable one make the output non-reproducible in replay?
  • The totals are in mixed currencies. Where does the FX conversion go, and what happens to the ranking when a rate updates mid-second?

Why the execution-report lookup stopped using its composite index

Medium · indexing · sargability · query-planning

execution_report holds 4 x 10^9 rows with an index on (account_id, received_ts). The query WHERE account_id = $1 AND received_ts::date = $2 runs a sequential scan. A second query, WHERE venue_exec_id LIKE $1 || '%', also scans despite a btree on venue_exec_id VARCHAR(48) in a database with a non-C default collation. A third, WHERE account_id = $1 alone, scans and the planner is right to. Explain each, give the rewrite for the first (a business date is a venue-local concept, not a server-timezone one), and say what EXPLAIN output you would ask for.

Approach

  1. First query: received_ts::date wraps the indexed column in a function, so the index's second column can no longer bound a range and only the account_id prefix is usable. If that prefix is not selective, a sequential scan is the cheaper plan and the index was never going to help.
  2. Get the rewrite's boundaries right rather than just removing the cast. Casting timestamptz to date depends on the session's TimeZone setting, so the same query means different instants in different sessions. A business date is defined by the venue's calendar, so take the session open and the next open from that calendar and write received_ts >= $open AND received_ts < $next_open.
  3. If the cast semantics are genuinely wanted, index the expression: (received_ts AT TIME ZONE 'UTC')::date is immutable because the zone is a literal, so it can be indexed, while the bare ::date is only stable and PostgreSQL will refuse it. The cost is pinning the zone into the index definition.
  4. Second query: a default-collation btree cannot serve a prefix LIKE, because the collation's sort order is not the byte order the pattern match needs. Rebuild it with varchar_pattern_ops (or run the database in the C collation), and keep the plain index too if equality lookups still need collation-aware comparison.
  5. Third query: an index scan returning a large fraction of the table reads most of the heap in random order on top of the index, so the sequential scan is correct. The fix is a different layout, such as partitioning by session_date or a BRIN on received_ts over a naturally time-clustered table, not a different btree.
  6. Ask for EXPLAIN (ANALYZE, BUFFERS) and compare estimated against actual rows. A two-orders-of-magnitude estimate error points at stale or correlated statistics, which CREATE STATISTICS or a composite index can fix; an accurate estimate that still chooses a scan means the scan is the right plan and the question is wrong.

Worked solution (30 min)

  1. Build a few million rows with (account_id, received_ts), ANALYZE, and run all three queries under EXPLAIN (ANALYZE, BUFFERS).
  2. Rewrite the first as a half-open range on the raw column and compare buffers read against the original.
  3. Create the index on ((received_ts AT TIME ZONE 'UTC')::date) and confirm the cast form now uses it; then attempt to index the bare ::date expression and read the error.
  4. Recreate the varchar index with varchar_pattern_ops and re-run the prefix query.

Expected result: Query one moves from a sequential scan to an index range scan with buffer reads falling by orders of magnitude; the bare ::date expression is rejected as not immutable; the prefix query uses the pattern_ops index; query three still scans, with its estimated rows matching actual.

Verify it yourself

  • Estimated rows are within an order of magnitude of actual on every plan you are willing to trust.
  • Buffers read by the rewritten query scales with rows returned, not with table size.
  • Dropping the expression index makes the cast form regress, which confirms it was the cause rather than a coincidence.

Follow-up

  • The rewritten range query is still slow because one account-day is 200 million rows. What changes about the physical design?
  • Before dropping an index you believe is unused, how do you establish that in production?
  • When does BRIN on received_ts beat the composite btree here, and what breaks that assumption?

As-of symbology resolution over an effective-dated instrument table

Easy · scd2 · temporal-queries · constraints · audit-history

instrument_version holds one row per instrument_id per effective-dated version: instrument_id BIGINT, venue_mic CHAR(4), venue_symbol VARCHAR(32), price_scale SMALLINT, effective_from TIMESTAMPTZ NOT NULL, effective_to TIMESTAMPTZ NULL. A vendor correction currently arrives as UPDATE ... SET venue_symbol = ... on the open row, and retired instruments carry an is_deleted BOOLEAN flag. Write the query that resolves (venue_mic, venue_symbol) to an instrument_id as of an arbitrary past instant, state the constraint that makes overlapping versions impossible, and explain why the in-place update and the is_deleted flag each break a replay.

Approach

  1. Fix the interval convention before writing anything: treat the window as half-open [effective_from, effective_to) with a NULL upper bound meaning open-ended, so every instant matches at most one version, an instant in a coverage gap matches none, and a boundary instant is not claimed by two.
  2. Write the predicate as effective_from <= $ts AND (effective_to IS NULL OR effective_to > $ts). Both halves are load-bearing, and dropping the upper bound in favour of ORDER BY effective_from DESC LIMIT 1 is not an equivalent query: at any instant inside a coverage gap, where a symbol has been released and not yet reassigned, the correct answer is zero rows, and the lower-bound-only form returns the last closed version instead, which is the issuer that used to hold the symbol. Keep both bounds. A btree on (venue_mic, venue_symbol, effective_from) still serves the full predicate with one descent and a backward walk, and INCLUDE (effective_to, instrument_id) lets the upper-bound test be evaluated from the index tuple, holding Heap Fetches at zero for as long as the visibility map is current for those pages. LIMIT 1 then buys a stopping condition rather than the semantics: under the non-overlap constraint at most one row can qualify anyway, and in a gap the walk reads every earlier version of that symbol before correctly returning nothing.
  3. Enforce non-overlap in the database rather than in the loader: EXCLUDE USING gist (instrument_id WITH =, tstzrange(effective_from, effective_to) WITH &&), which needs the btree_gist extension to get an equality operator class for a bigint. UNIQUE (instrument_id, effective_from) alone happily admits two windows that overlap.
  4. Replace the in-place update with close-the-current-row-then-insert-the-new-one inside one transaction, and replace is_deleted with a new version carrying trading_status = 'delisted'. A row that was true yesterday has to stay readable exactly as it was, because a replay reads it again.
  5. State the consequence that motivates all of it: venue symbols are reassigned to new issuers, so resolving today's symbol against today's open row can return a different instrument_id than the session being replayed actually traded.

Follow-up

  • A version was loaded three weeks ago with the wrong effective_from. How do you correct it without editing history, and what second time axis does that force into the table?
  • What does the lookup return during the instant a corporate action closes one version and opens the next, and what guarantees those two writes are one transaction?
  • A replay resolves a million symbol lookups per run. What changes about the query shape at that volume?

Classify order-entry errors as retryable, terminal, or unknown

Easy · error-taxonomy · retries · partial-failure

Your order API surfaces failures from four layers: transport (connection reset, write timeout), your own gateway (body validation, throttling, a full send queue), pre-trade risk (a risk_limit row with enforcement='block' and breach_action='reject_order'), and the venue (a reject carrying reject_reason_code). The caller is an automated strategy with no human in the loop. Define the error response body and a taxonomy it can branch on without parsing prose, then map at least six concrete failures to a class and a caller action. State clearly which failures are unknown rather than failed, and which are final even though nothing ever left your process.

Approach

  1. Fix the body first: a stable machine-readable code (closed string enum), a class field, a human message that carries no contract, the caller's client_order_id echoed back, and retry_after_ms where the class is retryable. Callers must never regex the message.
  2. Classify on two independent facts and only then collapse them into one class. Fact one: do you know for certain that no order from this request is live at the venue, either because nothing was sent or because the venue explicitly rejected it? That decides whether resending the same client_order_id is SAFE. Fact two: is the cause transient, so that the identical bytes could be accepted later? That decides whether resending can SUCCEED. A taxonomy that answers only the first question livelocks: the caller resends a deterministic rejection until its retry budget is gone, every attempt safe and every attempt identical.
  3. TERMINAL: the outcome is known and final, do not resend. This covers every deterministic rejection, wherever it was raised: a pre-trade risk block on max_order_notional, a 400 from body validation, a venue reject of an unknown symbol. Nothing about the order, the limit or the symbol changes by asking again with the same bytes, so a retry only burns rate budget and delays the strategy's own decision to do something different.
  4. RETRYABLE: no order exists at the venue AND the cause is transient, so the identical request can be accepted on a later attempt. That is a short list: 429 from your own throttle, a local send queue at capacity, connection refused before any byte was written, an explicit venue 'system busy' or 'session not ready' reject. Only this class carries retry_after_ms, and only it may be resent under the same client_order_id.
  5. UNKNOWN: you cannot rule out a live order, so the outcome is not knowable locally: write timeout, connection reset mid-write, no response at all, a 5xx your gateway raised after the venue write. Resolve by order status query or drop copy, and send nothing.
  6. Derive the class from what you know about venue state and whether the cause is deterministic, never from the HTTP status. A 500 from your own gateway raised after the venue write is UNKNOWN even though it is your error; a 400 from body validation never left your process and is still TERMINAL, because the same bytes fail the same check every time. For risk rejects, carry limit_id and limit_version in the payload so a rejection can be explained months later against the exact value in force, per risk_limit's PRIMARY KEY (limit_id, limit_version).
  7. Write the caller-side branch: TERMINAL records the code, updates local state and stops, and any follow-up (a smaller quantity after a notional block) is a new order with a new client_order_id, not a retry of this one; RETRYABLE resends the same key after retry_after_ms with jitter and a bounded attempt count; UNKNOWN issues an order status request or waits for drop copy, and never sends anything.

Worked solution (20 min)

  1. Write the error body as a concrete JSON object with every field named and typed.
  2. List seven failures (write timeout, connection reset after the venue write, connection refused before any byte was written, a 400 from body validation, a max_order_notional risk block, a venue reject of 'unknown symbol', 429 from your own throttle) and assign each a class.
  3. For each, write two sentences: whether the venue can now be holding an order from it, and what would have to change for an identical resend to be accepted. The pair, not either one alone, determines the class.
  4. Write the caller's three-branch handler and check by inspection that neither the UNKNOWN branch nor the TERMINAL branch contains a send call.

Expected result: A three-class taxonomy with a machine code per failure: every failure that can leave an order live at the venue is UNKNOWN and its only permitted action is a state query; every deterministic rejection is TERMINAL whether the venue, pre-trade risk or your own validator produced it; and RETRYABLE is reserved for failures that both stayed inside your process and can be accepted on a later identical attempt, with risk blocks carrying the limit version they were evaluated against.

Verify it yourself

  • For every entry classed RETRYABLE, state both why no order from that request can be live at the venue and what would have to change for an identical resend to be accepted; if you cannot state the first it is UNKNOWN, and if you cannot state the second it is TERMINAL.
  • Drive a deterministic failure in a loop (the same breached limit, the same malformed field): the caller must stop after the first response rather than resend, and no branch may resend after a timeout.
  • Every error code appears in a closed enum that a client can pin, and the human message appears nowhere in the caller's logic.

Follow-up

  • A venue reject arrives for an order your gateway already marked rejected locally on a risk block. Which one wins in the local state machine, and what does the API return to a status query in between?
  • How do you add a new reject_reason_code without breaking callers that switch exhaustively on it?

Resumable position stream with a replayable watermark

Hard · streaming · resume · idempotency · watermarks

Risk dashboards and an external allocator subscribe to intraday position updates over long-lived connections. position_snapshot is keyed by (account_id, instrument_id, business_date, snapshot_kind) and carries net_qty, avg_cost_px, mark_px, mark_source ENUM('last_trade','mid','settlement','vendor','stale') and fills_applied_through_exec_id as the watermark. Design the subscribe and resume contract: how a client disconnected for 90 seconds catches up without replaying the day, what happens when its resume point has aged out of retention, and how a consumer that must not double-count handles at-least-once delivery.

Approach

  1. Assign a server-side monotonic sequence per partition (account_id is the natural partition, since positions are folded per account) that is independent of any wall clock, and make the resume token (partition, last_seq, epoch). The epoch changes whenever the sequence is reset or the partition is rebuilt, so a stale token from a previous session cannot alias a new sequence and quietly skip a day.
  2. Define resume as 'send from last_seq+1, at-least-once, possibly redelivered', and publish the retention floor as part of the contract so a client knows how long it may be away.
  3. Make an expired resume point a typed error, not a silent restart from the head: the client must then do snapshot-then-increments, exactly as a market data book resynchronises. It fetches the snapshot with its sequence, buffers live increments arriving meanwhile, discards those at or below the snapshot's sequence, and applies the rest. Skipping the buffering step is what produces a book, or a position, that is wrong and well-formed.
  4. Prefer state-carrying messages over deltas: a message carrying net_qty, avg_cost_px and fills_applied_through_exec_id is idempotent under redelivery, whereas one carrying '+100' is not. Where a delta is unavoidable, carry the resulting sequence so the consumer can order and deduplicate.
  5. Put exactly-once at the sink, not on the wire: the consumer persists last_applied_seq per partition in the same transaction as the effect it applies, so a redelivered message is a no-op on restart. No transport gives exactly-once, and a design that assumes one has simply moved the bug.
  6. Carry staleness to the wire: mark_source='stale' must reach the client rather than being smoothed into a last known mark, so a dashboard can show a stale mark instead of an old number that looks current.

Follow-up

  • A slow consumer falls behind the retention floor while still connected. Do you disconnect it, drop messages, or buffer, and what does each choice cost the risk dashboard?
  • The allocator needs a consistent cross-instrument view for one account at a point in time. Does per-instrument sequencing give it that, and what would?
  • How do you prove after the fact that a client saw a given position value, given that the stream is ephemeral?

Position fold consumer restarts on the same message

Easy · poison message · idempotency · dead letter queue

At 10:14 the position fold stopped advancing. The consumer crashes and restarts every 30 seconds on an identical stack trace, lag grows, and intraday positions are stale on every desk. The message at the head is an execution_report with exec_type='trade_correct' whose corrects_exec_id refers to an execution id the consumer has never seen. The consumer commits its offset only after a successful apply. Give an ordered checklist to confirm the diagnosis and a fix that neither drops the correction nor blocks the partition.

Approach

  1. Identify the exact message before theorising. Read the stuck offset and the message itself; a restart loop with an unchanging stack is the cheapest bug in the system to pin down and the one most often guessed at instead.
  2. Classify it. Malformed and never-processable is one class; well-formed but not yet resolvable is another. A trade_correct whose referent is absent is usually the second: the original execution may have arrived only on the drop copy, may belong to an earlier business date, or may simply be behind this message in another partition.
  3. Confirm by looking for the referent. Query execution_report on (venue_mic, venue_exec_id) for the corrects_exec_id across all three source values and the adjacent business dates. If it exists somewhere, the defect is a visibility or ordering assumption in the consumer, not the message.
  4. Clear the head-of-line block without losing the record. Park the message on a retry queue with bounded attempts and increasing delay, and on exhaustion move it to a dead letter queue that raises an alert - never a silent drop, because a correction is the venue restating a trade already booked into a position.
  5. Make redelivery cheap rather than rare. Apply is idempotent on (venue_mic, venue_exec_id) and the offset commits after apply, so reprocessing costs nothing; a correction is applied as a negating entry against the original rather than an edit, which is what the retained corrects_exec_id is for.
  6. Alert on the shape, not the symptom. Consumer lag alone says slow; lag with a flat committed offset and a rising restart count says poisoned, and those two want different responses from whoever is paged.

Follow-up

  • The referent genuinely does not exist because the pipeline lost the original fill. What now, and what does the position row look like in the meantime?
  • How many retries before the dead letter queue, and what makes that number defensible rather than arbitrary?
  • Why is applying a correction by updating the original row the wrong shape for this table?

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.

The estimate and the day you knew

A slipped date is only a bad story if you sat on it. What matters is what you believed when you gave the number, the signal that told you it was wrong, how many days passed before you said so, and what you cut rather than asking for more time. Scope you defended counts as much as scope you dropped.

What has been your biggest obstacle, and how did you navigate it?

Medium · behavioural and engineering judgement

Approach

  1. Name the disagreement and how you resolved it with evidence.
  2. Close with what you would do differently, concretely.
  3. 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?

Tell me about a time during an internship where you experienced the most significant professional growth.

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

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

Retract a latency result you had already published

Medium · measurement · coordinated omission · retraction

You reported that a change cut p99 tick-to-trade by 40 percent, and the figure is now in a deck. A colleague shows that your harness sent the next request only after the previous reply arrived, so it stopped issuing load whenever the system stalled. Describe a time you retracted a result. State how you confirmed the error, who you told and in what order, what the corrected number was and how it was measured, and what you changed about how results get published so the next one is checkable by someone else.

Approach

  1. Confirm before announcing, but time-box the confirmation. Re-run under an open-loop generator that holds the intended send schedule and records latency from intended send time rather than actual send time, then compare it against the closed-loop run on the same build.
  2. Explain the mechanism precisely enough that the retraction is credible: a closed-loop harness never issues the requests that would have arrived during a stall, so the stall is sampled once instead of at the rate it would really have faced. That is coordinated omission, and it can understate the tail by an order of magnitude — so the correction is usually not a small adjustment to the same conclusion.
  3. Tell the people acting on the number before the people judging you, and say which decisions were made on it. If work was scheduled or descoped because of the figure, that belongs in the first sentence of the retraction, not the last.
  4. Give the corrected figure with its method attached: percentiles from a high-dynamic-range histogram, the load pattern, and the utilisation it was taken at. A tail number without a utilisation is not a number, because waiting time rises sharply as utilisation approaches one and real market data bursts harder than the queueing models that describe it.
  5. Separate what still holds from what does not. The change may remain a genuine improvement of smaller size, and saying so is part of being accurate — over-retracting is a failure of accuracy in the other direction.
  6. Change the process rather than only the number: require the harness type and the utilisation on every reported latency result, and publish percentiles rather than means by default. Say whether that was adopted or quietly ignored.

Follow-up

  • At what utilisation were both numbers taken, and why does that matter more than the change you made?
  • Your open-loop generator cannot keep up at the target rate. How do you tell that apart from the system being slow?
  • What would you need to see before trusting a latency claim from someone else?