Interview guides · Citadel Securities
Citadel Securities Software Engineer Interview Guide
Code reading practice for Citadel Securities software engineer prep: work out what unfamiliar code does before judging it.
Interviewing at Citadel Securities
- Practice bank: 6+ questions
- Rounds: 2
- Typical prep: 1–2 weeks
- Interview reports: 2
Interview rounds
| Round | Questions |
|---|---|
| Technical Screen | 2 |
| Onsite | 4 |
How hard is the Citadel Securities Software Engineer interview?
| Difficulty | Share | Questions |
|---|---|---|
| Easy | 0% | 0 |
| Medium | 100% | 6 |
| Hard | 0% | 0 |
What Citadel Securities actually tests for
| Topic | Share | Questions |
|---|---|---|
| Software Engineering Fundamentals | 100% | 6 |
The 60-second brief
A Software Engineer at Citadel Securities operates at the intersection of high-performance computing, complex financial modeling, and massive-scale data processing. You are not simply building applications; you are engineering the critical infrastructure that allows the firm to execute high-frequency trading strategies with microsecond-level precision. This role is fundamental to the firm’s ability to provide liquidity and maintain competitive advantages in global markets.
Citadel Securities candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
What a Software Engineer does at Citadel Securities
The work is characterized by extreme technical rigor and the need for absolute reliability. You will contribute to low-latency trading platforms, robust order management systems, or sophisticated data pipelines that handle millions of events per second. The environment is fast-paced and intellectually demanding, requiring engineers who thrive on solving "impossible" problems where performance, scalability, and stability are the primary constraints.
The Citadel Securities interview process
Citadel Securities candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Technical Screenings
The same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.
What it evaluates
- In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
- In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
- Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes
How to prepare
- Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
- Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
- For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first
Superday
A day like this is several different games in a row, and the expensive mistake is carrying the previous one into the next room. Coding rewards narrow precision and finishing inside a timer. Design rewards breadth, stated assumptions and naming what you are deliberately not building. Behavioural rewards specificity about people and decisions. Candidates who over-engineer a coding problem they were supposed to finish, or who start sketching class hierarchies before anyone has agreed what the system has to do, are usually still playing the last round. Between rooms, name out loud which game the next one is.
What it evaluates
- Whether the coding round ends with something that runs and has been traced against a degenerate input, rather than an extensible design that was never finished
- Whether a design discussion opens by agreeing on traffic shape, read-to-write ratio and what is allowed to be stale, instead of proceeding from an architecture you arrived with
- Whether a behavioural answer names a person, a disagreement and what you did about it, rather than describing the system the story happened inside
- Whether the opening habits still appear late in the day: restating the problem, asking for constraints, saying the plan before typing
How to prepare
- Book three mocks of different types back to back on one afternoon and ask each interviewer afterwards which round you answered in the wrong mode
- Write a three-line opening script per round type — coding: restate, name the approach and its cost, then type; design: ask for scale, read-write mix and what must not break; behavioural: name the person, the stakes and the decision — and run it off a card so the switch is mechanical rather than remembered
- Practise coding with a timer you do not extend, stopping when it stops, so the trained reflex is to finish a correct solution rather than to keep improving one
Team Interaction
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 it evaluates
- 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
Final Team-Matching Calls
Nobody in the room with you decides this. Interviewers typically write their rounds up separately, often before seeing anyone else's, and the outcome is settled later from those write-ups. A split panel gets resolved by whichever note carries specific evidence, so what you want out of each room is one concrete thing that person could write down: a bug you caught yourself, a trade-off you named, a decision you owned. The rest is arithmetic. The project you describe in a behavioural conversation is often the same system you sketched an hour earlier, and the two accounts have to agree.
What it evaluates
- Whether the scale, team size and timeline you attach to a project hold steady when that project resurfaces in a different round
- Whether each interviewer leaves with a specific thing to cite rather than a general impression of competence
- Whether a trade-off you defended in one round survives a challenge in another, instead of being quietly swapped for the answer the new interviewer seemed to want
- Whether a question you have already answered earlier in the day gets the same answer at the same depth, without visible impatience
How to prepare
- Write a one-page sheet per project fixing the figures you will quote — request volume, data size, team size, elapsed time, what broke — and say them aloud from the sheet until they come out identical every time
- For each round on the schedule, decide in advance the one sentence you want in that person's notes, then check in a mock that you said it outright instead of leaving it to be inferred
- Have someone ask you the same project question twice, an hour apart, and diff the two answers for numbers that moved or a trade-off that reversed
Questions Citadel Securities candidates report
Data structures and algorithms
Every fast solution here throws work away, and the claim you have to defend is that the discarded work could not have held the answer. A two-pointer walk over sorted values advances the left index only because every remaining pair using that element is also too small. A monotonic deque drops an element because a larger one sits to its right and will stay in the window longer. Say that sentence out loud. A weak answer names the technique and shows it passing the given examples, which is indistinguishable from a wrong technique that happens to pass the given examples.
- Implement a function to find the shortest path in a graph with specific constraints.
- Given a large stream of data, how would you design a system to maintain a running top-K elements?
- Write a thread-safe implementation of a circular buffer.
System design and architecture
Turn the prompt into a workload before drawing anything: requests per second at peak, the read to write ratio, the size of one stored object, how long it is kept, and how each of those grows. Those numbers decide the architecture, and without them every component is a guess. Size the simplest thing that fits, then add a tier only where a number says the simple version breaks. What separates a strong answer is that every box has a quantity attached to it. A cache, a queue and a partition scheme that each answer a specific number read as judgement; the same diagram without numbers reads as recall.
- Design a system for high-throughput messaging between different trading components.
- What are the differences between TCP and UDP in the context of high-frequency trading?
- How would you troubleshoot a scenario where network latency spikes intermittently between two data centers?
Behavioural and engineering judgement
Ownership questions here usually point at something with no owner and no deadline: an alert people mute, a suite everyone reruns until it passes, a service whose on-call is a rota of volunteers. Nothing forces action, so what you did says how you spend discretionary time. The strongest answers are not the ones where you fixed everything. They are the ones where you sized the problem first, chose between fixing it, containing it and writing it down for whoever comes next, told someone who could have overruled you, and can still say why the other two options were worse.
- How do you handle ambiguity when the requirements for a project are not fully defined?
- Tell me about a time you made a critical mistake in production; how did you resolve it?
Practice exercises with worked solutions
The prompts above tell you what gets asked.
Denormalising instrument attributes onto the position snapshot
A risk dashboard reads every open position for a desk once a second, roughly 50,000 rows, and each row needs price_scale, contract_multiplier and currency from instrument_version resolved as of now, plus a mark price. Today that is a range-predicate join per row against an effective-dated table. Propose a model that serves this read, state exactly which columns you copy and what makes the copy safe, and say what happens to yesterday's snapshot rows when a vendor correction changes an instrument version that was already in effect.
How a strong candidate works it
- Characterise the read before choosing a shape: 50,000 rows a second, attributes immutable within a version, versions changing at most daily for almost every instrument. Be accurate about why the as-of join costs what it does, because the tempting explanation is wrong. The join carries instrument_id equality alongside the effective-dating predicate, so the planner hashes or merges on that key and applies effective_from <= now() AND (effective_to IS NULL OR effective_to > now()) as a filter qual; a range predicate does not prevent a hash join. The real costs are that the scanned side is the whole version history rather than the single row per instrument actually in effect, that two correlated inequalities are a selectivity the planner estimates badly, and that the answer is recomputed every second while being constant for the entire business date. On 50,000 positions against 150,000 stored versions the hash join discards two rows for every one it keeps, and the per-row nested loop a bad estimate would choose instead costs 50,000 index descents and roughly three times the runtime.
- Prefer the middle option to either extreme. Materialize an as-of dimension once per business date, one row per (instrument_id, business_date) resolved at session start, and make the dashboard an equality join on that pair. The range predicate disappears without duplicating attributes onto every position row.
- If you do copy onto position_snapshot, copy price_scale, contract_multiplier and currency together with their provenance: the effective_from (or a version surrogate) of the instrument_version they came from. A denormalised copy without provenance cannot be audited, cannot be reconciled and cannot be replayed.
- Handle the intraday hazard explicitly instead of hoping: a corporate action effective mid-session changes contract_multiplier within a business date, so a per-date dimension is wrong for that instrument that day. Either key the dimension at a finer grain for affected instruments or fall back to the as-of join for them, and know which instruments those are from the corporate-action feed rather than from a support ticket.
- Answer the correction question the books-and-records way: a derived snapshot records what the system believed when it computed, so a later vendor correction does not rewrite yesterday's rows. It produces a new snapshot row or a break, which is the same principle that keeps eod_derived and eod_reconciled as separate rows rather than one mutable number.
- State the cost you accepted: writes amplify, the dimension build becomes a session-start dependency, and a missed rebuild serves yesterday's attributes silently, so the dimension needs a freshness assertion that fails the dashboard rather than a scheduled job and optimism.
Expected result (25 min): The scanned side shrinks from every stored version to the one row per instrument in effect that date, the effective-dating filter quals and the misestimate that can flip the plan into a per-row nested loop both disappear, and the resolution stops being recomputed on every refresh. The mid-session contract_multiplier change is visibly wrong in the per-date dimension, which is the reason to handle those instruments separately rather than a reason to abandon the design.
Check yourself
- Dimension row count equals the number of instruments with a version in effect on that date.
- Every position row joins to exactly one dimension row; a LEFT JOIN producing NULLs means a missing instrument, not an empty desk.
- The dimension carries the effective_from it resolved from, and three spot-checked rows match instrument_version.
Follow-ups
- The dashboard now wants 30 days of the same view. Does the per-date dimension still work, and what does the join key become?
- Where does mark_px live in your model, and what stops a stale mark from being indistinguishable from a fresh one?
- How large is the dimension, and does it fit in memory on the service that serves the dashboard?
A desk report that silently multiplies filled quantity
A report joins order_event (many rows per order_id: submit, ack, each partial_fill, fill), execution_report (one row per venue execution, keyed (venue_mic, venue_exec_id)) and risk_limit (PRIMARY KEY (limit_id, limit_version), one row per version per effective window) to show, per strategy_run_id, filled quantity, filled notional, and the max_order_notional in force. Filled quantity comes back roughly five times too large and notional shifts run to run. Identify both fan-outs, give the diagnostic that proves each one, and rewrite the query so the totals are correct.
How a strong candidate works it
- Name the mechanism rather than the symptom: an inner join multiplies rows, and SUM over a multiplied row set multiplies the measure. Each execution matches every lifecycle row of its order, and each limit scope matches every stored version, so the two factors compound.
- Prove each fan-out with a cardinality probe on the key alone, before touching the aggregate: GROUP BY order_id HAVING count() > 1 on order_event, and GROUP BY limit_id HAVING count() > 1 on risk_limit. Then compare COUNT() of the joined set against COUNT() of execution_report restricted the same way; the ratio is the multiplier.
- Fix the execution side by aggregating before joining: a subquery summing last_qty and the notional per order_id, joined one-to-one against a genuine one-row-per-order source such as the submit event, instead of joining the event log directly.
- Fix the limit side with a LATERAL that picks the one version in force: effective_from <= the order's sent_ts AND (effective_to IS NULL OR effective_to > sent_ts) ORDER BY effective_from DESC LIMIT 1. Picking by MAX(limit_version) is a different answer and usually the wrong one, because the newest version may not have been in force when the order was sent.
- Scale notional correctly while you are in there: last_px is an integer at the instrument's price_scale and derivatives carry a contract_multiplier, so notional is last_qty * last_px * contract_multiplier / 10^price_scale, with both attributes resolved from the instrument version as of the execution.
- Reject the reflex fixes explicitly: SELECT DISTINCT and SUM(DISTINCT last_qty) both change the answer by collapsing two legitimately equal fills into one.
Follow-ups
- A LEFT JOIN to risk_limit would keep orders that matched no limit. What does that do to your totals against an inner join, and which do you actually want?
- How would you catch this class of bug automatically before the report ships to a desk?
- Which of these joins would you push into a materialized view, and what event invalidates it?
Parse a tag-value wire message without allocating
A venue order-entry message is a byte buffer of tag=value fields separated by a 0x01 byte. The second field is a body length; the last is a modulo-256 checksum over every preceding byte, written as three ASCII digits. Some tags legitimately repeat inside repeating groups. Messages are at most 4 KiB and arrive at 10^6 per second. Extract eight named scalar tags as (offset, length) spans into a stack struct, and validate both the body length and the checksum, in one pass with zero allocations and no exceptions on the hot path. State the bytes touched per message and how you report a malformed message.
How a strong candidate works it
- Make one forward pass over the buffer that does three jobs at once: accumulate the checksum into a wrapping u8, parse each tag's ASCII digits into an integer until the '=', and record the value's start offset until the next 0x01. Bytes touched is exactly the message length once, which at 4 KiB and 10^6 messages per second is 4 GB/s of sequential reads and therefore the real ceiling on this design.
- Never materialise a value. Store (offset, length) pairs into a fixed-size struct on the stack, and parse integers directly out of the buffer when the field is consumed. A substring or a string_view constructed with a copy is the allocation that turns a 200 ns parse into a 2 us one under load.
- Dispatch on tag number with a small direct-indexed table for tags below 256 and a switch for the rest, rather than a hash map: eight tags of interest means the table is a branch-free load, and the map would add a hash and a probe per field.
- Handle repeating groups structurally, not by precedence. Record the offset at which the first group-count tag appears; a scalar capture is valid only before that point, and member fields after it belong to the group. Last-one-wins over a flat table is the failure this prevents, and it corrupts silently because the group's member tag is a perfectly legal tag number.
- Validate in the same pass: the body length counts the bytes from the byte after the body-length field's delimiter through the delimiter preceding the checksum tag, and the checksum is the running sum of every byte before the checksum field taken mod 256 and compared against the three digits. Both are cheap because the running sum already exists.
- Return a status enum, never throw. A malformed message on the decode path becomes a counter increment, a copy of the buffer into a slow-path ring for a logging thread, and a continue — throwing unwinds through the hot path and the cost lands on the next good message.
Expected result (30 min): The clean fixture returns status OK with eight spans whose resolved values match the scalar fields, and the colliding group member does not appear in any span. All three mutations return distinct non-OK statuses with no exception thrown and no partially populated struct handed onward. The allocation counter reads zero and the byte counter reads exactly message_length x iterations.
Check yourself
- The two independent checksum computations agree on the clean fixture and disagree on the mutated one.
- Allocation count is zero across 10^6 iterations, including on the malformed inputs where the error path runs.
- Removing the repeating-group guard makes one span change value, which is the concrete demonstration of the trap.
Follow-ups
- The message contains a length-prefixed raw data field whose value may itself contain a 0x01 byte. What changes in the scan?
- Where would SIMD actually help here, and where would it lose to the scalar loop?
- Two venues use the same framing with different tag numbering. How do you keep one hot loop without a per-venue branch inside it?
Evolve the as-of instrument contract without breaking clients
GET /v1/instruments/{instrument_id}?as_of=<ts> serves instrument_version rows: venue_mic, venue_symbol, tick_size NUMERIC(18,9), lot_size, contract_multiplier, price_scale, asset_class, trading_status ENUM('active','halted','auction_only','expired','delisted'), expiry_date, effective_from, effective_to. Six services consume it; two are operated by another team and cannot be redeployed on your schedule. Three changes are requested: add a new asset_class value, replace the scalar tick_size with a price-tiered schedule, and make expiry_date mandatory. Classify each as compatible or breaking, separately for readers and writers, and give the rollout.
How a strong candidate works it
- State the compatibility rule before classifying anything: for a response consumer, adding a field is safe, removing or repurposing one is breaking, and loosening a value domain is breaking. Direction matters, so classify per role rather than per field.
- New asset_class value: breaking for any client that switches exhaustively or validates against a closed schema. The only cheap mitigation is one you had to write into v1 before you needed it, a documented unknown-value rule such as 'an unrecognised trading_status is treated as halted' (fail closed, because the cost of quoting a halted instrument exceeds the cost of skipping a live one). Absent that rule, a new enum value needs a new version or a per-client value mapping.
- Tiered tick size: keep tick_size as the value applicable at the current reference price and add tick_schedule as a new optional array. Old clients continue to read a scalar that is correct for the common case; new clients read the array. Cut a major version only when the scalar becomes actively wrong rather than merely incomplete, and say which instruments those are.
- Mandatory expiry_date: for readers this is a tightening of the response and therefore safe, they simply stop seeing NULL. For writers, it is breaking, because a loader that omits the field now fails. Two different answers for one field is the point of classifying by role.
- Roll out with the version in the URL path or a media type, run N and N-1 concurrently, and gate the sunset on measured per-client usage rather than on an announcement, because the two services you do not control are exactly the ones whose deploy you cannot schedule.
- Pin the contract with tests over recorded responses for both versions in CI; the same recordings that make replay possible make this nearly free.
Expected result (30 min): A classification in which the enum addition is breaking absent a pre-declared unknown-value rule, the tick schedule is additive as an optional field alongside a still-correct scalar, and the mandatory expiry_date is safe for readers and breaking for writers, plus a dual-version rollout gated on measured usage.
Check yourself
- Deserialize a new-version payload with the old client's strict schema; if it throws, your 'additive' change is breaking.
- Confirm an old client reading only tick_size still computes a valid price for every instrument you can name, and list the ones where it does not.
- Confirm the sunset criterion is a number from a metric, not a date on a wiki page.
Follow-ups
- What does as_of mean for a row whose effective_from is in the future at the time of the call, and what does a correction to a past row do to a client that already cached the old answer?
- How do you tell which clients are still on N-1 when calls arrive through a shared gateway with no client identity?
End-of-day reconciliation slowed from four to seventy minutes
End-of-day reconciliation took four minutes a year ago and now takes seventy. The statement log shows 9.1 million SELECTs against instrument_version during the job, one per execution folded, each around 400 µs and dominated by round-trip time. The job converts last_px using price_scale from the instrument version in force at the execution's venue_ts. Give an ordered checklist that confirms the pattern from server-side evidence, the rewrite, the index it needs, and the complexity change you expect.
How a strong candidate works it
- Prove the pattern from the server rather than by reading code. Group the statement log or pg_stat_statements by normalised text and sort by call count, not by total time. An N+1 is identifiable as one statement whose call count is proportional to rows processed.
- Establish that the per-call cost is round trip rather than work. 400 µs for a single-row indexed lookup on a table of 10^4 to 10^6 rows is protocol and network; the plan will show an index scan returning one row in microseconds. That distinction decides the fix, because batching addresses round trips and indexing addresses work.
- Rewrite as one as-of join instead of N lookups: join execution_report to instrument_version on instrument_id with effective_from <= venue_ts AND (effective_to IS NULL OR effective_to > venue_ts). Row count is unchanged and total work is still O(N), but round trips fall from N to one and the join executes as a hash or merge join inside the server.
- Support the predicate with the right index. A btree on (instrument_id, effective_from) lets the as-of lookup be a descending range scan stopped at the first qualifying row. The exclusion constraint's gist index exists to enforce non-overlap and is generally not the cheaper path for a single as-of lookup, so read the plan rather than assuming which one is chosen.
- If the fold must stay row-by-row in application code, preload the dimension once into a hash map from instrument_id to that instrument's versions sorted by effective_from and binary search per execution. 10^4 to 10^6 rows is a bounded amount of memory, the lookup is O(log v) in that instrument's version count, and round trips drop to one.
- Re-measure and state the expectation explicitly: the job becomes bounded by the scan and the fold rather than by latency, which is minutes of CPU instead of an hour of waiting.
Follow-ups
- The versions table gains a row per instrument per corporate action. At what size does the preload stop being reasonable, and what replaces it?
- Why is a cache in front of the per-row lookup not the fix here?
- The as-of join returns two rows for one execution. What does that tell you about the data rather than the query?
Ship an order gateway with named, scheduled reconciliation debt
A venue connection must be live for a session start in three weeks. The order state machine, idempotent application on (venue_mic, venue_exec_id) and the pre-trade check are done. Drop-copy reconciliation and trade_correct / trade_cancel handling are not. Describe a time you shipped with known debt. State exactly what you cut, the detector you put in its place, the date the debt was scheduled for, who agreed to it, and what the debt actually cost before it was paid. Include the item you cut that you should not have.
How a strong candidate works it
- Split the cut list into two piles out loud: things that make the system slower to operate, and things that make it silently wrong. Only the first pile is negotiable, and saying so is the whole judgement being tested.
- Place trade corrections in the right pile. A fold that only adds quantities cannot represent a negating entry referencing an earlier execution at all, so cutting trade_correct is not an unhandled edge case — it is a position that is wrong with nothing raised. If you cut it, the compensating control is a break report that blocks the next session, not a log line.
- Attach a detector, a threshold and an owner to every item in the silently-wrong pile. Without drop-copy reconciliation the control is an end-of-day diff of derived against clearing positions, paging a named person on break_qty != 0, with a stated maximum position size until the real thing lands.
- Make the schedule concrete. The debt lands in a named release with a ticket, and the interim risk limit is what gets relaxed when it lands — which gives the desk a reason to care about the date instead of treating it as engineering's problem.
- Report what it actually cost: how many breaks per day, how much manual work, whether any of it reached a position, and which cut you would take back. The signal is your calibration, not your willingness to ship.
Follow-ups
- A trade correction arrives for a fill booked three sessions ago. What does your compensating control actually do with it?
- The debt slipped twice. What changes on the second slip?
- How would you scope this if the date were immovable and the desk wanted twice the size?
Where candidates lose points
Comparing timestamps from two different venues, or subtracting a venue timestamp from a local one and calling the result latency.
Each venue stamps on its own clock, at its own point in its own pipeline, at its own resolution, so a difference between two of them mixes elapsed time with clock offset and with unstated stamping conventions. Subtracting a venue timestamp from a local receipt timestamp yields network delay plus the offset between two clocks, and unless both are disciplined to a common source the offset can exceed the quantity being measured. Use one local clock for any latency you intend to act on, discipline it properly - PTP with hardware timestamping reaches sub-microsecond, while plain NTP over a LAN is typically sub-millisecond, three orders of magnitude too coarse here - and keep venue and receipt timestamps in separate columns so nobody is tempted to conflate them.
Representing prices and quantities as binary floating point.
IEEE-754 binary64 cannot represent 0.01 exactly, so a cost basis accumulated in doubles drifts, and an equality test against a tick boundary fails unpredictably. The fix is an integer at a fixed scale - a count of ticks, or a scaled integer at the instrument's price_scale - which also turns the tick-size check into a modulus rather than a tolerance comparison. The nuance that lets this trap survive code review: binary64 represents every integer exactly up to 2^53, about 9.0e15, so a scaled integer carried in a double is exact right up until it is not, and the first failure tends to be a large notional in a low-priced instrument, in production.
Writing code before the input contract is pinned down
Before the first line, state the types, the size bounds, whether duplicates, negatives or an empty input are possible, whether the input is sorted, whether you may mutate it, and what the function returns when nothing matches. Every one of those answers changes the code, and discovering one at minute twenty costs a rewrite you no longer have time for.
Sharing mutable state with no stated owner
Say which thread, request or task owns each mutable structure, and what protects it when the answer is more than one: a lock, a queue that hands ownership across, or an immutable copy per reader. A structure documented as safe for concurrent reads is usually not safe for a concurrent write alongside those reads.
Debugging and fundamentals, for someone arriving from a scripting background
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.