cswork / libraryOpen practice workspace

Interview guides · Alpha Financial Markets Consulting

Alpha Financial Markets Consulting Software Engineer Interview Questions & Guide 2026

Alpha Financial Markets Consulting software engineer prep on partial failure: a timed-out request has an unknown outcome, not a failed one.

The 60-second brief

As a Software Engineer at Alpha Financial Markets Consulting, you occupy a unique position at the intersection of high-level financial strategy and rigorous technical implementation. Unlike traditional software roles, this position demands a deep understanding of the asset management industry, where your code directly influences how clients solve complex operational and investment challenges. You are not just building software; you are architecting solutions that facilitate the digital transformation of global financial institutions.

Ask how many rounds there are and what each one is before you plan your weeks, because the composition is what your hours should follow. A loop with three coding rounds and one short design conversation deserves a different split from the reverse, and whoever schedules it will usually say plainly which it is.

Focus areas

  • Replay recorded input through the same build
  • Treat a send timeout as unknown, not failed
  • Detect sequence gaps before pricing off a book

Overview

This role is critical to the firm's mission, as you will contribute to projects ranging from data engineering pipelines to full-stack application development. You will often work in a consultative capacity, meaning your ability to translate technical concepts for non-technical stakeholders is just as important as your proficiency in languages like SQL or platforms like Azure and Snowflake. Expect to be challenged by the scale of the data and the precision required in the financial sector.

The Alpha Financial Markets Consulting interview process

Alpha Financial Markets Consulting candidates report 5 rounds · ≈ 4-6 weeks. The stages below are what candidates describe, not a published process.

Recruiter Screening

The person on this call usually cannot evaluate your code and does not need to. They write a short paragraph, and that paragraph is what a hiring manager skims when deciding who to put on your loop. So the test is not whether your work was hard, it is whether a non-engineer can repeat it correctly. Name systems by what they did rather than by their internal codename, give each project a shape (what was breaking, what you changed, what happened after), and keep the whole walkthrough near ninety seconds. Depth that cannot survive a paraphrase reads as vagueness.

What to demonstrate

  • Whether a non-engineer can restate your projects without distorting them, since their paraphrase is what travels to the hiring manager, not your sentences
  • Whether each project has a shape rather than a stack list: the failure or constraint, the change you made, the result and how it was measured
  • Whether you can say what was yours inside a team project without either inflating it or disappearing into the plural

How to prepare

  • Rewrite each headline project as two sentences with no internal system names and no acronyms outside your company, then say them to someone outside engineering and have them repeat them back. Fix whatever came back wrong
  • Attach one measured number to each project: the baseline, the change, and the window it was measured over. Where nothing was ever measured, say that plainly rather than reaching for a plausible percentage
  • Time the background walkthrough against a clock. If it runs past two minutes, compress the earliest role to a single clause and spend the recovered time on the most recent one

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.

Technical Evaluations

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 to demonstrate

  • 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

Behavioral Interviews

Many of these questions are about something that went wrong, and the grading sits mostly in the hours after you knew. Who found out first, whether that was you or an alert or a user, how long it took you to say it out loud, and whether the people who needed the news got it while they could still act on it. Engineers under-tell this part because it feels like confessing. The pattern it is looking for is the opposite: the quiet fix, an incident absorbed without telling anyone, after which nothing changed and the same failure is still available.

What to demonstrate

  • How the problem was found, and whether that route was one you had built or one that happened to you, since a user reporting it first means your instrumentation did not cover that failure
  • Whether time-to-detect and time-to-tell are separate numbers in your account and whether you know both, because a fast fix that nobody heard about until the retro is a different answer from a slow one that was announced immediately
  • Whether the resolution left something durable behind, a check that fires or a default that changed, rather than depending on people remembering to be careful
  • Whether you can say what the failure cost without either inflating it or waving it away

How to prepare

  • Reconstruct one incident you were part of as a timeline with clock times: first bad request, first signal, first person who knew, first message outside the team, mitigation, permanent fix. The gaps between those entries are what gets asked about
  • Look up the configuration of the signal that caught it, including its evaluation window and threshold. An alert defined on a five-minute aggregate cannot fire until the condition holds across that window, which puts a floor under time-to-detect that has nothing to do with how severe the failure was. Be able to say what that floor was and whether anyone had chosen it deliberately
  • Prepare one story where you escalated early and the severity turned out to be smaller than you thought, including what it cost the people you pulled in. Without it, every answer you give about raising alarms is unfalsifiable

Assessment Center

The same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.

What to demonstrate

  • In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
  • In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
  • Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes

How to prepare

  • Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
  • Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
  • For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first

Where candidates lose points

Comparing timestamps from two different venues, or subtracting a venue timestamp from a local one and calling the result latency.

Each venue stamps on its own clock, at its own point in its own pipeline, at its own resolution, so a difference between two of them mixes elapsed time with clock offset and with unstated stamping conventions. Subtracting a venue timestamp from a local receipt timestamp yields network delay plus the offset between two clocks, and unless both are disciplined to a common source the offset can exceed the quantity being measured. Use one local clock for any latency you intend to act on, discipline it properly - PTP with hardware timestamping reaches sub-microsecond, while plain NTP over a LAN is typically sub-millisecond, three orders of magnitude too coarse here - and keep venue and receipt timestamps in separate columns so nobody is tempted to conflate them.

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.

Listing technologies instead of trade-offs

Name the property the design needs first, such as ordered range scans, multi-entity transactions, cheap appends, or a predictable p99, then pick something that provides it and say what it gives up in exchange. Almost any component is defensible once you state the requirement it satisfies and the one it sacrifices.

Never running a concrete value through the code

Trace one small input and one edge input by hand, index by index, out loud. Re-reading your own code catches design mistakes; walking a real value through it catches the off-by-one, the uninitialised accumulator and the loop that never advances.

Questions & practice

What experience do you have with the different software we use?

Medium · data structures and algorithms

Approach

  1. Restate the input: its shape, its size, and what is guaranteed about it.
  2. State the target complexity and say which constraint rules the naive version out.
  3. Name the brute-force solution and its complexity before improving on it.

Follow-up

  • How does this change if the input no longer fits in memory?
  • Which test case would catch an off-by-one here?

What steps would you take if a machine is running slowly?

Medium · data structures and algorithms

Approach

  1. Choose the data structure from the access pattern, not from familiarity.
  2. Restate the input: its shape, its size, and what is guaranteed about it.
  3. Walk one small example through your approach before writing the whole thing.

Follow-up

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

What is your worst defect as a developer?

Medium · data structures and algorithms

Approach

  1. Restate the input: its shape, its size, and what is guaranteed about it.
  2. Walk one small example through your approach before writing the whole thing.
  3. State the target complexity and say which constraint rules the naive version out.

Follow-up

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

Find 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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)

  1. 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.
  2. Run the sweep with COALESCE to infinity and record which rows are flagged for each instrument.
  3. Run the same sweep with plain MAX(effective_to) and diff the two outputs, evaluating the comparison for r2 and r3 separately.
  4. Run an adjacent-pairs-only variant and diff again.
  5. 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?

Check-then-act on a position limit under concurrent submits

Hard · isolation-levels · write-skew · concurrency · pre-trade-risk

A pre-trade check reads the current net position for (account_id, instrument_id) from position_snapshot where snapshot_kind = 'intraday_derived', compares it against risk_limit.limit_value for limit_type = 'max_position_qty', and if the order fits, inserts an order_event row and updates that same snapshot row. Two orders for the same account and instrument are submitted concurrently under READ COMMITTED. Name the anomaly precisely and say what separates it from write skew, say whether PostgreSQL's REPEATABLE READ prevents it and under exactly what precondition, and give three implementations that hold the cap, with the cost of each and what happens when the snapshot row does not yet exist.

Approach

  1. Pin the interleaving before naming anything. Both transactions read the same position_snapshot row for (account_id, instrument_id): net_qty = 900 against a cap of 1000, both find 900 + 80 inside the cap, both proceed, and both then write that same row. That makes it a read-modify-write conflict on a single row, not write skew. Write skew requires the two transactions to write rows that the other's check did not read, which is precisely not the case here, as Implementation B's conditional UPDATE targeting the very row the check read makes obvious.
  2. Say which failure the SET clause buys. SET net_qty = 980, the value the application computed from its stale read, is a lost update: the second write overwrites the first, the row settles at 980, and the ledger now disagrees with the 1060 the venue actually filled. SET net_qty = net_qty + 80 loses nothing arithmetically and settles at 1060, over the cap and faithfully recorded. Both admitted two orders against one reading of the headroom.
  3. READ COMMITTED allows it. The second UPDATE blocks on the row lock, and once the first commits it re-evaluates its WHERE clause against the newly committed row version and proceeds. The write lands against fresh data; nothing re-checks the decision that authorised it.
  4. REPEATABLE READ does prevent this interleaving, and the precondition is the whole answer. PostgreSQL's REPEATABLE READ is snapshot isolation: a transaction that updates a row which a concurrent transaction committed after its snapshot is aborted with SQLSTATE 40001 rather than let through, so the second submit fails and a replay of the whole transaction re-reads 980 and rejects the order. That holds only because both transactions write the same row. Derive the position from a SUM over inserted order rows instead, so each transaction only appends and neither writes what the other read, and the identical check becomes genuine write skew, which REPEATABLE READ permits and only SERIALIZABLE catches.
  5. Implementation A, serialize on one row: SELECT ... FOR UPDATE the (account_id, instrument_id, business_date, 'intraday_derived') row inside the transaction. Correct only if every writer takes the same lock, and it has a hole worth naming: FOR UPDATE matching zero rows locks nothing, so the first order of the day in an instrument needs INSERT ... ON CONFLICT to materialize the row, or a lock on a surrogate such as pg_advisory_xact_lock over a hash of the pair, remembering that a hash collision merely over-serializes two unrelated pairs rather than letting either through. Cost: every order in a hot instrument serializes behind one row.
  6. Implementation B, collapse the check and the act into one statement: UPDATE position_snapshot SET pending_qty = pending_qty + :qty WHERE ... AND pending_qty + :qty <= :limit_value, and treat zero affected rows as the rejection. At READ COMMITTED an UPDATE that blocks on the row lock re-tests its WHERE against the version the other transaction committed, so the predicate is evaluated against fresh data with no window between reading and acting, and the second order is rejected with zero rows rather than admitted. Note the level interaction before deploying it: the same statement at REPEATABLE READ or SERIALIZABLE raises 40001 instead of quietly rejecting, so a retry loop is mandatory there rather than optional.
  7. Implementation C, SERIALIZABLE: serializable snapshot isolation tracks read-write dependencies rather than row versions, so it covers both shapes, the single-row conflict here that REPEATABLE READ already catches and the append-only variant it does not. It aborts with SQLSTATE 40001 and does not block. The retry has to replay the transaction from BEGIN including the read the decision rested on, because an aborted transaction rejects every further command with 25P02, so retrying just the failed statement is not a thing that exists. Throughput falls as contention on the pair rises, and an unbounded retry loop turns a hot instrument into a livelock.
  8. Then say what a trading system actually does, because the interviewer is waiting for it: the check is not a database round trip at all. It runs in process on a single-writer order path against a preloaded, versioned limit snapshot, because one round trip costs more than the entire tick-to-trade budget. The rows are the audit record, and the check records which limit_version it evaluated against so a rejection is explainable months later.

Worked solution (40 min)

  1. Open two sessions at READ COMMITTED and step the interleaving by hand: read, read, update, update, commit, commit. Run it once writing an absolute net_qty computed in the client and once with SET net_qty = net_qty + 80, and record the final row both times.
  2. Repeat at REPEATABLE READ, making sure both sessions issue their read before either commits, and record which statement errors and with which SQLSTATE.
  3. On that failed session, issue one further statement to see what retrying the statement alone actually returns.
  4. Build the append-only variant, where the check is a SUM over inserted order rows and neither transaction updates a shared row, and run it at REPEATABLE READ and again at SERIALIZABLE.
  5. Implement the conditional UPDATE and re-run the interleaving with the second session's UPDATE issued while the first still holds the row lock, checking the affected row count in each session.
  6. Delete the snapshot row and re-run the FOR UPDATE version to observe both sessions proceeding.

Expected result: READ COMMITTED admits both orders: the position ends at 1060 with the relative write, or at 980 with the venue holding 1060 if the client wrote an absolute value. REPEATABLE READ aborts the second updater with 40001, and a replay of the whole transaction re-reads and rejects, while a further statement on the aborted session returns 25P02. The append-only variant is admitted at REPEATABLE READ and aborted at SERIALIZABLE, which is exactly the line between a read-modify-write conflict and real write skew. The conditional UPDATE admits one order and rejects the other with zero rows affected. FOR UPDATE against a missing snapshot row matches nothing, locks nothing and admits both.

Verify it yourself

  • After every run of A, B or C with its retry loop, net_qty plus pending_qty never exceeds limit_value; the READ COMMITTED baseline is the run that does exceed it.
  • The rejected path left a row recording limit_id and limit_version, not just a boolean outcome.
  • Every 40001 retry replays the transaction from BEGIN including the read; a loop that re-issues only the failed statement gets 25P02, which proves it never re-read.

Follow-up

  • A limit is tightened at 10:00:00.000 while an order approved at 09:59:59.999 is in flight. What does the venue see, and what does your ledger record?
  • Your in-process check is the authority. How do you bound its divergence from the database, and what happens when they disagree?
  • Two order gateway processes serve the same account. What stops each from believing it owns the whole position?

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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-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?

Are you interested in asset management?

Medium

Approach

  1. Clarify what is being asked and what a complete answer contains.
  2. Say what you would check first and why it is the highest-information step.
  3. Work from the requirement backwards to the design.

Follow-up

  • What assumption would you test first?
  • How would you know your answer was wrong?

Which companies are you particularly excited to work with as an Alpha Financial Markets Consulting consultant?

Medium

Approach

  1. Say what you would check first and why it is the highest-information step.
  2. Clarify what is being asked and what a complete answer contains.
  3. Work from the requirement backwards to the design.

Follow-up

  • How would you know your answer was wrong?
  • What assumption would you test first?

Idempotent order submission under an ambiguous send timeout

Medium · idempotency · retries · http-semantics

Design the HTTP contract for POST /v1/orders, called by an execution management system that submits one order per request. Its socket times out at 250 ms and its retry policy fires immediately. order_event records client_order_id VARCHAR(40) (venue-unique, changes on replace), order_id BIGINT (stable across replaces), seq_no, status_after and received_ts. Specify the request body, the idempotency key with its scope and retention, the status codes for first accept, exact replay and key reuse with a different body, and exactly what the caller does when no response arrives at all.

Approach

  1. Make client_order_id the idempotency key and require the caller to allocate it, not the server: a server-generated key cannot deduplicate the retry of a request whose response was lost, because the retry would be given a fresh one.
  2. Persist (account_id, client_order_id, request_fingerprint) durably BEFORE the outbound venue send, so a crash between persist and send leaves a record to resolve against rather than a silent gap.
  3. Define the three outcomes: 201 with order_id on first accept; 200 replaying the stored response when the same key arrives with a matching fingerprint (a SHA-256 over the canonicalized body, so the server is not diffing JSON); 409 when the key repeats with a different fingerprint, which is a caller bug, not a retry.
  4. Implement the write as a single conditional insert (INSERT ... ON CONFLICT (account_id, client_order_id) DO NOTHING RETURNING event_id), so two concurrent retries race to one winner in one round trip instead of both passing a read-then-write check.
  5. State scope and retention explicitly: unique per (account_id, client_order_id), retained at least as long as the venue session plus the caller's maximum retry horizon. A key that ages out early converts a late retry into a second live order.
  6. Specify the no-response path as a separate operation: the caller MUST NOT resend; it calls GET /v1/orders?client_order_id=... or reads the drop-copy stream, and only acts once the venue's view is known.

Worked solution (30 min)

  1. Write the request body and response body for the happy path, marking which fields the caller supplies and which the server assigns.
  2. Write the persist-then-send sequence as numbered steps, and annotate each gap between steps with what a crash there leaves behind.
  3. Write the conditional-insert statement and trace two concurrent identical requests through it, naming which one gets 201 and which gets 200.
  4. Write the fingerprint rule: what is canonicalized, what is hashed, and what field difference must produce 409 rather than a silent accept.
  5. Write the caller's failure handling as pseudocode with three branches (2xx, 4xx, no-response), and check that the no-response branch contains no send.

Expected result: A contract in which the same client_order_id may be POSTed any number of times and at most one order ever reaches the venue: 201 on first accept, 200 replaying the stored response, 409 on fingerprint mismatch, and a documented status-query path that is the only legal response to receiving nothing.

Verify it yourself

  • Delete the response on the wire for every request in a test run: the number of orders at the venue must equal the number of distinct client_order_ids, not the number of POSTs.
  • Kill the server between the idempotency-record insert and the venue send; recovery must be able to classify that order as unknown rather than as absent.
  • Resubmit a stored key with order_qty changed by one and confirm a 409, not a second order and not a silent 200.

Follow-up

  • The process crashes after persisting the key but before the venue send. What does the recovery path do, and how does it tell that case apart from a send that succeeded?
  • How do you expire idempotency records without reopening the duplicate window, and what does the endpoint return for a key that has aged out?
  • A replace changes client_order_id but keeps order_id. Which of the two is the idempotency key for POST /v1/orders/{order_id}/replace, and why?

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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 someone who has spent the last few years shipping features and reading other people's code, and who has not solved a timed problem from a blank file in a long time. Five days rebuild the primitives and the patterns that sit on them, working from invariants rather than remembered solutions, and the last two attach that back to the rest of the loop.

Day 1 — Rebuild the primitives by implementing them

  • Implement a dynamic array with doubling growth and an operation counter, then change the growth rule to add a fixed sixteen slots instead, and time both for n of ten thousand, a hundred thousand and a million. The fixed-increment version resizes n/16 times at O(n) each, so its total work is quadratic; doubling is what makes append amortised constant.
  • Implement a hash map with separate chaining and a load-factor resize, then insert ten thousand keys engineered to land in one bucket and record what happens to lookup time, so that average-case O(1) becomes a claim with a stated precondition rather than a reflex.
  • For dynamic-array append and hash-map insert, write down which cost is amortised rather than worst-case, which single operation pays the whole bill, and what a system with a hard per-operation deadline would have to do instead.

Deliverable: Two working implementations plus a timing table showing the input at which each structure's advertised complexity stops holding.

Day 2 — Arrays under an invariant: two pointers, sliding window, binary search

  • Solve longest-subarray-with-sum-at-most-K using a sliding window, then run it on an input containing negative numbers and watch it return the wrong answer: extending the window only moves the sum monotonically when every element is non-negative, and that precondition is the whole reason the technique works.
  • Write the binary search that finds the first index satisfying a predicate rather than an exact value, put the loop invariant above the loop in a comment, and verify termination on the two inputs that break careless versions: the empty range, and a range where every element satisfies the predicate.
  • Compute the midpoint as lo + (hi - lo) / 2 and write one line on why the obvious (lo + hi) / 2 is a genuine defect in a fixed-width integer type and a non-issue in a language with arbitrary-precision integers.

Deliverable: Three solved problems, each with its invariant written above the loop, plus one recorded input on which the sliding window is provably wrong.

Day 3 — Sorting, heaps, and the greedy argument that has to be proved

  • Solve one top-k problem three ways, by full sort, by a size-k heap, and by quickselect, then write the values of n and k at which each becomes the right choice, along with quickselect's quadratic worst case and why a randomised pivot makes that unlikely rather than impossible.
  • Implement bottom-up heapify and count sift-down steps to confirm it does linear work rather than n log n, because most nodes sit near the bottom of the tree and therefore move only a short distance.
  • Take interval scheduling by earliest finishing time and write the exchange argument out in full: given any optimal schedule, swapping in the earliest-finishing interval keeps it feasible and no smaller. Then construct the weighted variant where that same greedy fails and name what has to replace it.

Deliverable: A three-way top-k comparison with measured crossover points, one written exchange argument, and one counterexample to a greedy rule that looks almost identical.

Day 4 — Recursion, memoisation, and the step to a table

  • Take one problem with overlapping subproblems, such as edit distance or coin change, instrument the plain recursion with a call counter to show the blow-up, then add memoisation and re-count.
  • Convert the memoised version to a bottom-up table and state the two properties you relied on: each subproblem's result depends only on its arguments, and the dependencies form a DAG you can enumerate in order.
  • Rewrite one deep recursion with an explicit stack, then find the input length at which the original hits the interpreter's frame limit, which defaults to about a thousand frames in CPython, so you know when the rewrite is required rather than decorative.

Deliverable: One problem in three forms, naive, memoised and tabulated, with call counts for each and the input length at which recursion depth becomes the binding constraint.

Day 5 — Graphs, where most of the work is choosing the traversal

  • Implement BFS and DFS over one adjacency list, then answer for each which finds a shortest path in an unweighted graph and which you would use to detect a cycle in a directed graph, including why the in-progress versus finished distinction matters for the second.
  • Implement topological sort by in-degree, feed it a graph containing a cycle, and confirm the failure signature is that fewer than V nodes come out rather than an exception, then note that the order it produces is one of several valid ones.
  • Run a shortest-path search on a graph with a single negative edge weight and show the wrong answer, then write the precondition Dijkstra actually needs, non-negative weights, because it finalises a node's distance the first time that node is popped, and name the algorithm you would switch to and its own limit.

Deliverable: A small graph library with BFS, DFS and topological sort, plus two inputs that produce documented wrong answers under the wrong algorithm choice.

Day 6 — One day for everything that is not an algorithm

  • Sketch one system only to the depth a coding-heavy loop tends to reach: the endpoints, what the service stores, and the single query pattern that decides the schema. Stop at twenty-five minutes.
  • Prepare the project answer for an interviewer who codes, which means rehearsing the two levels they push to: the specific thing you built, and why you chose that approach over the alternative they will name. Open with a number and be ready to say what it excludes.
  • Prepare the answer to what you would do differently, choosing a real technical mistake with a specific fix rather than a complaint about process or staffing.

Deliverable: One design sketch at endpoint-and-schema depth, plus a project answer rehearsed to two levels of follow-up.

Day 7 — Solve out loud, under time

  • Do three timed problems at twenty-five minutes each in a plain editor with no autocomplete and no execution until the end, then tally separately the failures that were syntax and the ones that were approach, because those two numbers call for different fixes.
  • Narrate one solution from the first sentence, stating the approach and its complexity before writing any code, and rehearse the sentence you will use when you realise mid-solution that the approach is wrong.
  • Re-solve from blank the two problems you were slowest on this week and compare the times against the day they first appeared.

Deliverable: A recording of one fully narrated solution and a tally that separates syntax failures from approach failures.

'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.

Give an example of when you've had to change the way you speak to suit a specific audience.

Medium · behavioural and engineering judgement

Approach

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

Where do you see yourself in 10 years?

Medium · behavioural and engineering judgement

Approach

  1. State the situation in two sentences and spend the rest on the reasoning.
  2. Give the blast radius: what could have broken, and what you measured.
  3. Pick a story where you made the decision, not one where you watched it.

Follow-up

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

Why do you think you are fit for this job?

Medium · behavioural and engineering judgement

Approach

  1. Pick a story where you made the decision, not one where you watched it.
  2. State the situation in two sentences and spend the rest on the reasoning.
  3. Close with what you would do differently, concretely.

Follow-up

  • What would you do differently if you ran that again?
  • What did you decide not to do, and why?