The 60-second brief
As a Software Engineer at Millennium, you are at the intersection of high-frequency finance and advanced engineering. You will be responsible for building, maintaining, and scaling the sophisticated technology platforms that drive the firm's trading operations. Whether working on Fixed Income Technology or Equities Research Technology, your code directly impacts the firm's ability to execute complex strategies with precision and speed.
Millennium candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
What a Software Engineer does at Millennium
This role is critical to maintaining a competitive edge in global markets. You will be expected to solve high-stakes problems involving low-latency systems, massive datasets, and real-time analytical tools. Working at Millennium requires a blend of rigorous technical discipline and the agility to adapt to the fast-moving requirements of a premier hedge fund.
The Millennium interview process
Millennium candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Technical Screens
What this round decides is narrow: whether you can produce code that runs and is correct on inputs nobody showed you. An elegant solution that does not compile scores below a plain one that does, so write a correct brute force first, say out loud that you know its cost, and improve it with the working version still on screen. What separates strong answers is who finds the broken case. Trace your own code against an empty input, a single element, and duplicate keys before you say you are finished, because being told is far more expensive than noticing.
What it evaluates
- Whether degenerate inputs get checked without being asked for: an empty collection, one element, every element equal, and the extreme value the input type allows
- Whether the complexity you state matches the code you actually wrote, including a sort or a copy sitting inside a loop
- Whether the finished answer is verified against the worked examples before you call it done, rather than assumed correct because the code reads correctly
How to prepare
- Take five problems you have already solved and, without running anything, write down what each returns for empty input, a single element, and all-duplicates. Then run them and count how many you predicted wrong.
- Drill the brute force as its own skill: on ten problems, write only the obviously-correct slow version and time how long it takes to get it passing. If that is more than a few minutes, that is what to practise, not the optimal version.
- Add a fixed last step before you submit anything, reading only the loop bounds and the initial value of each accumulator, which is where most off-by-one errors live
Deep-Dive Coding Sessions
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 it evaluates
- 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.
System Design Discussions
Because the format is not fixed, the first job in the room is classification. Listen to the opening question and decide what it is: a probe into work you have already described, a fresh problem to solve now, or a conversation about how you operate. Each wants a different register, and the common failure is forcing a rehearsed structure onto a question that did not ask for it. Running a full design ritual on a ten-minute debugging question reads as not listening. When you cannot tell which it is, ask how long they want to spend and answer at that depth.
What 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
Cultural Fit Assessment
Input bounds are the part of the prompt most often skimmed, and they usually contain the answer. They tell you which complexity class is admissible, which narrows the search before you have thought about the problem itself. As a rough planning figure, a compiled language does on the order of 10^8 simple operations per second and an interpreted one roughly an order of magnitude less. So n up to about twenty admits enumerating subsets, a few thousand admits a quadratic pass, and a million admits neither: you need near-linear, or linear with a log factor. If the bounds are missing, ask for them.
What it evaluates
- Whether the approach is justified by the stated input size rather than by whichever pattern you recognised first
- Whether you ask about the properties that change the algorithm: whether the input arrives sorted, whether duplicates occur, whether values are bounded integers, whether it all fits in memory
- Whether you can name the bottleneck in your own solution and what would remove it, even when you deliberately leave it in place
- Whether a claimed speedup is real, since memoising a recursion only helps when subproblems genuinely overlap and the state can be keyed cheaply
How to prepare
- For each algorithm you rely on, write down the largest n it handles in roughly a second, then check two of those figures by timing them in the language you will actually type in
- For two weeks, write one line naming your target complexity and the bound that justifies it before you write any code, then compare that line with what you ended up submitting
- Practise the conversion backwards: given a required O(n log n), list the mechanisms that get you there (sorting, a heap, an ordered map, divide and conquer) and choose by what the problem needs to query, not by what you used last
Questions Millennium candidates report
These are the prompts PracHub has collected for this role and company. They are a record of what candidates describe being asked, so treat them as the shape of the conversation rather than a script Millennium works from.
Data structures and algorithms
Ask which resource the problem actually takes away, because it is not always time. If the input does not fit in memory, the set you were going to build is gone and the bound that matters is space. If the data arrives once and cannot be revisited, anything needing a second pass is out whatever its complexity, and a constraint banning modification of the input removes the in-place trick that would otherwise be free. Naming the scarce resource first cuts the candidates down to a handful, and it stops you optimising a dimension the problem was never short of.
- Describe an efficient way to find the top K elements in a massive stream of incoming data.
Behavioural and engineering judgement
Most engineering disagreements worth asking about are not settled by evidence, because both positions are defensible on what is known at the time: a schema shape, an abstraction one of you thinks is premature, fixing versus rewriting. The useful move is to say what would settle it, and what you do when nothing will. A common tiebreaker is which option is cheaper to undo, so you take the reversible one and set a date to look again. Weak answers argue who turned out to be right. Strong ones explain how the call got made while it was still undecidable.
- Tell me about a time you disagreed with a technical decision made by a team lead; how did you resolve it?
- How do you balance the need for rapid feature delivery with the necessity of maintaining robust, bug-free code?
- Describe a time you had to troubleshoot a critical system failure under tight deadlines.
Other topics
A large share of these are really questions about evidence: why something is slow, or how much of something you would need. Convert the prompt into a number or an observation. For diagnosis, name the measurement that would tell two candidate explanations apart and say what each predicts before you collect it. For sizing, carry the units through and stop at one significant figure. Listing everything that could plausibly be true is what the weak version looks like. What separates the strong one is a hypothesis, the observation that would falsify it, and what the arithmetic already rules out before any new data arrives.
- How would you handle a scenario where a data feed experiences significant latency spikes?
Practice exercises with worked solutions
The prompts above tell you what gets asked. These are PracHub exercises written to drill the same skills, with the reasoning spelled out so you can check your own answer instead of guessing whether it was good enough.
Derive end-of-day positions from executions including trade corrections
You have position_snapshot rows with snapshot_kind = 'sod_clearing' (account_id, instrument_id, business_date, net_qty) and execution_report (PRIMARY KEY (venue_mic, venue_exec_id), order_id, instrument_id, account_id, exec_type, last_qty, last_px, corrects_exec_id, source, venue_ts, received_ts). Write the query producing eod_derived net_qty per account and instrument for one business date. exec_type includes trade_correct and trade_cancel, which arrive as new rows referencing an earlier execution; the original row is retained. Side lives on order_event, not here. Then say why avg_cost_px cannot be computed in the same statement.
How a strong candidate works it
- Get side without fanning out: join each execution to the single order_event row with event_type = 'submit' for its order_id. Joining on order_id alone multiplies every fill by the number of lifecycle rows that order accumulated, which is five or more on a partially filled order.
- Reduce corrections to an anti-join instead of arithmetic. Exclude any execution whose (venue_mic, venue_exec_id) appears as the (venue_mic, corrects_exec_id) of a later row: a superseded fill then contributes nothing, a trade_cancel contributes nothing itself, and a trade_correct contributes its restated last_qty once. Match on venue_mic too, since venue_exec_id is only unique within a venue.
- Sum CASE WHEN side IN ('buy','buy_to_cover') THEN last_qty ELSE -last_qty END over exec_type IN ('partial_fill','fill','trade_correct'), grouped by (account_id, instrument_id), then FULL OUTER JOIN onto the sod_clearing rows with COALESCE on both sides so an instrument traded from flat and one closed to flat both appear.
- Scope the date by the venue's business date, not by received_ts::date in the server's timezone. A correction for yesterday arrives today and belongs to yesterday.
- Do not dedupe in the reader. The primary key already makes a resend from drop_copy or the clearing file a no-op at write time (ON CONFLICT DO NOTHING), so a DISTINCT in the query is either redundant or hiding a schema problem.
- Answer the avg_cost_px question precisely: average cost depends on the running position at the moment of each fill, because a reducing trade leaves it unchanged and realizes P&L while a sign flip resets it. That is a sequential fold with carry, not an aggregate. SUM(...) OVER (ORDER BY ...) gives running quantity but cannot carry the average, so it needs a recursive CTE or an application-side fold.
Expected result (35 min): Per (account_id, instrument_id): net_qty equals the start-of-day quantity plus the signed sum of surviving fills, where the corrected partial contributes its restated quantity exactly once and the cancelled fill contributes nothing. Instruments appearing only in executions and only in the baseline are both present.
Check yourself
- Re-running after the duplicate insert attempt produces an identical result set.
- Hand-computing one instrument from the fixture matches the query to the share.
- The count of fill rows feeding the sum equals the number of surviving executions, not the number of order_event rows; a larger count means the side join fanned out.
Follow-ups
- A resent report for an execution already inserted from the trading session arrives on the drop copy. What does your write path do, and what does the reader have to change?
- Make this incremental so the intraday_derived row refreshes every few seconds without rescanning the day. What does the watermark have to be ordered by?
- The derived total differs from eod_reconciled by 100 shares in one instrument. What do you do to the derived row?
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?
Choose a book structure for constant-time top-of-book reads
You maintain a price-level book for one instrument on one venue. Deltas arrive as (side, price, new_aggregate_qty), where price is an integer at the instrument's price_scale and tick_size is constant within the session; new_aggregate_qty of zero deletes the level. Sustained 10^5 to 10^6 deltas per second, over 90 percent landing within ten ticks of the touch, and top of book is read after every delta. Choose the representation, then give apply(delta) and best_bid()/best_ask() with their expected cost, their worst case, and the memory per instrument per side. Say what happens when the price leaves your band.
How a strong candidate works it
- Derive the structure from the access pattern rather than from the abstract operation set: reads are overwhelmingly of one element (the touch), writes cluster within ten ticks of it, and the price domain is a dense integer lattice because every valid price is a multiple of tick_size. That argues for a flat array of aggregate quantities indexed by (price - base_px) / tick_size, with cached best_bid_idx and best_ask_idx, over any comparison-based container.
- apply(delta) writes one slot in O(1). An insert at a price better than the current best updates the cached index in O(1) with no scan. A delete of a non-best level is O(1). Only a delete of the best level requires finding the next occupied level inward, which is where the worst case lives.
- Make that scan cheap by shadowing the array with a bitset, one bit per level, 64 levels per 64-bit word: the next occupied level is a find-first-set from the current index, so a 4096-tick band costs at most 64 word reads and typically one. Memory is 4096 x 8 bytes plus 512 bytes of bitset per side, about 32.5 KiB per side, 65 KiB per instrument.
- Price the alternative honestly. A sorted map is O(log L) per update with correct semantics and no band, but it allocates a node per new level on the decode path and chases pointers on every read; the disqualifier is the allocation and the cache misses, not the logarithm. Keep the flat array for the actively quoted set (1,000 instruments is about 65 MB) and a map for the long tail, because 10^5 instruments at 65 KiB each is 6.5 GB.
- Handle the band exit explicitly: when a price falls outside [base_px, base_px + width x tick], rebase by memmoving the live window and clearing the rest, an O(width) operation that must be counted and alarmed rather than hidden, since it is a latency spike correlated with fast markets.
- Tie it to the gap invariant: the book carries a staleness flag, and an unrecovered sequence gap sets it so best_bid()/best_ask() report unusable rather than returning the last well-formed state.
Expected result (30 min): After the first two deltas, best bid is 10000 at qty 200 and best ask is 10001 at qty 400, a one-tick spread. After the remaining deletes the bid side is empty and best_bid() returns an explicit empty sentinel, never a zero-quantity level. The scan-word histogram is dominated by 1 with a tail no longer than 32/64 = 1 word for this band.
Check yourself
- Every best-level read matches a brute-force scan of the whole band over a 10^6-delta replay.
- An insert at a price better than the current best never increments the scan counter.
- Emptying a side returns the empty sentinel rather than the last known price, and flipping the staleness flag makes both accessors refuse rather than answer.
Follow-ups
- Move from price-level to order-level (each resting order tracked individually). What does that cost you, and what does queue-position modelling need from it?
- The venue sends a snapshot plus increments after a gap. Where do you buffer the increments, and at which sequence number do you start applying them?
- How do you verify this book against an independent full-depth snapshot mid-session without stalling the decode thread?
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.
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?
Signed drop-copy webhooks with at-least-once delivery
A clearing broker pushes execution reports to your HTTPS endpoint rather than letting you poll. Each POST carries venue_mic, venue_exec_id, exec_type, cum_qty, leaves_qty and corrects_exec_id, plus signature and timestamp headers. Delivery is at-least-once and unordered, and any non-2xx is retried with backoff. Specify signature verification, the accepted timestamp window, what you return and how quickly, and how the handler stays correct when the same execution arrives twice and when a trade_correct arrives before the fill it corrects.
How a strong candidate works it
- Verify over the raw request bytes before parsing: HMAC-SHA256 over timestamp, a separator, and the body exactly as received, compared in constant time. Parsing and re-serializing JSON changes key order and whitespace and will fail a signature that is actually valid, so the framework must hand you the buffered body.
- Support two active secrets at once so rotation is an overlap rather than an outage, and accept a signature matching either.
- Bound replay with a timestamp tolerance (a few minutes, sized to your clock-skew budget plus the sender's retry backoff): outside it, reject. This bounds how long a captured request stays useful; it does not replace idempotency, because redelivery inside the window is legitimate.
- Acknowledge fast and apply asynchronously: return 2xx once the raw message is durably enqueued, not once it is folded into a position. The sender's retry timer is shorter than your downstream pipeline, and a non-2xx means 'send it again', so never return 500 for a message you have already accepted.
- Make the sink idempotent on (venue_mic, venue_exec_id) with an upsert, and make it order-insensitive by taking the venue's cum_qty and leaves_qty as authoritative rather than incrementing a local counter. Unordered delivery then costs nothing.
- Handle corrections as arrivals, not edits: a trade_correct or trade_cancel names corrects_exec_id, which may reference a row you have not received. Store it pending and resolve on arrival of the original, and keep the original row so the fold sees a negating entry instead of an in-place update.
Expected result (30 min): A handler that authenticates on raw bytes with rotatable secrets, rejects stale timestamps, returns 2xx only after durable enqueue, and whose sink is idempotent on (venue_mic, venue_exec_id) and insensitive to arrival order because it takes the venue's cumulative quantity as authoritative and stores corrections as new negating rows.
Check yourself
- Replay a captured request an hour later and confirm rejection; replay it ten seconds later and confirm it is accepted and folds to no change.
- Deliver a report stream shuffled and duplicated; the resulting positions must equal the in-order, deduplicated run.
- Confirm that no code path returns a non-2xx after the message has been made durable.
Follow-ups
- The sender is down for two hours. How do you detect that, given that no request arriving looks exactly like no executions happening?
- Your endpoint returns 200 but the enqueue silently drops. What end-of-day control catches it?
- Would you let the sender's payload carry your position directly rather than the execution? What breaks?
Replay diverges once in twenty runs on wide machines
Replaying one recorded session through one build produces outbound order streams that differ in about one run in twenty. It reproduces only on 32-core hosts, never under the thread sanitizer, and never in the single-threaded debug build. Two divergence shapes appear: two orders swapping position in the stream, and one limit price differing in the last digit. Give an ordered checklist that locates the first diverging decision and separates the two causes, naming what you would pin and in what order.
How a strong candidate works it
- Make the divergence cheap to locate. Byte-diffing two output streams finds the first differing byte, which is usually far downstream of the first differing decision. Hash each decision into a running chain keyed by the input event's sequence number; the first differing chain entry names the event and the decision, turning a rerun-until-it-happens loop into a single comparison.
- Separate the two shapes, because their cause sets are disjoint. An ordering difference is an interleaving or an iteration-order problem. A value difference in the last digit is arithmetic, and the precise statement matters: one binary executing the same operations in the same order on the same inputs is bit-reproducible, so a value differing across runs means the order of operations changed or a different code path executed.
- For the ordering shape, enumerate and pin one at a time: more than one thread consuming the input stream; a work-stealing pool whose chunk boundaries vary with scheduling; a container whose iteration order is not a function of insertion order - a map keyed on a pointer whose value moves with address layout, a runtime with randomised map iteration or a per-process hash seed, or a set of objects with no stable hash. Force single-consumer ingestion inside the strategy boundary and replace those containers with ordered ones keyed on instrument_id or sequence number.
- For the value shape, pin the arithmetic. A parallel reduction whose partition depends on thread count or on stealing gives a different summation order and therefore different rounding; runtime dispatch to a different vector kernel does the same. Then pin the compiler: fast-math permits reassociation, and contracting a multiply and an add into a single fused operation changes the rounding of the intermediate. Build with contraction disabled and fast-math removed, and make any reduction's partition a function of input length alone.
- Do not treat a quiet sanitizer as a result. It perturbs timing and allocation enough to close the window and instruments only what it can see, so its silence is weak evidence. Run it, but prove the fix by making the run deterministic by construction and then replaying the recording several hundred times with a zero-tolerance comparison.
- Keep it from returning. Add a CI job that replays a fixed recording N times and fails on any difference, and enforce that nothing inside the strategy boundary reads a wall clock: event time arrives as data, and the wall clock is consulted once at ingestion to stamp received_ts.
Follow-ups
- It will not reproduce on an 8-core host. Why would core count matter, and does that narrow which of the two shapes you are chasing?
- A colleague proposes comparing prices with a 1e-9 tolerance so the test goes green. What does that cost you six months from now?
- The strategy needs a 200 ms timeout. How do you express it without reading a wall clock?
Where candidates lose points
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.
Reporting mean or median latency, and benchmarking with a harness that stops issuing load while the system is stalled.
The distribution that matters is the tail - p99.9 is where a stall meets a burst, and it is precisely the sample an average hides. The common closed-loop harness sends a request, waits for the reply, then sends the next, so it never issues the requests that would have arrived during a stall and never samples the stall at the arrival rate it would have faced. That is coordinated omission, and it can understate the tail by an order of magnitude. Measure with an open-loop generator that holds the intended send schedule, record latency from intended send time rather than actual, and publish percentiles from a high-dynamic-range histogram rather than from a running average.
Comparing floating-point values for equality, or holding money in them
Binary floating point cannot represent 0.1 exactly, so repeated addition drifts and an equality check fails on values that are mathematically equal. Store currency as integer minor units or a decimal type, and compare floats against a tolerance you chose for a stated reason.
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.
Breadth then depth, for a generalist facing a mixed loop
Four days sample coding, design, fundamentals and the practical rounds at deliberately shallow depth, which is enough to surface the topics you did not know were in scope. That map, rather than a guess made on day one, decides where the last three days go.
Day 1 — Coding, one pass at shallow depth
- Solve one problem from each of six families, an array with two pointers, hash counting, binary search, a tree traversal, a graph traversal and one dynamic program, under a hard twenty-minute cap with no extensions, marking each finished, late, or stalled.
- For every stall, write the exact move you could not make rather than the subject, so the note reads could not turn the recurrence into a loop rather than bad at dynamic programming.
- Fix nothing today. The value of the pass is the unfixed record.
Deliverable: Six timed attempts marked finished, late or stalled, each stall carrying a named blocking move.
Day 2 — Design, one pass at shallow depth
- Spend twenty minutes each on three different shapes, a read-heavy feed, a write-heavy ingest path, and something needing a transaction across two entities, stopping each at requirements, interface and data model.
- After each, write the first question you could not answer, which is usually a number you could not estimate or a failure mode you had no vocabulary for.
- Mark which of the three you would be most relieved not to be asked, and treat that as data rather than as a preference.
Deliverable: Three shallow designs, each with the first unanswerable question written at the bottom.
Day 3 — Fundamentals and the practical rounds
- Answer eight short questions in writing at four minutes each, covering the material that fills the gaps between the big rounds: what happens between a URL and a rendered page, what an index costs on write, when a process is preferable to a thread, and what conditions a deadlock requires.
- Do one thirty-minute practical task of the kind a take-home compresses: read an unfamiliar two-hundred-line file and write what it does, what you would change, and the one thing you remain unsure of.
- Score every answer fluent, correct but slow, or absent, and keep the absent ones visible.
Deliverable: Eight scored short answers and one written reading of unfamiliar code.
Day 4 — The rounds that are about you, and the map
- Deliver three behavioural answers aloud against a timer, a conflict, a failure you owned, and a decision made without enough information, marking any that ran past three minutes or contained no number.
- Assemble the map: every marked item from days one to three on a single page, sorted by how likely it is to appear in your loop rather than by how uncomfortable it felt.
- Choose exactly two areas for the remaining three days and write down what you are deliberately abandoning.
Deliverable: A one-page scored map of the whole surface area with two areas chosen and the rest explicitly abandoned.
Day 5 — First chosen area, to the depth you skipped
- Work the higher-ranked area in four focused blocks, choosing items one level above where you stalled rather than repeating what already works.
- After each block write the rule you extracted in one sentence with its precondition attached, since a rule carrying no precondition is exactly what fails under a variation.
- Re-attempt the day-one or day-two item that exposed this area and compare against the original timing.
Deliverable: Four worked blocks, a timed re-attempt against the original, and three one-sentence rules with preconditions.
Day 6 — Second chosen area, where the gap is coverage rather than speed
- Treat the second area differently from the first. Day five drilled something you could already half-do; this one is usually a topic you had simply never met, so build one worked reference example end to end and keep it, rather than attempting six problems badly.
- Write down the vocabulary you were missing on day two or three, five terms at most, each with the one sentence that makes it usable in an answer rather than the textbook definition.
- Redo the shallow attempt that exposed this area and note whether you now fail later in the problem, because moving the failure point is the realistic gain from a single day and is worth more than a score that did not change.
Deliverable: One worked reference example for the newly covered area, a five-term vocabulary list, and a note on where the failure point moved.
Day 7 — Reassemble the loop
- Sit two rounds back to back with no gap, ordering them so the area you chose second comes last, because the map was built from rested, isolated attempts and the loop will reach your weaker area when you are already spent.
- Write where the second round suffered from the first, which is normally the point at which structure collapses into narration.
- Reduce the week to one page holding only the rules you can state without reading them.
Deliverable: Mock notes on cross-round carryover plus a one-page card of rules you can recite from memory.
Frequently asked questions
Is this an official Millennium interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at Millennium. Rounds and questions reflect what candidates have reported, not a process Millennium has published, and they change over time. Confirm the current format and scope with your recruiter.
How difficult is the technical assessment?
The technical bar is high. Expect challenging, real-world problems that require both theoretical knowledge and practical coding skills.
Is knowledge of finance required?
While prior experience in finance is a plus, it is not always a hard requirement for every role. However, showing a strong interest in how your engineering work enables trading strategies will set you apart.
How long does the interview process take?
Timelines can vary based on the team and location, but candidates should prepare for a process that moves efficiently once you pass the initial technical screens.
How many rounds is the Millennium Software Engineer interview process?
Candidates report 4 stages: Technical Screens, Deep-Dive Coding Sessions, System Design Discussions, and Cultural Fit Assessment. The interview process section above breaks down what each stage covers.
Keep practising
- Millennium questions on PracHub — 12 Software Engineer questions in the bank.
- Software Engineer question bank — cross-company practice.
- Interview preparation framework
- All interview guides