Design a Finance Platform
- Model the finance platform from first principles: ingestion, an authoritative position store, and risk on top
- Keep the numbers correct under load with idempotent fills, an audit trail and reconciliation
- Split the intraday path from the end-of-day batch, and reason aloud about feed drops and races
Understanding the problem
"Design a finance platform" lands on the whiteboard underspecified. Start by saying what it has to do before you name a single database. A finance platform takes in two firehoses, market data and trades, keeps an authoritative record of what every portfolio holds, and computes numbers on top of that record: profit and loss, exposure, risk. Three responsibilities, three layers. Keep them separate and the design stays coherent under every follow-up. Blur them and the analytics start quietly reporting on positions that were never fully written.
The trap is answering with the Aladdin diagram you half-remember. Resist it. Picture a candidate who can recite "the risk engine reads from the position service" but can't build the position service from scratch. They fall apart the instant the interviewer asks what happens when the same trade fill arrives twice. Build the general platform first, ingestion to storage to compute, and reason about correctness at every seam. The finance domain then bolts real constraints on top. The numbers are client money, so a wrong position is more than a cosmetic glitch, and a regulator can ask you to reproduce any figure months after the fact.
Bounding the requirements first
Spend a minute bounding the problem and it pays for itself, because a finance platform sprawls the moment you start naming stores without a scope. State the functional surface plainly. The platform ingests a high-rate market-data feed of prices and reference data. It ingests trade fills and orders as they execute. It maintains a position store, the authoritative record of what each portfolio holds. It computes analytics, profit and loss, exposure, and risk, both continuously through the day and as an official snapshot at the close. It serves queries and reports to the people who manage the money. And it keeps an audit trail complete enough to rebuild any number after the fact.
Then draw the line around what you handle now and what you name and defer. The full risk mathematics is deep, and deriving a value-at-risk model or a full instrument taxonomy in the first pass guarantees you run out of time before the skeleton is clean. Model the platform so those pieces have an obvious home, then say so. Position keeping and its correctness are the core you build. The specific risk formulas, the exotic instrument types, and the multi-region replication are named, given a seam, and left for the follow-up. A design where "swap the simple mark-to-market for a full risk model later" changes one component is a good design. One where it means rewriting ingestion is not.
- The grading axis: correctness over availability. This is a platform of record for client money, so a stale or double-counted number outranks a dropped request. When you reach a fork, keep the guarantee that protects the data and say what you traded for it. Throughput math is not the centerpiece here.
- Source of truth: the position store. Everything derived, P&L, exposure, risk, is a function of positions and prices. Positions are themselves derived from an ordered log of trades. Keep that dependency chain explicit so a wrong analytic can always be traced back to the fill that caused it.
- The feed is at-least-once: assume duplicates and gaps. A real market-data or trade feed redelivers on reconnect and occasionally drops. The platform cannot wish that away, so it must dedupe and it must detect gaps. Exactly-once is something you build on top of an at-least-once transport, not something you are handed.
- Reproducibility: every number, months later. A regulator or a client can ask why a figure was what it was on a given day. That turns the audit trail from a nice-to-have into a hard requirement: an append-only record of every event and every mark, enough to replay the state as of any point in time.
The object model, before the boxes
Start from the types, because this round rewards a clean object model far more than a busy diagram. Five nouns carry the whole platform. An Instrument is the thing you can hold, a stock, a bond, a currency, identified by a stable id and carrying its reference data. A Price is a mark for an instrument at a moment in time. A Trade is a single executed fill: an instrument, a signed quantity, a price, a timestamp, and a unique id the venue stamped on it. A Position is what one portfolio holds in one instrument, a running aggregate of quantity and cost. A Portfolio is a named bag of positions. Underline those nouns and the classes are already halfway written.
The decision that makes the model correct rather than merely tidy is treating a Position as a derived view, not a primary record you mutate in place. A Position is the fold of every Trade for that instrument in that portfolio, applied in order. The authoritative store is the ordered trade log; the live position table is a materialized cache of that fold. That single choice buys three things at once. You can rebuild any position by replaying its trades, which is exactly what reconciliation needs. You get a natural audit trail for free, because the log already holds every event. And you can apply a fill idempotently, because "have I already folded this trade id in" is a question the log can answer.
// A Trade is an IMMUTABLE event: what filled, how much, at what price, when,// and a venue-assigned id that makes it unique. Immutability is deliberate.// A trade is a fact that already happened, and facts are not edited in place.final class Trade { final String tradeId; // unique per fill: this is the idempotency key final String instrumentId; final long quantity; // SIGNED: positive buy, negative sell final long priceTicks; // integer minor units; never a float on money final long timestamp; // event time, from the venue clock // constructor + accessors omitted}// A Position is a MATERIALIZED VIEW of one instrument in one portfolio: the// running aggregate of every trade folded in. It is DERIVED, so it can always// be rebuilt by replaying the trade log, which is what makes reconciliation// and audit possible. The ledger, not the Position, enforces "fold once".final class Position { final String instrumentId; long netQuantity; // sum of signed trade quantities long costBasisTicks; // sum of quantity * price, for average cost + P&L void fold(Trade t) { // apply one already-deduped fill netQuantity += t.quantity; costBasisTicks += t.quantity * t.priceTicks; }}Defend two details in that snippet out loud. Money is an integer count of minor units, never a floating-point number. A float silently loses cents, and a platform that loses cents at scale fails an audit. And the Position knows how to fold a trade but not whether it should. Deduplication lives one layer up in the ledger, so the Position stays a pure aggregate and the "apply exactly once" rule has exactly one home. Keep those responsibilities apart and the correctness argument stays simple enough to say in a sentence.
The platform end to end
Now lift the object model into a running system and draw it, because the architecture is really the seams between the layers and the order data moves through them. Read the diagram by role. Blue is the outside world handing you data or asking for answers. Indigo is a service that transforms. Amber is the durable event log that everything hangs off. Green is state the platform owns. Follow the numbered edges for one full path. Raw feeds land at the gateway, which appends normalized events to the log. The log drives both the position store and the price store. The analytics engine reads a consistent view of both, and a separate reconciliation job checks the live positions against a fresh replay.
Ingestion: exactly-once on an at-least-once feed
Assume the feed will betray you, because it will. A real market-data or trade transport redelivers messages when a consumer reconnects, occasionally delivers them out of order, and now and then drops one. None of that is exotic; it is the normal operating condition. The platform's job is to turn that messy at-least-once stream into an exactly-once effect on positions. It gets there with two mechanisms working together: a durable idempotency key on every event, and an append-only log that makes replay safe.
The idempotency key is the venue-assigned trade id, or a synthetic hash for prices, and the dedup store is the set of keys already applied. Before a consumer folds a fill into a position, it checks the key; if it has already been folded, the message is dropped on the floor with no effect. That check is what makes redelivery harmless, and it is the single most important line in the whole ingest path. Order matters too, so the gateway partitions by instrument, keeping every event for one instrument on one ordered lane while different instruments flow in parallel. And when a consumer falls behind, the log absorbs the backlog rather than forcing the gateway to drop live data, so a slow analytics run never corrupts the position record.
Idempotent apply, keyed by trade id
Every fill carries a unique id, and the consumer folds it only if that id has not been seen. A redelivered message hits the dedup set and is discarded with no effect on the position. This is what converts an at-least-once transport into an exactly-once outcome, and it is the correctness guarantee the whole platform rests on.
Order per instrument, parallel across them
Fills for one instrument must apply in sequence or the cost basis goes wrong, so the gateway partitions by instrument id and keeps each instrument on one ordered lane. Different instruments flow on different lanes at once, so ordering where it matters does not cost you throughput where it does not.
Append-only log as the backbone
Events are appended to a durable, ordered log before any view is updated, so a consumer that crashes resumes from its last offset and a new view is built by replaying from the start. The log, not the live tables, is the source of truth, which is exactly why a replay can rebuild any position.
Detect gaps, absorb bursts
At-least-once does not rule out a genuine drop, so events carry sequence numbers and a gap raises an alert rather than passing silently. When a consumer lags, the log buffers the backlog instead of the gateway shedding live data, so slow downstream work never corrupts the record upstream.
Analytics: two clocks, one consistent snapshot
The compute layer runs on two clocks, and conflating them is a common way this design goes wrong. Through the trading day the platform wants a live view: as fills and ticks arrive, it updates P&L and exposure incrementally so a manager sees a current picture. At the close it wants an authoritative view: an official end-of-day batch, valued against agreed marks, that becomes the number of record for reporting and risk. The intraday path optimizes for freshness and the end-of-day path for authority. Both are computed from the same event log, which lets you reconcile the fast approximate view against the slow official one.
The consistency rule underneath both is the part to say out loud: an analytic is only meaningful against a consistent snapshot. A portfolio total that values today's positions with yesterday's prices, or half the positions before a trade and half after, is not a rounding error, it is a wrong number that looks right. So the analytics engine reads a fixed version of the positions and a fixed set of marks together, computes against that frozen pair, and stamps the result with the versions it used. When a new fill lands, the derived analytic for that portfolio is invalidated and recomputed, rather than being patched in place while someone is reading it.
Intraday, the live view
- Updated incrementally as fills and ticks arrive, so managers see a current picture through the day
- Optimized for freshness and low latency on the read path, computed against the latest consistent snapshot
- Approximate in the sense that marks are the latest available, not the official close, but never inconsistent within a single figure
- Serves the trading and monitoring use case, where "roughly right, right now" beats "exactly right, tomorrow"
End of day, the authoritative view
- Computed as a batch at the close against agreed official marks, becoming the number of record
- Optimized for correctness and reproducibility over speed, and fully reconciled against the trade log
- The figure that feeds regulatory reporting, client statements, and formal risk, so it must be defensible months later
- Derived from the same event log as the intraday view, so the two can be compared and any divergence explained
Correctness, consistency, and proving it
Pull the correctness story together, because on this platform it is the design. Four mechanisms carry it, and they reinforce each other. Idempotency stops a redelivered fill from double-counting. Exactly-once is the effect that idempotency plus the ordered log produce out of an at-least-once feed. The audit trail records every event and every mark so any figure can be rebuilt as of any moment. Reconciliation independently recomputes positions from the log and compares them to the live store. A bug or a dropped event then surfaces as a flagged break instead of a silent lie in a client report.
Reconciliation deserves a sentence of its own, because it is the mechanism that makes the whole thing trustworthy rather than merely hopeful. The live position store is a cache of a fold; caches drift. So a scheduled job replays the trade log from the beginning, rebuilds each position, and diffs it against what the live store holds. A match is quiet confidence. A mismatch is a break, recorded and raised for a human, never auto-overwritten. You want to know whether the fold path has a bug or the feed lost an event before you paper over the symptom. Being able to say "and here is how I would catch it when the store and the log disagree" is the reliability instinct the room is set to find.
- Idempotency: a durable key per event. Every fill carries a unique id and every consumer checks it before folding. A redelivered message is dropped with no effect, so the same fill applied twice moves a position exactly as much as applying it once. This is the guard that makes an unreliable feed safe.
- Exactly-once: built, not assumed. The transport gives you at-least-once at best. Exactly-once is the effect you construct on top of it with an idempotency key and an ordered, replayable log. Claiming the feed itself is exactly-once is the kind of hand-wave the interviewer is waiting to hear.
- Audit trail: append-only, replayable. Store the full history of events and marks, not just current state, so any number can be reconstructed as of any point in time. This is what turns "we believe it was right" into a replay that proves it, which is the difference between passing an audit and failing one.
- Reconciliation: recompute and diff. Independently rebuild positions from the log and compare to the live store on a schedule. A break is flagged and recorded, never silently overwritten, because a mismatch means a bug or a dropped event, and you want to see it before a client does.
The runnable core: an idempotent position ledger
The interviewer often compresses the whole platform into one class you implement. For a finance platform the natural one is the position ledger, which folds trade fills into positions and refuses to fold the same fill twice. It is the entire correctness story in miniature. The dedup on the trade id is exactly-once made concrete, and the running aggregate is the materialized view. The boundary case is a duplicate fill that must leave the position untouched, and it's where a candidate either states a clean invariant or hand-waves.
The problem below keeps the bookkeeping and drops the pricing math, because that is what runs. A fill here is a trade id, an instrument, and a signed quantity. That's enough to define idempotent application (fold each id once), position lookup (the net for an instrument), and the book (every non-zero position). The exercise is handling the duplicate fill and the net-to-zero position cleanly, much as a deduplicating cache turns on the duplicate key.
Position ledger: fold trade fills exactly once
Problem
-
PositionLedger() creates an empty ledger.-
apply(tradeId, instrument, qty) folds one fill: instrument instrument traded qty shares, signed (positive buy, negative sell). If tradeId has already been applied, this is a redelivery, so change nothing and return false. Otherwise record the id, add qty to that instrument's net position, and return true.-
position(instrument) returns the current net signed quantity for instrument, or 0 if it has never traded.-
book() returns the current holdings as [instrument, qty] pairs for every instrument whose net position is non-zero, sorted by instrument ascending. A position that has netted back to zero does not appear.-
applied() returns the number of distinct fills applied so far.The idempotency check and the net-to-zero rule carry the exercise. A redelivered fill must move nothing, and an instrument bought and fully sold must drop out of the book rather than linger at zero.
Input encoding. A test case is a JSON array of operations. The first entry is
["PositionLedger", []]; each later entry is [method, [args]]. The expected array holds each operation's return, with null for the constructor.Input: PositionLedger(); apply("t1","AAPL",100); apply("t1","AAPL",100); position("AAPL"); applied()
Output: the second apply returns false; position("AAPL") returns 100; applied() returns 1
The first apply folds t1 and returns true. The second apply carries the same trade id, so it is a redelivery: the ledger returns false and the AAPL position stays at 100, not 200. Exactly one distinct fill has been applied.
Input: PositionLedger(); apply("t1","AAPL",100); apply("t2","AAPL",-100); position("AAPL"); book()
Output: position("AAPL") returns 0; book() returns []
A buy of 100 then a sell of 100 nets AAPL to zero, so the position lookup returns 0. AAPL doesn't appear in the book at all, since only non-zero holdings are listed.
Keep a set of applied trade ids and a map from instrument to net quantity. apply is a membership check with two branches; position is a map lookup defaulting to 0; book filters out zero nets and sorts; applied is the size of the id set.
Position ledger: fold trade fills exactly once
Click "Run" to execute your code
Test Cases
[["PositionLedger",[]],["apply",["t1","AAPL",100]],["apply",["t1","AAPL",100]],["position",["AAPL"]],["applied",[]]]Expected: [null,true,false,100,1][["PositionLedger",[]],["apply",["t1","AAPL",100]],["apply",["t2","AAPL",-40]],["apply",["t3","MSFT",50]],["position",["AAPL"]],["book",[]]]Expected: [null,true,true,true,60,[["AAPL",60],["MSFT",50]]][["PositionLedger",[]],["apply",["t1","AAPL",100]],["apply",["t2","AAPL",-100]],["position",["AAPL"]],["book",[]]]Expected: [null,true,true,0,[]][["PositionLedger",[]],["apply",["t1","AAPL",100]],["apply",["t2","MSFT",30]],["apply",["t1","AAPL",100]],["apply",["t2","MSFT",30]],["position",["AAPL"]],["position",["MSFT"]],["applied",[]]]Expected: [null,true,true,false,false,100,30,2][["PositionLedger",[]],["apply",["t1","AAPL",100]],["position",["TSLA"]],["book",[]]]Expected: [null,true,0,[["AAPL",100]]]Scale and failure, without losing a number
A platform that works in a demo and loses a fill under load is a failing answer, so spend real time on scale and the ways it breaks. Scale here is less about raw requests per second and more about keeping the write path and the read path from fighting each other. Ingestion and position keeping are write-heavy and must stay ordered per instrument; queries and reporting are read-heavy and bursty. Separate them, let the read side serve from materialized views and the analytics outputs, and each scales on its own axis without a heavy report run slowing down the fold of a live fill.
Then walk the failure modes out loud, because on a live platform they are routine. A consumer restarts mid-stream, so it resumes from its last committed offset on the log and the idempotency check makes any reprocessed events harmless. A feed drops a message, so sequence numbers turn the gap into an alert instead of a silent hole, and reconciliation catches the drift even if the alert is missed. Two updates race for the same position, so writes to one position are serialized on its partition lane rather than left to a last-writer-wins race that would lose a fill. And the position store and the log disagree, which is why reconciliation exists at all. Name these before the interviewer does and you read as senior.
- Split read from write: they scale differently. Ingestion and position folding are ordered and write-heavy; reporting is bursty and read-heavy. Serve reads from materialized views and analytics outputs so a large report never slows the fold of a live fill, and each side scales on its own.
- Consumer restart: resume from the offset. A crashed consumer restarts and resumes from its last committed log offset. Because apply is idempotent, any events reprocessed on the way back are harmless, so a restart costs a little rework and never a corrupted position.
- Dropped message: gap becomes an alert. At-least-once still allows a genuine drop, so events carry sequence numbers and a missing one raises an alert rather than passing silently. Even if the alert is missed, the reconciliation replay surfaces the resulting drift as a break.
- Racing writes: serialize per position. Two fills for the same position must not race, or one is lost to last-writer-wins. Keeping each instrument on one ordered partition lane serializes writes to a given position without serializing the whole platform.
What the round rewards
The bar on this design is a clean object model, correctness under concurrency, and consistency of the numbers, not traffic scale. Whichever finance prompt you draw, a portfolio service, a market-data pipeline, a risk platform, the moves are the same. Bound the problem and name the three layers before you draw a box, so ingestion, position keeping, and analytics stay separate. Model the types first and make positions a derived fold of an immutable trade log, so audit and reconciliation come almost for free. Treat exactly-once as something you build with an idempotency key over an at-least-once feed, never something you assume. Keep derived analytics consistent with a versioned snapshot of the positions they came from. And name the failure modes yourself, redelivery, a dropped message, racing writes, a store that drifts from the log, because on a platform of record those are the design, not the footnotes. Show that judgment and the finance skin, Aladdin or otherwise, sits naturally on top.
Key Takeaways
- ✓Build the general data platform first, then add the finance skin. Split it into three separate layers, market-data ingestion, an authoritative position store, and a risk-and-analytics compute layer, and reason about correctness at each seam. A candidate who can only recite the Aladdin diagram but cannot design the position store from scratch fails the first correctness follow-up.
- ✓Model the types before the boxes and make positions derived. An immutable Trade is a fact; a Position is a materialized fold of the trade log, and money is integer minor units, never a float. That one choice gives you rebuildable positions, a free audit trail, and a natural place for the exactly-once rule to live.
- ✓Exactly-once is built, not assumed. The feed is at-least-once, so an idempotency key on every fill plus an ordered, replayable log turn redelivery into a no-op and make a consumer restart harmless. Keep analytics consistent by computing against a versioned snapshot of positions and marks, and separate the live intraday view from the authoritative end-of-day close.
- ✓Correctness is the design here, not a footnote. Idempotency, exactly-once, an append-only audit trail, and a reconciliation job that recomputes positions and flags breaks are what make the numbers trustworthy on a platform of record. The runnable core is the idempotent position ledger, where the dedup check and the net-to-zero rule carry the exercise.
BlackRock interview guideKnow what BlackRock asks before you walk in.
13 coding · 3 system design · 15 behavioral questions
Get their questions first.
InterviewLegend is independent and not affiliated with or endorsed by any company it covers.