Interview guides · First Quantum Minerals
First Quantum Minerals Software Engineer Interview Questions & Guide 2026
First Quantum Minerals software engineer prep: algorithm drills where every complexity claim has to name the assumption under it.
The 60-second brief
As a Software Engineer at First Quantum Minerals, you are at the intersection of heavy industry and cutting-edge digital transformation. You are responsible for developing and maintaining the software solutions that power one of the world’s leading mining companies. Your work directly impacts operational efficiency, safety protocols, and data-driven decision-making across global mining sites.
Treat capacity estimation as a conversion skill rather than a table to memorise: turn a user count and an action rate into requests per second and bytes per day, then name the component that number breaks first. The figure only matters if it changes the design.
Focus areas
- Treat a send timeout as unknown, not failed
- Budget and measure latency at the tail
- Fold execution reports idempotently on venue exec id
Overview
This role is not just about writing code; it is about solving complex engineering challenges in remote and high-stakes environments. You will collaborate with cross-functional teams to integrate software with physical infrastructure, ensuring that First Quantum Minerals' systems are resilient, scalable, and secure. Whether you are optimizing data pipelines or building user-centric applications, your contributions support the company's commitment to sustainable and innovative mining practices.
The First Quantum Minerals interview process
First Quantum Minerals candidates report 3 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 Discussions
Most of the time lost in this format is not lost to thinking. It goes to a standard-library call you half-remember, an off-by-one in a loop bound, and a debugging loop that mutates code at random until something passes. When output is wrong, stop re-reading the whole function: take the smallest input that reproduces it and walk the state through by hand, printing intermediates if the environment allows. Guessing at a fix without a failing case you understand is how a five-minute bug becomes twenty, and the clock does not pause while you do it.
What to demonstrate
- Whether you reach the right structure without a detour, and can write it from memory rather than only recall that one exists
- Whether overflow is considered where the language has fixed-width integers, since a signed 32-bit value stops at 2,147,483,647 and then wraps in Java, is undefined behaviour in C++, and does not arise in Python, whose integers grow instead
- Whether recursion depth is treated as a constraint on large inputs, given that CPython's default limit is 1000 frames and a deep recursion can exhaust the stack in any language where an iterative version would not
- Whether a failing case is isolated and explained before any edit is made to the code
How to prepare
- From an empty file and with no references open, implement the pieces you lean on most: a heap push and pop, an iterative DFS with an explicit stack, and a binary search whose midpoint is written lo + (hi - lo) / 2, which avoids the overflow that (lo + hi) / 2 can hit in a fixed-width integer type
- Time yourself on the ten library calls you look up most, such as sorting with a custom comparator, splitting and joining strings, and finding the next key at or above a value in an ordered map, until the lookup is gone
- Take a solution you know is broken and, before touching it, write one sentence naming the input, the expected value and the actual value. Repeat until you do it without deciding to.
Site Visit
When a round has no standard shape, it is often there because something is still open: an area no earlier conversation reached, a round where the signal came out mixed, or a decision someone is not ready to make alone. Work out which by going back over what each earlier round actually covered rather than how it felt, and arrive able to give evidence on that point without being asked twice. Weak answers replay the loop's earlier material at the same depth. Strong ones go a level deeper and stay consistent with what you already said.
What to demonstrate
- Whether your account of a project matches the one you gave earlier in the loop, since what you said before may be available to whoever runs this round
- Whether you can go a level deeper on something already covered, reaching the decision and its alternatives rather than repeating the summary
- Whether you state your own uncertainty accurately, including parts of a system you did not build and decisions you inherited, instead of claiming even ownership across all of it
- Whether you can answer a question you handled poorly earlier by naming what you missed, rather than delivering a polished second version as if the first had not happened
How to prepare
- Reconstruct the loop on one page: for each round, the questions you were asked and the answer you actually gave, not the better one you thought of afterwards. The gaps on that page are your best available guess at why this round exists.
- Take the two claims you made earlier that carry the most weight and assemble the backing for each: the measurement, the date, what broke, the decision you would make differently now.
- Write down the three facts about your work that must not drift between tellings, such as team size, timeline and your own role, and check your stories against that list rather than trusting recall under pressure
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.
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.
Treating a network call as though it were a local function call
A remote call can be slow, fail, or return after you stopped waiting, so name the timeout, the retry policy, and what the caller sees while the dependency is down. A call with no timeout turns one slow dependency into an exhausted thread or connection pool in every service upstream of it.
Trusting input because it came from your own front end
Anything crossing a trust boundary is hostile: parameterise queries instead of building SQL by concatenation, validate against an allow-list rather than a deny-list, and bound the size of anything you allocate from a request. Raising this unprompted in an API or design question is a cheap and unusually strong signal.
Questions & practice
Find every overlapping effective window before adding the constraint
Hard · interval sweep · skew · null semantics
instrument_version holds about 10^7 rows of (instrument_id, effective_from, effective_to nullable). Versions for one instrument_id must never overlap, and at most one may have effective_to NULL. Adding the EXCLUDE constraint aborts on the first conflicting pair it meets, so you need the whole picture first. Produce every row that overlaps another row for its instrument, per-instrument overlap counts, and every instrument holding more than one open-ended row. The median instrument has about ten versions; the worst has 10^5 after repeated vendor reloads. Explain why the pairwise comparison is correct but cannot ship.
Approach
- Cost the naive version properly rather than dismissing it. Grouped by instrument_id it is the sum of g^2 over groups, and at a median g of ten that is trivial — the blow-up lives entirely in the tail, where one 10^5-version group contributes 5 x 10^9 comparisons. Worse, its output is also quadratic: if that group is a repeated reload, nearly every pair overlaps, so the self-join emits billions of rows describing a few thousand bad versions. The output size, not the runtime, is the reason it cannot ship.
- Restate the deliverable as rows rather than pairs, which is what makes a linear output possible, then sort by (instrument_id, effective_from, effective_to nulls last) for O(n log n) and sweep each group once.
- In the sweep, carry running_max_end = MAX(COALESCE(effective_to, infinity)) over the rows already seen in this group, plus the row that set it. A row overlaps iff its effective_from is strictly less than running_max_end; flag both that row and the max-setter, so both sides of every overlap appear in the output. O(1) extra state per group.
- Do not shortcut to comparing adjacent rows only. It is sufficient to answer the boolean does any overlap exist — if every adjacent pair is disjoint then end_i <= start_(i+1) <= start_j for all j > i, so nothing overlaps — but it misses nested intervals when you must list every offending row, and a long open-ended version nesting later ones is the single most common real shape.
- If you write it in SQL, the window-function form is MAX(COALESCE(effective_to, 'infinity'::timestamptz)) OVER (PARTITION BY instrument_id ORDER BY effective_from ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING). The COALESCE is load-bearing, and in two distinct ways: MAX ignores NULLs, so an earlier still-open version contributes nothing to the running maximum, and where it is the only preceding row the maximum is NULL outright, which makes effective_from < running_max evaluate to NULL rather than true. Either way the comparison is not true and the overlap disappears from the report.
- Handle the open-row rule as its own aggregate in the same pass — COUNT(*) FILTER (WHERE effective_to IS NULL) > 1 per instrument — since a second open row is a violation whether or not your range model makes it overlap, and since it is the only part of the report that survives the COALESCE being dropped. Only then add EXCLUDE USING gist (instrument_id WITH =, tstzrange(effective_from, effective_to) WITH &&), which needs btree_gist for equality on a bigint, and expect to iterate because each failed build names one pair.
Worked solution (35 min)
- Build instrument 7 with r1 = [2026-01-01, NULL), r2 = [2026-03-01, 2026-04-01), r3 = [2026-05-01, 2026-06-01), and instrument 9 with two rows both open from 2026-01-01 and 2026-02-01.
- Run the sweep with COALESCE to infinity and record which rows are flagged for each instrument.
- Run the same sweep with plain MAX(effective_to) and diff the two outputs, evaluating the comparison for r2 and r3 separately.
- Run an adjacent-pairs-only variant and diff again.
- Attempt to add the EXCLUDE constraint on the unfixed fixture and record what it reports.
Expected result: The correct sweep flags all three rows of instrument 7 (r2 and r3 both nest inside r1's open window) and both rows of instrument 9, and lists instrument 9 in the multiple-open-rows output. Plain MAX flags nothing at all for either instrument: over r2's preceding frame the maximum is MAX(NULL) = NULL, so 2026-03-01 < NULL is NULL rather than true; over r3's frame it is MAX(NULL, 2026-04-01) = 2026-04-01, and 2026-05-01 is not less than that; instrument 9's second row compares against MAX(NULL) = NULL too. The only thing the plain-MAX report still surfaces is instrument 9, and only via the separate open-row count. Adjacent-only, with the COALESCE kept, catches r1/r2 but misses r3, which nests inside r1 and does not touch r2. The constraint build fails naming exactly one pair.
Verify it yourself
- On a small fixture, the sweep's flagged-row set equals the set produced by a brute-force O(g^2) pairwise comparison — same rows, regardless of pair count.
- Every row flagged has a named counterpart row that it actually overlaps, checkable by hand from the two ranges.
- After the flagged rows are repaired, the EXCLUDE constraint builds on the first attempt; if it does not, the report was incomplete rather than the data newly broken.
Follow-up
- tstzrange(a, a) is empty and empty ranges never overlap, so a zero-width version passes the constraint. Is that acceptable, and what would catch it?
- The fix requires closing thousands of rows. How do you do that without editing history that a replay depends on?
- How would you run this check continuously, so the next bad vendor load is caught at write time instead of at constraint-build time?
Fold a duplicated execution stream into per-order state
Easy · idempotency · hashing · aggregation
A day's execution reports arrive as one unordered array of tuples (venue_mic, venue_exec_id, order_id, exec_type, last_qty, cum_qty, leaves_qty). The same execution appears up to three times, once from the trading session, once from the drop copy, once from the clearing file. Assume 10^7 rows over 10^6 orders, and ignore trade_correct and trade_cancel for now. Given a map order_id to order_qty, return each order's final cum_qty and leaves_qty, plus the list of orders whose final cum_qty exceeds order_qty. One pass, O(n) expected time; justify your space and say what you would give up to shrink it.
Approach
- Notice what the deliverable needs before reaching for a dedupe set: final cum_qty and leaves_qty are a max-fold over the venue's own cumulative field, and max is idempotent, so a resent report changes nothing. That reduces state to one record per order_id in an open-addressed hash map keyed by a 64-bit id, roughly 24 bytes each, about 24 MB at 10^6 orders.
- Fold under an explicit total order rather than arrival order: keep the report with the greatest cum_qty, breaking ties toward the smaller leaves_qty. The tie-break is what lets a canceled report (same cum_qty, leaves_qty 0) displace the partial fill that preceded it without consulting any clock, which matters because the three sources arrive interleaved.
- Reintroduce deduplication only for the fields that are sums rather than maxima: commission_amt, fee_amt, rebate_amt, and any fill count. Those need a set on (venue_mic, venue_exec_id), and at 10^7 executions with roughly 50-byte keys that is about a gigabyte of state. Hashing the key to 64 bits shrinks it to around 256 MB but accepts a collision probability near n^2/2^65, about 3e-6 per run, where a collision silently drops a real fill.
- Emit the violation list in the same pass: any order whose winning cum_qty exceeds its order_qty. The fold exists to protect that invariant, so the check belongs inside the job rather than in a dashboard that reads its output later.
- State the bound: O(n) expected time, O(orders) space, and the job is I/O bound on reading 10^7 rows rather than bound by the fold itself.
Follow-up
- Now admit trade_correct and trade_cancel, which arrive later and reference an earlier venue_exec_id via corrects_exec_id. What breaks in the max-fold, and what replaces it?
- The clearing file disagrees with the session on cum_qty for 40 orders. Which side do you write into position_snapshot, and what do you do with the difference?
- How would you make this fold restartable partway through a 10^7-row file without recomputing from the beginning?
Parse a tag-value wire message without allocating
Medium · parsing · zero allocation · hot path
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.
Approach
- 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-up
- 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?
Current order state from an append-only event log
Easy · window-functions · indexing · event-sourcing
order_event is append-only: event_id BIGINT PRIMARY KEY, order_id BIGINT, strategy_run_id UUID, seq_no INT (1-based and contiguous per order, UNIQUE (order_id, seq_no)), status_after, cum_qty, leaves_qty, received_ts TIMESTAMPTZ(6). The table holds 10^8 rows across 10^7 orders. Write the query returning current status_after, cum_qty and leaves_qty for every order in a supplied list of order_ids, then the variant returning current state for all orders of one strategy_run_id. State the index each needs, and why ordering by received_ts instead of seq_no is wrong.
Approach
- For the id list use SELECT DISTINCT ON (order_id) ... ORDER BY order_id, seq_no DESC, which takes the first row of each group and stops. The portable equivalent, ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY seq_no DESC) = 1, ranks every row of every group before discarding all but one.
- Index (order_id, seq_no DESC). A btree can be scanned forwards or backwards as a whole, not per key, so the mixed-direction ORDER BY (order_id ASC, seq_no DESC) is not satisfied by (order_id, seq_no) and the planner inserts a sort.
- For the strategy_run_id variant the leading column changes to (strategy_run_id, order_id, seq_no DESC); otherwise the run's orders are found by a scan and grouped afterwards, which is the difference between 10^4 index descents and 10^8 row reads.
- Justify seq_no over received_ts concretely: received_ts is neither unique nor monotonic across reports. Two reports can share a microsecond, and a resend after a reconnect can arrive after a report it precedes, so MAX(received_ts) picks a non-deterministic row and can return an older state than the one the system acted on.
- Name the cost that motivates a maintained order_current table: this query is one index descent per order in scope, fine for a list and unacceptable for a dashboard refreshing every live order in a session. The trade-off is a second write on the order path and a crash window between the append and the update.
Worked solution (20 min)
- Seed one order with six events, giving two of them the same received_ts and inserting the final event with a lower event_id than its predecessor.
- Run the DISTINCT ON query and a MAX(received_ts) variant against that order and diff the two rows.
- EXPLAIN the query under both index shapes, (order_id, seq_no) and (order_id, seq_no DESC), and look for the sort node.
- Repeat for the strategy_run_id variant over a run holding 10^4 orders.
Expected result: One row per order_id carrying the highest-seq_no state. The MAX(received_ts) variant returns a different, older row for the order with the tied timestamp, and the DESC index removes the sort node from the plan.
Verify it yourself
- The result row count equals the number of distinct order_ids requested.
- For every returned row, seq_no equals MAX(seq_no) for that order.
- With the DESC index in place the plan shows no sort node above the index scan.
Follow-up
- How do you assert seq_no is contiguous per order, and what should the system do when it is not?
- Write the query listing orders whose latest row violates cum_qty + leaves_qty = order_qty while the order is live.
- How would you keep order_current correct if the writer can crash between appending the event and updating the summary row?
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
- 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.
- 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.
- 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.
- 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.
- 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?
What is the importance of asynchronous programming?
Medium
Approach
- Say what you would check first and why it is the highest-information step.
- Clarify what is being asked and what a complete answer contains.
- Work from the requirement backwards to the design.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Can you explain the models and frameworks you have utilized in previous projects?
Medium
Approach
- Clarify what is being asked and what a complete answer contains.
- State your assumptions explicitly before working the problem.
- Say what you would check first and why it is the highest-information step.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Signed drop-copy webhooks with at-least-once delivery
Medium · webhooks · idempotency · authentication
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.
Approach
- 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.
Worked solution (30 min)
- Write the verification function signature showing it takes raw bytes plus headers, and the constant-time comparison call.
- Write the handler as ordered steps with the exact point at which 2xx is returned, and label what has been made durable by then.
- Write the upsert on (venue_mic, venue_exec_id) and trace two deliveries of the same report through it.
- Trace a trade_correct arriving before its original and write the pending-resolution rule.
- List the status codes you return and what each instructs the sender to do.
Expected result: 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.
Verify it 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-up
- 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?
End-of-day reconciliation slowed from four to seventy minutes
Easy · n+1 query · as-of join · indexing
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.
Approach
- 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-up
- 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?
Suggested practice checklist
Day one measures instead of guessing, under a fixed rubric, and the remaining hours are allocated in proportion to the gaps before any studying begins. The allocation is deliberately not renegotiated midweek, because the area that feels worst on day three is usually the one that is moving.
Day 1 — Diagnostic, scored before you study anything
- Sit a 110-minute diagnostic in four blocks: forty-five minutes on two coding problems, twenty-five on one design prompt taken to interface and data model, twenty of short-answer fundamentals, and twenty delivering two behavioural answers aloud.
- Score each block from 0 to 3 on a fixed rubric where 3 is correct and fluent, 2 is correct but slow or prompted, 1 is partially correct and 0 is stuck, grading the artifact rather than how the attempt felt.
- Allocate days two to five in proportion to 3 minus each block's score, write the allocation down, and commit to leaving it alone.
Deliverable: A scored rubric and a fixed hour allocation for the rest of the week.
Day 2 — Largest gap: find the boundary rather than the subject
- Split the weakest area into named sub-skills and rate each separately. For coding those are restating the problem, choosing the structure, stating the invariant, turning the invariant into loop bounds, handling empty and single-element input, and accounting for complexity out loud.
- Attempt three items positioned just above where the rating drops off, and for each write the first move you failed to make.
- Re-attempt one of them from blank four hours later with nothing open.
Deliverable: A sub-skill map with the two blocking sub-skills circled.
Day 3 — Drill the blocking sub-skill by repeating the shape
- Do eight short repetitions of the same shape rather than eight different problems, so what gets practised is the pattern and not the puzzle.
- State the rule you now hold in one sentence, then test it against a case built to break it, a sliding window over an array containing negative values, or a cache-aside read path whose invalidation message is dropped.
- Have someone else read your one-sentence rule and find the precondition you left out.
Deliverable: One rule statement with its preconditions attached and one counterexample that would have caught the incomplete version.
Day 4 — Second gap, plus maintenance on the strongest area
- Run the same sub-skill decomposition on the second-largest gap in half the time.
- Spend twenty-five timed minutes on the block you scored highest, choosing the hardest item you can still finish rather than a warm-up.
- Write whether each area fails you on recall, on setup, or on execution, and set the fix accordingly: repetition for recall, a written checklist for setup, timed work for execution.
Deliverable: A second sub-skill map plus a one-line failure diagnosis for each area.
Day 5 — The gap that is not a skill
- Record one technical and one behavioural answer, then count two things in the playback: seconds before your first clarifying question, and sentences you began without knowing where they would end.
- Practise saying that you do not know, followed by how you would find out, without letting it soften into a guess, and practise stating a complexity or an estimate before being asked for it.
- Redeliver one answer under a hard ninety-second cap, which forces structure ahead of detail.
Deliverable: Two recordings with a counted reduction in time-to-first-question.
Day 6 — Retest under day-one conditions
- Sit the same 110-minute structure with new prompts of comparable difficulty and score it on the identical rubric.
- For any block that did not move, change the method rather than adding hours: a block stuck at 1 usually means the practice was too varied, not too short.
- Write down which single block you would still lose the offer on.
Deliverable: A second scored rubric placed beside the first, with one named remaining risk.
Day 7 — Full loop under interview conditions
- Run a sixty-minute mock over the two blocks that moved least, with an interviewer briefed to interrupt and change direction mid-answer.
- Write the recovery script for going blank: restate the question, state your assumption, name the first thing you would check.
- Say every rule from the week aloud without reading it, and cut any you cannot state in a single sentence, since a rule you have to reconstruct mid-answer will not survive an interruption.
Deliverable: A one-page card holding the recovery script and only the rules you could state from memory.
Disagreement needs an outcome you did not choose
Conflict answers where you were right and everyone came round are the weakest ones. Stronger: the evidence you went and collected, what would have changed your mind, and what you did in the weeks after the call went against you. Implementing a design you argued against, properly, is a specific and checkable behaviour.
Have you had experience with Root Cause Analysis (RCA)?
Medium · behavioural and engineering judgement
Approach
- Give the blast radius: what could have broken, and what you measured.
- Close with what you would do differently, concretely.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
Can you provide an example of how you have dealt with workplace conflicts and reached a solution?
Medium · behavioural and engineering judgement
Approach
- Pick a story where you made the decision, not one where you watched it.
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
How do you handle system architecture challenges in your development work?
Medium · behavioural and engineering judgement
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Give the blast radius: what could have broken, and what you measured.
- Close with what you would do differently, concretely.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
How do you demonstrate a proactive attitude toward safety in your daily work?
Medium · behavioural and engineering judgement
Approach
- Give the blast radius: what could have broken, and what you measured.
- Pick a story where you made the decision, not one where you watched it.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?