Interview guides · A global energy trading organisation
A global energy trading organisation Software Engineer Interview Questions & Guide 2026
Concurrency drills for A global energy trading organisation software engineer prep: races, lock ordering, and what a check-then-act sequence assumes.
The 60-second brief
As a Software Engineer at A global energy trading organisation, you sit at the intersection of high-stakes financial markets and complex technical infrastructure. Your work is fundamental to ensuring that energy flows efficiently across global networks, requiring systems that are not only performant but also highly reliable and secure. In this role, you will build and maintain the platforms that power real-time trading, data analysis, and operational logistics.
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
- Budget and measure latency at the tail
- Fold execution reports idempotently on venue exec id
- Detect sequence gaps before pricing off a book
Overview
You will contribute to a fast-paced environment where software precision directly impacts business outcomes. Whether you are optimizing database queries for high-frequency trading data or developing robust backend services using modern frameworks, your contributions will scale to meet the demands of the global energy landscape. It is a position that requires both technical rigor and the ability to understand the broader economic challenges facing the energy sector today.
The organization values candidates who demonstrate a balance between technical proficiency and a genuine curiosity about the global energy market.
The A global energy trading organisation interview process
A global energy trading organisation candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Initial Introductions
You cannot drill a format you do not know, so put the preparation into material that travels. Three pieces of your own work, each rehearsed until you can take a follow-up you did not anticipate, will carry a conversation or a code walkthrough equally well. Specificity is what separates that from filler. A number needs its definition before it means anything: a p99 is over some window and measured at some hop, and a server-side figure excludes the queueing and network time a client would see. The number you cannot qualify is the one to leave out.
What to demonstrate
- Whether your examples carry detail only someone who did the work would hold, such as what the binding constraint actually was, which alternative you rejected and why it was worse, and what you measured on each side of the change
- Whether a number survives one follow-up, meaning you can say what it was measured over and whether it moved because of your change or merely alongside it
- Whether a failure is described with the specific change that followed it, rather than a lesson stated in general terms
- Whether your part in a team effort is stated accurately, including what other people did
How to prepare
- Write a page on each of three projects covering the constraint, the option you rejected, the measurement before and after, and what went wrong. Cut any line you cannot take a follow-up on, since you are writing the parts you will be pressed on rather than a summary.
- Recover the real figures while you still have access: request volume, data size, latency with its percentile and window, team size, timeline. Note where each came from, whether a dashboard, a design document or memory, and mark the estimates so you can say which they are out loud.
- Take your weakest project story to someone who works in a different area and have them ask why four times in succession. The point where you run out of answer is the part to go and re-read before the round.
Technical Assessments
Most of the time lost in this format is not lost to thinking. It goes to a standard-library call you half-remember, an off-by-one in a loop bound, and a debugging loop that mutates code at random until something passes. When output is wrong, stop re-reading the whole function: take the smallest input that reproduces it and walk the state through by hand, printing intermediates if the environment allows. Guessing at a fix without a failing case you understand is how a five-minute bug becomes twenty, and the clock does not pause while you do it.
What to demonstrate
- Whether you reach the right structure without a detour, and can write it from memory rather than only recall that one exists
- Whether overflow is considered where the language has fixed-width integers, since a signed 32-bit value stops at 2,147,483,647 and then wraps in Java, is undefined behaviour in C++, and does not arise in Python, whose integers grow instead
- Whether recursion depth is treated as a constraint on large inputs, given that CPython's default limit is 1000 frames and a deep recursion can exhaust the stack in any language where an iterative version would not
- Whether a failing case is isolated and explained before any edit is made to the code
How to prepare
- From an empty file and with no references open, implement the pieces you lean on most: a heap push and pop, an iterative DFS with an explicit stack, and a binary search whose midpoint is written lo + (hi - lo) / 2, which avoids the overflow that (lo + hi) / 2 can hit in a fixed-width integer type
- Time yourself on the ten library calls you look up most, such as sorting with a custom comparator, splitting and joining strings, and finding the next key at or above a value in an ordered map, until the lookup is gone
- Take a solution you know is broken and, before touching it, write one sentence naming the input, the expected value and the actual value. Repeat until you do it without deciding to.
Behavioral Fit Assessment
Your first answer is not really what is scored. It buys the follow-up questions, and those decide the round. An interviewer with fifteen minutes takes one thread and pushes on it four or five times, so a story you can only tell at a single level of detail collapses under the third why. That is an argument for fewer stories known deeply rather than one prepared per prompt. Four or five pieces of work you can still explain down to the code you changed and the argument you had about it will cover nearly anything asked in this round.
What to demonstrate
- Whether a story holds as the questioning moves from what you did to why that instead of the alternative, and then to what you would change knowing what you know now
- Whether you can re-cut a project to answer the question actually asked rather than delivering a rehearsed block that answers an adjacent one
- Whether your level of detail is chosen rather than habitual: going down to the schema when the question is about the data model, staying out of it when the question is about the person who disagreed with you
How to prepare
- Pick four projects and write the chain out four levels deep for each: what you did, why that, why not the alternative, and what would have to be true for the alternative to have won. Where you cannot reach the fourth level, you have a placeholder rather than a story
- Have someone ask why three times in a row on a single thread with nothing else added, and mark the point where you start repeating a sentence you already said. That point is where the interviewer stops learning anything
- Build a one-page index instead of an answer bank: the common prompts in this round (disagreement, a failure that was yours, thin requirements, a deadline you missed, work you inherited) mapped to which of your four projects you would use for each, so the choosing is done now rather than while an interviewer waits
Remote and Onsite Interactions
Where the day includes a partner from product, design or data, that conversation is weighted like the technical ones and prepared for least. They are deciding one thing: whether having you in the room makes their decisions cheaper. That means options with costs attached, not implementation detail and not "it depends". An estimate someone can plan against — a range, the assumption that would push it to the high end, and what you would drop to hit the low one — is worth more than a confident single number, which everyone present already knows is wrong.
What to demonstrate
- Whether an estimate comes as a range with the assumption most likely to break it, and states what a specific scope cut would actually buy
- Whether a technical constraint is handed over as a choice with consequences on their side, rather than as a verdict they have no standing to argue with
- Whether you establish what decision is on the table before proposing anything
- Whether risk is raised while it can still change the plan, with the trigger that would confirm it, instead of reported afterwards as a slip
How to prepare
- Take a project that shipped late and write the two-sentence warning you could have given three weeks earlier, naming what you would have needed decided at that point
- Rehearse one estimate out loud until it arrives in three parts: the range, the single assumption that would blow it, and the smallest thing you would cut to protect the date
- Rewrite an objection you have actually made — the "we can't do that" version — as two options with their costs, so the choice ends up with the person who owns it
Where candidates lose points
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.
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.
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.
Starting work without saying what you are about to spend time on
State the plan before executing it: the approach, roughly how long it will take, and what you intend to leave hand-waved. That gives the interviewer a chance to redirect you in ten seconds rather than watching you spend fifteen minutes on the wrong sub-problem.
Questions & practice
Publish top instruments by traded notional every second
Medium · top-k · heap · integer overflow · streaming
Fills stream in as (instrument_id, last_qty, last_px, contract_multiplier), with last_px an integer at the instrument's price_scale. Roughly 10^7 fills a day across 10^5 instruments. Publish the fifty instruments with the highest cumulative traded notional for the session, refreshed once a second, using exact integer arithmetic. Per-fill work must be O(1), and the per-second emit must not rescan all 10^5 instruments in the common case. Trade cancels and corrections can reduce an instrument's total. Give the accumulator width you need, and the argument that your pruning cannot drop a qualifying instrument.
Approach
- Size the accumulator before choosing the algorithm. A single fill of 10^6 lots at a price of 100 carried at price_scale 9 is 10^6 x 10^11 = 10^17 in scaled units, so an int64 session total overflows after about 92 such fills (9.22e18 / 1e17). Accumulate in int128, or rescale to a coarser notional unit at ingest and document the rounding; a double is out on the usual grounds, since it is exact only to 2^53.
- Per fill: one open-addressed hash map lookup on instrument_id, one 128-bit add, and an insert into a dirty set — O(1) expected, no allocation, no ordering work at all between emits.
- Per emit: maintain tau, the current fiftieth-largest total. Candidates are the previous top fifty plus the dirty instruments whose total exceeds tau. Select with a bounded min-heap of size 50, which is O(c log 50) for c candidates, and c is a few thousand per second rather than 10^5.
- State the precondition that makes the pruning sound: while totals only increase, an untouched instrument below tau cannot have crossed it, because nothing changed its value. Cancels and corrections break exactly that precondition, so a corrected instrument must enter the dirty set and, if it falls out of the top fifty, the replacement may be an instrument the pruning skipped.
- Close that hole rather than ignoring it: keep a reserve of the top 200 and refill from it when a member drops, falling back to a full O(m) quickselect over all 10^5 totals when the reserve is exhausted. The fallback runs rarely, is bounded, and is the reason the output is correct rather than usually correct.
- Report both costs: amortised O(1) per fill, O(c log k) per emit with an O(m) worst case, and note that the emit runs on its own thread off a snapshot so it never sits in the fill path.
Worked solution (30 min)
- Compute the overflow point by hand for the worst realistic fill (qty 10^6, px 100 at price_scale 9) and confirm the count at which an int64 total wraps.
- Build a fixture of 20 instruments with k = 3, feed fills so totals are 100, 90, 80, 70, ... and record tau after the first emit.
- Send a fill of size 5 to the instrument at 70, taking its running total to 75, and confirm it is pruned without entering the heap because 75 is still below tau; then send a further fill of size 20 to that same instrument, taking it to 95, and confirm it now enters the heap and displaces the instrument at 80.
- Send a trade cancel that removes 30 from the instrument at 100 and confirm the reserve supplies the replacement rather than the pruned set being consulted.
- Drain the reserve deliberately with a run of cancels and confirm the quickselect fallback fires and returns the same answer as a brute-force sort.
Expected result: An int64 total wraps after about 92 worst-case fills, so the accumulator is int128. With k = 3 the first emit gives {100, 90, 80} and tau = 80. The +5 fill carries that instrument from 70 to 75, still below tau, so the top three are unchanged; the further +20 fill carries the same instrument to 95, which enters the heap, displaces 80, and yields {100, 95, 90} with tau now 90. The cancel then takes the leader from 100 to 70, and the top three become {95, 90, 80} sourced from the reserve, identical to a full re-sort.
Verify it yourself
- For a random 10^6-fill fixture, the incremental top-k matches a full sort of all totals at every emit, byte for byte.
- The per-emit candidate count stays far below 10^5 in the steady state, and every fallback to quickselect is counted and explainable by a cancel or correction.
- Replacing int128 with int64 on the worst-case fixture produces a negative total, which is the observable symptom of the overflow you sized for.
Follow-up
- Make it a rolling five-minute window rather than a session total. What state does each instrument now need, and what does that do to memory at 10^5 instruments?
- Two instruments tie exactly at the fiftieth place. What is your tie-break, and why does an unstable one make the output non-reproducible in replay?
- The totals are in mixed currencies. Where does the FX conversion go, and what happens to the ranking when a rate updates mid-second?
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?
Choose a book structure for constant-time top-of-book reads
Medium · data structure choice · cache locality · order book
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.
Approach
- 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.
Follow-up
- 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?
A desk report that silently multiplies filled quantity
Medium · join-fanout · aggregation · query-rewrite
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.
Approach
- 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.
Worked solution (30 min)
- Reproduce on a fixture: one order with five lifecycle rows and two fills of 100 each, and one limit scope carrying three versions.
- Run the naive join and confirm filled quantity comes back as 200 * 5 * 3 = 3000.
- Rewrite with a pre-aggregated execution subquery and a LATERAL limit lookup, and confirm 200.
- Add a second order to the same run and confirm the run total is the sum of the two orders, not a multiple of it.
- Add a sixth lifecycle row to the first order and confirm no total moves.
Expected result: Filled quantity per strategy_run_id equal to the sum of last_qty over surviving executions for that run, a limit column showing the version whose effective window contains the order's sent_ts, and a result whose row count equals the number of distinct strategy_run_ids in scope.
Verify it yourself
- Result row count equals COUNT(DISTINCT strategy_run_id) over the same filter.
- Filled quantity matches a standalone SUM(last_qty) over execution_report for the same run.
- Changing the number of ack rows on an order changes no total in the report.
Follow-up
- 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?
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 will be the output of the provided Python program?
Medium
Approach
- State your assumptions explicitly before working the problem.
- Work from the requirement backwards to the design.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
How do technical challenges in the energy sector influence software architecture decisions?
Medium
Approach
- State your assumptions explicitly before working the problem.
- Work from the requirement backwards to the design.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Serve as-of instrument definitions to every service on the path
Easy · read-heavy · caching · invalidation · as-of queries
instrument_version is effective-dated (SCD2), keyed by instrument_id plus effective_from and effective_to, carrying venue_mic, venue_symbol, tick_size, lot_size, price_scale, contract_multiplier, trading_status and expiry_date. Every service resolves through it: the order path reads price_scale and tick_size on every order at 10,000 orders/second per process, and the replay engine must resolve a symbol exactly as the live session did months earlier. Writes are a nightly corporate-action load plus intraday vendor corrections. Design the read path: cache shape and key, how a correction reaches processes already holding a snapshot, and what invalidation rule you enforce.
Approach
- Separate the two read patterns instead of building one lookup API. The order path needs an as-of-now read in nanoseconds with no network hop. Replay needs an as-of-T read, resolving the version whose [effective_from, effective_to) window contains T, and can afford one lookup per instrument at run start. Both are materialised from the same rows by one loader.
- Shape the hot-path cache as a process-local array indexed by a dense internal instrument index, resolved from symbol once at subscription time. At 10,000 to 1,000,000 instruments and roughly 100 bytes per definition that is 1 MB to 100 MB, which fits in RAM; a direct array index costs single-digit nanoseconds where a hash lookup on a symbol string costs tens and touches more cache lines.
- Invalidate by version rather than TTL. The loader builds a complete immutable snapshot with a monotonic snapshot_id and the reader swaps a pointer to it, so no reader ever observes a half-applied corporate action. This falls out of the storage model anyway: SCD2 rows are closed and superseded, never edited, so a correction is always a new snapshot.
- Pin replay to the snapshot the live session used by recording snapshot_id on strategy_run. A vendor correcting yesterday's tick size today will otherwise change a replay's output with no code change, and the divergence gets attributed to whatever was edited most recently.
- State the trade-off: preloading the full active set costs memory and a slow process start, and a lazy per-symbol fetch costs a network round trip inside the order path. Take the memory, and size it rather than assuming it.
Worked solution (20 min)
- Load 200,000 instrument_version rows covering 50,000 instruments with several versions each, add the non-overlap constraint (in PostgreSQL, EXCLUDE USING gist (instrument_id WITH =, tstzrange(effective_from, effective_to) WITH &&) with btree_gist installed), then try to insert an overlapping version.
- Time three resolutions over the same data: the as-of SQL query with a range predicate, a process-local hash lookup keyed by the symbol string, and a direct array index by dense instrument index.
- Swap the snapshot pointer while a reader loop is running and measure the reader's pause and the version stamped on each read.
- Resolve one symbol that was reassigned mid-year, once as of March and once as of today.
Expected result: The overlapping insert is rejected by the constraint rather than by review. The SQL as-of query costs hundreds of microseconds to milliseconds including the round trip, the string hash lookup costs tens of nanoseconds, and the array index costs a few nanoseconds, which is the only one of the three inside a per-order budget. The pointer swap produces no measurable reader pause and every read carries exactly one snapshot_id. The reassigned symbol resolves to two different instrument_ids on the two dates, which is the concrete reason symbol is never a join key.
Verify it yourself
- No two versions of one instrument_id overlap, enforced by the database rather than by the loader.
- Exactly one version per instrument_id has effective_to IS NULL at any instant.
- Replaying one past session twice, a week apart, resolves the same instrument_id for every symbol in it.
Follow-up
- A vendor corrects a tick size for a date three months back. Which research runs are now unreproducible, and how do you enumerate them?
- An instrument is halted at 10:00:00. How many milliseconds until every gateway refuses new orders in it, and what happens to the orders already in flight?
- Where does the price_scale integer convention break down, and what would you do for an instrument quoted in fractions?
Order gateway heap grows only on reconnect days
Medium · memory leak · heap profiling · order lifecycle
The order gateway's resident set grows about 400 MB a day, masked by a nightly restart. On a day with three session drops it grew 6 GB in an hour and was OOM-killed mid-session. Order state is indexed two ways: a map keyed by order_id and a map keyed by client_order_id, and client_order_id changes on every replace. Give an ordered checklist that separates a real leak from resident-set growth, attributes it to an allocation site, and names the fix and the regression test.
Approach
- Separate resident set from live heap before anything else. Memory freed to the allocator is not necessarily returned to the operating system, and transparent huge pages and per-thread arenas inflate resident set without live bytes. Compare resident set against the allocator's own accounting over the same window; if live bytes are flat, this is a retention or fragmentation question and a different investigation.
- Take two heap profiles an hour apart under representative load and diff them by allocation site rather than reading current totals. A profiler sorted by total size points at whatever is largest, which is rarely what is growing.
- Correlate growth with traffic shape rather than with elapsed time. Plot live bytes against submits, replaces and inbound execution reports. Growth proportional to replaces implicates the per-message index; growth proportional to reconnects implicates the resend path.
- Audit lifetime against the order state machine. Terminal state is a property of order_id, not of client_order_id: an eviction that fires once when an order reaches filled, canceled, rejected or expired removes exactly one client_order_id and orphans every earlier one the order used. Entries also survive indefinitely when a send times out and no terminal event ever arrives.
- Fix by bounding the per-message index's lifetime rather than its size. Keep it as a lookup from client_order_id to order_id, evict every id attached to an order when that order goes terminal, and give an unresolved send an explicit expiry that resolves through an order status request or the drop-copy stream instead of lingering.
- Add the test that would have caught it: replay a recorded session containing replaces and a mid-session disconnect, and assert both index sizes return to their pre-session values.
Follow-up
- A send times out and no terminal event ever arrives for that order. How long do you hold the entry, and what resolves it?
- Someone proposes an LRU on the client_order_id map. What does that turn the leak into?
- The heap profiler costs around 3% on the order path. Where and when do you run it?
Suggested practice checklist
For a candidate senior enough that the loop turns on design and judgement rather than on whether the coding round gets finished. Five days build one system properly and then stress it; coding gets a single maintenance day, on the assumption that the risk at this level is an unexamined tradeoff rather than a missed algorithm.
Day 1 — Numbers before diagrams
- Build your own reference card of the figures you will re-derive all week: bytes for a realistic record, requests per second implied by a given daily active count, and the storage that a year at a given write rate produces. Derive each one rather than copying it, because the derivation is what survives a follow-up.
- Turn one product statement into capacity requirements. From ten million daily users at four writes and forty reads each, state the peak-to-average factor you are assuming and why, then produce peak write QPS, peak read QPS and a year of storage.
- Write the two numbers whose order of magnitude changes the design, the read-to-write ratio and the working-set size against memory per node, and state the threshold at which each one flips your answer.
Deliverable: A one-page numbers card and one worked capacity estimate with every assumption written down.
Day 2 — One system, from requirements to schema
- Spend the first ten minutes producing only functional requirements, non-functional targets with numbers attached, a p99 latency, a durability expectation, a consistency requirement, and an explicit out-of-scope list.
- Define the interface before the boxes: the three or four endpoints, their parameters, what each returns, and which of them are idempotent.
- Write the data model, then write the single access pattern that justifies it, and state what the schema would have to become if the dominant access pattern were the other one.
Deliverable: One design carried to endpoint-and-schema depth, with non-functional targets expressed as numbers and a written out-of-scope list.
Day 3 — The consistency you are actually buying
- Write out what a client sees under asynchronous replication when its write commits on the leader and its next read is served by a lagging follower, then write the two fixes, pinning that session's reads to the leader for a bounded window or carrying a version token the replica must reach, and the cost of each.
- Work the quorum arithmetic on paper for N of three with W and R of two, and separate what R + W > N does guarantee, that any read set intersects any write set, from what it does not: on its own it is not linearizability, and a sloppy quorum that accepts writes on nodes outside the preference list breaks even the intersection.
- Take two storage choices with different defaults, a single-leader relational store committing synchronously and a quorum-replicated store that converges eventually, and write the specific product behaviour that would be wrong under each, rather than a general statement about which is stronger.
Deliverable: A page separating what quorum overlap guarantees from what it does not, with one concrete product misbehaviour attached to each gap.
Day 4 — Failure is the design
- For one write path, work through the case where the client times out after the server has already committed, then design the idempotency key: who generates it, how long it is retained, and what the duplicate request returns.
- Express the retry policy as parameters rather than as a word: maximum attempts, base delay, backoff factor, jitter, and which error classes are retried at all. Then state why retrying a non-idempotent write without a key is a correctness bug and not merely waste.
- Compute the fan-out effect on tail latency. If a request waits on ten backends and each independently exceeds its p99 one percent of the time, the chance at least one is slow is 1 - 0.99^10, about ten percent. Then write why independence is the optimistic assumption and what correlates them in practice.
- Name the backpressure mechanism for one queue or one dependency in the design, a bounded queue with shedding or a concurrency limit, and write what the caller is told when it engages.
Deliverable: One write path with an idempotency design, a parameterised retry policy, and a written tail-latency calculation with its assumption named.
Day 5 — Scaling the hot path
- Choose cache-aside or write-through for one read path and write the staleness window each produces, then name the invalidation event and what the system does when that event is lost.
- Design against the stampede: either coalesce requests so only one recomputes a missing key, or refresh early with jittered expiry, and write why identical TTLs on keys populated in the same moment produce a synchronised expiry and a thundering herd.
- Shard one table by a key you choose, then answer the two questions that break the choice: which queries now require a scatter-gather, and what happens to the distribution when one tenant is a hundred times larger than the median.
- Write the cost of adding a node under plain modulo placement, where nearly every key moves, against consistent hashing, where roughly one key in n+1 moves, and state what virtual nodes are for.
Deliverable: A caching and sharding decision for one path, each with its failure mode and its rebalancing cost written beside it.
Day 6 — Keep the coding hand in, at the bar that applies to you
- Solve one medium problem in thirty minutes, then spend twenty more making it production-shaped: named invariants, validation at the boundary, and errors that distinguish a caller mistake from an internal fault.
- Write the tests you would require of a colleague's version of that function: one for empty input, one for the boundary, and one for the case the implementation is most likely to get wrong.
- Read a piece of your own code from six months ago and write the change you would ask for, phrased as you would actually phrase it in review.
Deliverable: One problem hardened to review standard, with its test list and one written review comment.
Day 7 — Defend it while being interrupted
- Run a forty-five-minute design mock with an interviewer briefed to change a requirement halfway, a tenfold traffic increase or a new strict consistency requirement, and to push on one number you estimated.
- Rehearse the two sentences a senior loop is listening for: naming the tradeoff you are choosing against and why, and saying what you would measure to learn that the choice was wrong.
- Prepare the design you regret: a real decision, the constraint that produced it, what it cost, and what you changed afterwards.
Deliverable: Mock notes recording how the design changed under the new requirement, plus a written account of one regretted decision.
'We rewrote it in a better framework' is not a story
A migration is a cost you chose to pay, not an achievement. The story is what the old system made expensive, what you measured before committing, what kept serving traffic during the cutover, and what you would have done if the numbers had come back flat. Without those, a rewrite reads as taste.
Describe a time you worked through a challenging technical requirement.
Medium · behavioural and engineering judgement
Approach
- Name the disagreement and how you resolved it with evidence.
- Pick a story where you made the decision, not one where you watched it.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- How did you know your change caused the improvement?
- What did you decide not to do, and why?
How do you handle collaborative projects within a team?
Medium · behavioural and engineering judgement
Approach
- Give the blast radius: what could have broken, and what you measured.
- Name the disagreement and how you resolved it with evidence.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- What did you decide not to do, and why?
- What would you do differently if you ran that again?
Can you explain your experience with Spring Boot and Java?
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
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
Tell us about yourself and your professional journey.
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- Name the disagreement and how you resolved it with evidence.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- How did you know your change caused the improvement?
- What did you decide not to do, and why?