Skip to content
QSWEQB
hardDesignScenarioSeniorStaffLead

How would you put mandatory pre-trade risk checks on the order path when the desk will not accept the latency?

Keep the checks in-process over precomputed state so each is bounded arithmetic rather than a lookup, partition limit state by the key order flow already uses, and push anything needing a global view onto a drop-copy aggregator that grants budgets downwards. Add a kill switch that works when nothing else does.

6 min readUpdated 2026-07-26

What the interviewer is scoring

  • Does the candidate accept that the check is non-negotiable and make the latency the design problem rather than the other way round
  • Whether they separate checks that can be evaluated from local state from those that genuinely need a global view
  • That the exposure aggregate is described with an explicit staleness budget instead of being called real time
  • Whether the kill switch is placed so that it still works when the component that decided to pull it has failed
  • Does the answer name what happens to in-flight and resting orders at the moment the switch is thrown, not just new ones

Answer

Start by refusing the framing

The desk's request is to make the checks optional. That is not available. A broker giving a client direct market access is required by its regulator to apply risk controls on systems under the broker's own control, and cannot rely on the client's controls instead; the US market access rule and the algorithmic trading provisions of MiFID II both land in the same place. So the negotiable variable is latency, not coverage. Saying this out loud early is part of what is being scored, because a candidate who offers to sample orders or check asynchronously after release has answered a different, easier question.

What you can legitimately do is make the checks cheap enough that the argument goes away, and separate the checks that must be exact at the moment of release from the ones that only need to be exact eventually.

Classify the checks by what state they need

Most of the list needs nothing but the order and a small amount of static reference data. Is the symbol tradable and not suspended? Is the client permissioned for it? Is the price within a band of the last trade or reference price, and the quantity within a fat-finger cap? Is the notional under the per-order maximum? Is the instrument on a restricted list? Is this a short sale requiring a locate flag? Every one of these is a comparison against values that changed at start of day, or at worst on a slow control-plane update. They belong in memory, in the same process as the order handler, behind no network call at all.

A second group needs mutable state that only this account touches: cumulative traded notional today, open order value, net and gross position by symbol, message rate. These are counters. The check is a comparison and the update is an increment, so the cost is an atomic operation on a cache line rather than a query.

The third group is the awkward one: anything requiring a view across gateways, across products or across a legal entity. Aggregate exposure for a client trading through four sessions, margin utilisation against a portfolio model, or a house-wide limit spanning desks. These cannot be evaluated exactly in-process without coordination, and coordination is what costs the latency.

Get the first two groups off the latency budget entirely

The design principle is that the risk check is not a service the order path calls. It is code the order path executes, over state that has already been resolved.

// Limit state for one account, owned by exactly one thread on this gateway.
// No lock, no map lookup per field, no allocation on the hot path.
final class AccountLimits {
    long maxOrderNotional;      // static, refreshed by control plane
    long maxOpenNotional;       // static
    long openNotional;          // mutable, this account only
    long tradedNotionalToday;   // mutable, this account only

    // Reserve before release, so two orders cannot both fit in the same headroom.
    boolean tryReserve(long notional) {
        if (notional > maxOrderNotional) return false;
        if (openNotional + notional > maxOpenNotional) return false;
        openNotional += notional;   // released on cancel, reject or fill accounting
        return true;
    }
}

The important detail in that sketch is not the arithmetic, it is tryReserve. A check that tests headroom and then releases the order without consuming the headroom is a race: two orders each individually within limit can be approved into a combined breach. You reserve on release and unwind on reject, cancel or expiry. Get that wrong and the limit is advisory.

Two structural choices make this work. First, partition ownership so a given account's limit state is mutated by one thread on one gateway, which is normally free because order flow is already partitioned by session or account. Second, keep the control plane strictly out of the data path: limit changes, restricted-list updates and permission changes arrive on a separate channel and are swapped in as immutable snapshots, so a risk manager tightening a limit never blocks an order.

Reference data belongs in flat, preallocated arrays indexed by an internal instrument ID assigned at start of day, not in a hash map keyed by a symbol string. The symbol-to-ID resolution happens once, on the way in.

Accept staleness where it is honest, and bound it

For the cross-gateway aggregates, stop calling the result real-time and instead state the staleness budget. Every gateway publishes its own fills and order events to a drop-copy stream. An aggregation service consumes that stream, maintains firm-wide and client-wide exposure, and pushes revised headroom back down to the gateways as control-plane updates. The gateways enforce a locally held slice of the global limit rather than querying the global number.

That slice is the mechanism worth naming. Instead of asking a central service "may I", each gateway is granted a budget out of the client's total and enforces it locally at zero coordination cost; the aggregator redistributes budget between gateways as flow concentrates. The exposure number a risk manager looks at is therefore behind by the propagation delay of the drop-copy stream, and the correct engineering answer is to measure and alarm on that delay rather than to pretend it is zero. If the stream falls behind its threshold, the safe response is to shrink the granted budgets, not to keep trading on a number you no longer trust.

The same reasoning covers margin. A portfolio margin model is too expensive to evaluate per order. You evaluate it on a cycle, derive a conservative per-order and per-position envelope from it, enforce the envelope on the hot path, and re-derive the envelope when the model runs. The envelope is deliberately tighter than the model, and that gap is the price of not doing matrix algebra inside the order handler.

The kill switch is a separate system, not a feature of the risk service

A kill switch exists for the case where the automated controls are not working, which means it must not depend on them. The properties that matter:

It has to be reachable by a human under stress, from a place that does not require the trading application to be healthy. It has to act at the venue boundary as well as internally, because internal suppression of new orders does nothing about the orders already resting on an exchange. So the sequence is: stop accepting new orders, cancel resting orders at every venue by mass-cancel where the venue supports it, and drop the sessions. Cancel-on-disconnect at the venue is the backstop for the case where you cannot even send the cancels, and knowing whether each venue you trade offers it is a real operational fact about your setup, not a detail.

It has to have granularity, because the useful action is rarely "stop everything". Per strategy, per session, per account, per instrument. And it has to be idempotent and fast to confirm: the operator needs to see that the switch took effect, with a count of what was cancelled and what could not be, because an order that failed to cancel is the one that will hurt.

The state question people miss is what happens on the way back up. After a kill, the position is whatever the last fills made it, some of which may have arrived after the cancel was sent. Restart must reconcile against the venue's order status and drop copy before releasing anything, never against the application's own last known state.

What separates a strong answer here

The adequate answer lists the checks. The strong one is explicit about which invariant each check preserves exactly and which it preserves only eventually, and is willing to say out loud that a firm-wide exposure figure is a slightly stale number with a monitored bound. Regulators and risk officers are not upset by bounded staleness that is measured and defended. They are upset by a system that claims a guarantee it does not have, which is precisely what "we check everything in real time" means when the aggregation is asynchronous and nobody watches the lag.

Make the per-order checks arithmetic over locally owned state and they cost almost nothing; make everything that needs a global view a budget granted downwards rather than a question asked upwards. Then build the kill switch as if the rest of it has already failed, because that is the day you will use it.

Likely follow-ups

  • Which checks would you be willing to evaluate on data that is a few hundred milliseconds stale, and how do you justify that to a regulator?
  • How do you prevent two gateways from each approving an order that together breaches a single account limit?
  • What is your fail-safe posture if the limit-state store is unreachable at start of day versus mid-session?
  • How would you test the kill switch in production without stopping the desk?

Related questions

pre-trade-riskmarket-accessexposurekill-switchlow-latency