Format: 60-minute pair programming on HackerRank IDE, 1–2 business-logic problems, Java/Kotlin preferred. Team context: Stock-market broker (Personal Investing platform). Goal: Translate spoken business rules into clean, testable code while thinking out loud.


1. What the Interviewers Are Really Evaluating

The recruiter and engineer both emphasized that finishing is not the goal. They are scoring you on:

Signal What "good" looks like
Requirement clarification Restate the problem; ask 3–5 sharp questions before writing code.
Logical decomposition Identify entities, inputs, outputs, edge cases out loud before typing.
Pragmatism Pick the simplest data structure that works; defer optimization until asked.
Communication Narrate trade-offs ("I'll use a TreeMap because we need price-sorted access in O(log n)").
Receptiveness When they suggest an alternative, treat it as a hint, not a trap. Probe, then incorporate.
Code quality Naming, encapsulation, immutability where sensible, no magic numbers.
Testability Verbalize a few test cases up front; run them mentally or in main.
Production mindset Mention concurrency, persistence, observability only when asked — but be ready.

Anti-patterns to avoid: - Coding silently for 5+ minutes. - Jumping straight to micro-optimizations (e.g., bit-twiddling) on a business-logic problem. - Stubbornly defending a design when the interviewer pushes back twice. - Apologizing instead of recovering — narrate the fix instead.


2. Communication Framework — "RECAP"

Use this loop for every problem. It maps to what JPMC engineers explicitly look for.

  1. Restate — "So I need to build X that does Y given Z…"
  2. Examples — Walk through 1 happy-path and 1 edge case verbally.
  3. Clarify — Ask requirement questions (see banks below).
  4. Approach — Describe data structures + algorithm in plain English before typing.
  5. Program & probe — Code in small slices; after each slice, dry-run an example.

3. Universal Clarifying Question Bank

Memorize these — they apply to almost any broker problem:

  • Inputs: What format? Stream of events, batch, or method calls? Are timestamps provided or do I generate them?
  • Order of events: Are events guaranteed to arrive in chronological order?
  • Currency / precision: Should prices be double, BigDecimal, or fixed-point integers (cents/pence)?
  • Concurrency: Single-threaded for this exercise, or do I need to worry about thread-safety?
  • Persistence: In-memory only?
  • Validation: Should I reject malformed input or assume it's clean?
  • Partial fills: Are quantities atomic, or can an order fill across multiple counterparties?
  • Cancellation / amendment: Do I need to support these in v1?
  • Negative balances / short selling: Allowed?
  • Tie-breaking: Price-time priority (FIFO at same price)? Or pro-rata?

Tip: Pick 2–3 questions that materially change your design. Don't ask all ten — that wastes time.


4. PRIMARY MOCK PROBLEM — Limit Order Matching Engine

This is the single most likely problem for a broker team. It tests data structures, ordering, business invariants, and partial fills — all classic JPMC broker logic.

4.1 Problem Statement (as the interviewer would read it)

"We're building the matching engine for a single stock. Clients submit limit orders with a side (BUY/SELL), a price, and a quantity. Implement a class OrderBook that supports:

  • place(Order order) — add an order. If it can match against the opposite side of the book at a compatible price, generate one or more trades. Any unfilled remainder rests on the book.
  • cancel(long orderId) — remove an order from the book if it's still resting.
  • getTrades() — return all trades generated so far, in the order they happened.

Matching rule: a BUY at price P_b matches a SELL at price P_s when P_b >= P_s. The trade price is the resting order's price (price-time priority)."

4.2 Sample Clarifying Questions to Ask

  1. "Are prices in whole cents/pence, or do I need decimals? I'd prefer integers to avoid floating-point bugs." — They will almost always say "integers are fine."
  2. "Should I support market orders or just limit orders for v1?" — They'll say limit only initially, then maybe extend.
  3. "For tie-breaking at the same price, FIFO by arrival time?" — Yes (price-time priority).
  4. "Can an incoming order partially fill against multiple resting orders?" — Yes.
  5. "Single-threaded?" — Yes for the exercise.

4.3 Approach to Verbalize Before Coding

"I'll model two sides of the book. Each side maps a price level to a FIFO queue of resting orders. For BUYs I want the highest price accessible first; for SELLs the lowest. A TreeMap<Long, Deque<Order>> gives me O(log n) access to the best price and natural sorted iteration. When a new order arrives, I peek at the opposite side's best price; if it crosses, I match against the queue head, decrement quantities, record a trade, and advance. I stop when the order is fully filled or the prices no longer cross — then I rest the remainder."

That paragraph is what an interviewer wants to hear before you touch the keyboard.

4.4 Reference Solution (Java)

import java.util.*;

public class OrderBook {

    public enum Side { BUY, SELL }

    public static final class Order {
        public final long id;
        public final Side side;
        public final long priceCents;
        public long remainingQty;
        public final long timestamp;

        public Order(long id, Side side, long priceCents, long qty, long timestamp) {
            if (qty <= 0)        throw new IllegalArgumentException("qty must be > 0");
            if (priceCents <= 0) throw new IllegalArgumentException("price must be > 0");
            this.id = id;
            this.side = side;
            this.priceCents = priceCents;
            this.remainingQty = qty;
            this.timestamp = timestamp;
        }
    }

    public static final class Trade {
        public final long buyOrderId;
        public final long sellOrderId;
        public final long priceCents;
        public final long qty;

        public Trade(long buyOrderId, long sellOrderId, long priceCents, long qty) {
            this.buyOrderId  = buyOrderId;
            this.sellOrderId = sellOrderId;
            this.priceCents  = priceCents;
            this.qty         = qty;
        }

        @Override public String toString() {
            return String.format("TRADE buy=%d sell=%d px=%d qty=%d",
                                 buyOrderId, sellOrderId, priceCents, qty);
        }
    }

    // BUY side: highest price first  -> reverseOrder
    private final TreeMap<Long, Deque<Order>> bids = new TreeMap<>(Comparator.reverseOrder());
    // SELL side: lowest price first  -> natural order
    private final TreeMap<Long, Deque<Order>> asks = new TreeMap<>();

    private final Map<Long, Order> byId = new HashMap<>();
    private final List<Trade> trades = new ArrayList<>();

    public List<Trade> place(Order incoming) {
        List<Trade> generated = new ArrayList<>();
        TreeMap<Long, Deque<Order>> opposite = (incoming.side == Side.BUY) ? asks : bids;

        while (incoming.remainingQty > 0 && !opposite.isEmpty()) {
            Map.Entry<Long, Deque<Order>> bestLevel = opposite.firstEntry();
            long bestPrice = bestLevel.getKey();

            boolean crosses = (incoming.side == Side.BUY)
                    ? incoming.priceCents >= bestPrice
                    : incoming.priceCents <= bestPrice;
            if (!crosses) break;

            Deque<Order> queue = bestLevel.getValue();
            Order resting = queue.peekFirst();

            long fillQty = Math.min(incoming.remainingQty, resting.remainingQty);
            long buyId   = (incoming.side == Side.BUY)  ? incoming.id : resting.id;
            long sellId  = (incoming.side == Side.SELL) ? incoming.id : resting.id;

            Trade t = new Trade(buyId, sellId, resting.priceCents, fillQty);
            trades.add(t);
            generated.add(t);

            incoming.remainingQty -= fillQty;
            resting.remainingQty  -= fillQty;

            if (resting.remainingQty == 0) {
                queue.pollFirst();
                byId.remove(resting.id);
                if (queue.isEmpty()) opposite.pollFirstEntry();
            }
        }

        if (incoming.remainingQty > 0) rest(incoming);
        return generated;
    }

    public boolean cancel(long orderId) {
        Order o = byId.remove(orderId);
        if (o == null) return false;
        TreeMap<Long, Deque<Order>> book = (o.side == Side.BUY) ? bids : asks;
        Deque<Order> queue = book.get(o.priceCents);
        if (queue == null) return false;
        boolean removed = queue.removeIf(x -> x.id == orderId);
        if (queue.isEmpty()) book.remove(o.priceCents);
        return removed;
    }

    public List<Trade> getTrades() {
        return Collections.unmodifiableList(trades);
    }

    private void rest(Order o) {
        TreeMap<Long, Deque<Order>> book = (o.side == Side.BUY) ? bids : asks;
        book.computeIfAbsent(o.priceCents, k -> new ArrayDeque<>()).addLast(o);
        byId.put(o.id, o);
    }
}

4.5 Tests to Verbalize / Quickly Type

public static void main(String[] args) {
    OrderBook ob = new OrderBook();

    // Two sells rest on the book
    ob.place(new OrderBook.Order(1, OrderBook.Side.SELL, 101, 50, 1));
    ob.place(new OrderBook.Order(2, OrderBook.Side.SELL, 102, 30, 2));

    // Aggressive buy crosses both levels
    ob.place(new OrderBook.Order(3, OrderBook.Side.BUY, 103, 60, 3));
    // Expect: TRADE buy=3 sell=1 px=101 qty=50
    //         TRADE buy=3 sell=2 px=102 qty=10
    //         20 left of order 2 still resting at 102

    ob.getTrades().forEach(System.out::println);
}

4.6 Likely Follow-Up Probes (and how to answer)

Interviewer probe Strong answer
"What if I add market orders?" Treat as a limit with Long.MAX_VALUE for BUY / 0 for SELL — same code path. Mention the IOC (immediate-or-cancel) semantic: don't rest the remainder.
"What if quantity is huge — millions of resting orders?" The current design is already O(log P + F) per place, where P = price levels, F = fills. Bottleneck would be removeIf in cancel — switch to a doubly-linked list per level + Map<orderId, Node> for O(1) cancel.
"How do you make it thread-safe?" Single writer thread + lock-free reader queues is the production pattern (LMAX Disruptor). For a simpler answer: a single ReentrantLock around the whole book — matching is fast and a single hot lock is fine until proven otherwise.
"How would you persist this?" Event-source it: append every place/cancel to a Kafka topic. The book is a fold over events; rebuild on startup by replaying.
"How do you avoid floating-point bugs on price?" Use integer minor units (pence/cents) or BigDecimal with a fixed scale. Never double for money.
"What about self-trade prevention (same client both sides)?" Add a clientId on Order; when matching, if incoming.clientId == resting.clientId, cancel the resting order (or the incoming) per business rule.

5. SECONDARY MOCK PROBLEM — Position & P&L Tracker

Likely as a second problem if the first goes quickly, or as a standalone if the team focuses on portfolio/back-office logic.

5.1 Problem Statement

"Given a stream of executed trades for a single account, build a Portfolio class that, on demand, returns:

  • Position per ticker (signed quantity, positive = long, negative = short).
  • Average cost per ticker (weighted average of buys at the time of each buy; resets when the position flips through zero).
  • Realized P&L — locked in when a sell reduces a long position (or a buy reduces a short).
  • Unrealized P&L(currentMarketPrice - avgCost) * position."

5.2 Clarifying Questions

  1. "Can a single trade flip the sign of a position (e.g., long 5, sell 8)?" — Almost certainly yes. This is the trickiest case.
  2. "Are prices BigDecimal or integer minor units?" — Pick integers for simplicity unless they say otherwise.
  3. "Do I need FIFO/LIFO lot accounting, or weighted-average cost?" — They will usually say weighted-average for v1. This is a big simplifier — confirm it.
  4. "Multiple currencies?" — No, single currency for v1.

5.3 Approach to Verbalize

"I'll keep one record per ticker holding signed quantity, avgCost, and realizedPnl. On each trade I split into three cases: 1. Same-direction trade (adding to a long via buy, or to a short via sell): update weighted average cost. 2. Opposite-direction trade fully within the existing position: realize P&L on the closed quantity, leave avgCost unchanged. 3. Opposite trade larger than the position: realize P&L on the entire existing position, then open a new position in the opposite direction at the trade price.

Unrealized P&L is computed on demand using the latest market price."

5.4 Reference Solution (Java)

import java.util.*;

public class Portfolio {

    public enum Side { BUY, SELL }

    public static final class Trade {
        public final String ticker;
        public final Side side;
        public final long quantity;     // always positive
        public final long priceCents;
        public Trade(String ticker, Side side, long quantity, long priceCents) {
            this.ticker = ticker; this.side = side;
            this.quantity = quantity; this.priceCents = priceCents;
        }
    }

    static final class Position {
        long signedQty;     // + long, - short
        long avgCostCents;  // average cost while position is open
        long realizedPnl;   // cumulative
    }

    private final Map<String, Position> book = new HashMap<>();

    public void onTrade(Trade t) {
        Position p = book.computeIfAbsent(t.ticker, k -> new Position());
        long delta = (t.side == Side.BUY) ? t.quantity : -t.quantity;

        if (p.signedQty == 0) {
            // Opening a new position
            p.signedQty = delta;
            p.avgCostCents = t.priceCents;
            return;
        }

        boolean sameDirection = Long.signum(p.signedQty) == Long.signum(delta);
        if (sameDirection) {
            // Weighted-average cost update
            long newQty = p.signedQty + delta;
            long totalCost = Math.abs(p.signedQty) * p.avgCostCents
                           + Math.abs(delta)       * t.priceCents;
            p.avgCostCents = totalCost / Math.abs(newQty);
            p.signedQty    = newQty;
            return;
        }

        // Opposite direction: realize P&L on the overlap
        long closingQty = Math.min(Math.abs(p.signedQty), Math.abs(delta));
        long pnlPerUnit = (p.signedQty > 0)
                ? (t.priceCents - p.avgCostCents)   // closing a long: profit if sold higher
                : (p.avgCostCents - t.priceCents);  // closing a short: profit if bought lower
        p.realizedPnl += pnlPerUnit * closingQty;

        long remaining = p.signedQty + delta;
        if (remaining == 0) {
            p.signedQty = 0;
            p.avgCostCents = 0;
        } else if (Long.signum(remaining) == Long.signum(p.signedQty)) {
            // Reduced but not flipped
            p.signedQty = remaining;
            // avgCost unchanged
        } else {
            // Flipped through zero — open new position at trade price
            p.signedQty = remaining;
            p.avgCostCents = t.priceCents;
        }
    }

    public long position(String ticker) {
        Position p = book.get(ticker);
        return p == null ? 0 : p.signedQty;
    }

    public long realizedPnl(String ticker) {
        Position p = book.get(ticker);
        return p == null ? 0 : p.realizedPnl;
    }

    public long unrealizedPnl(String ticker, long marketPriceCents) {
        Position p = book.get(ticker);
        if (p == null || p.signedQty == 0) return 0;
        return (marketPriceCents - p.avgCostCents) * p.signedQty;
    }
}

5.5 Walk-through Example to Narrate

Buy  10 AAPL @ 100   -> long 10, avg 100, realized 0
Buy  10 AAPL @ 120   -> long 20, avg 110, realized 0
Sell  5 AAPL @ 130   -> long 15, avg 110, realized +100   ((130-110)*5)
Sell 20 AAPL @ 140   -> short 5, avg 140, realized +100 + (140-110)*15 = +550

Talking through that example out loud convinces the interviewer your code is correct without them having to read it line by line. Huge signal.

5.6 Likely Follow-Up Probes

  • "What if the business wants FIFO lot accounting (for tax)?" — Replace the Position with a Deque<Lot> per ticker; each buy pushes a lot, each sell consumes from the head. Acknowledge it changes the data model and is more memory.
  • "What about dividends / corporate actions?" — Out of scope for v1; mention you'd model them as a separate event type that adjusts cost basis or cash, not position.
  • "How would you serve this at scale?" — Partition by accountId; portfolio state is an aggregate, naturally shardable. Persist via event sourcing (every trade is the source of truth).

6. Backup Problem Bank (in case you finish early or get a different one)

Practice each one for 20 minutes. They all share the same shape: business rules → small classes → clear invariants.

  1. Cash account / walletdeposit, withdraw, transfer, getBalance. Reject overdraft. Add transaction history.
  2. Trade settlement (T+2) — given a stream of executed trades, on a given date return the cash and stock movements that should settle today.
  3. Fee calculator — tiered commission: 0.5% up to £10k, 0.3% up to £100k, 0.1% above. Plus a flat £1 minimum and a £25 cap. (Tiered logic is a JPMC favourite.)
  4. Best-execution router — given quotes from N venues, route an order to the best price; if quantity exceeds the best venue, sweep across venues.
  5. Stop-loss / take-profit trigger — given a stream of price ticks, fire alerts when a configured threshold is crossed.
  6. Rate limiter — N trades per client per second (sliding window). Pure business rule, no algorithms-y tricks.
  7. Currency conversion graph — given a set of FX rates (A→B, rate), compute the converted amount for any pair via cheapest path. (BFS over a small graph — simple, business-flavoured.)
  8. Dividend distribution — given a list of shareholders and a total dividend amount, distribute pro-rata, handling rounding so that the sum equals the input exactly.

7. Domain Vocabulary Cheat Sheet

Knowing these terms saves clarification time and signals you understand the business.

Term Meaning
Bid / Ask Highest buy price / lowest sell price on the book
Spread Ask − Bid
Limit order Execute only at the specified price or better
Market order Execute immediately at the best available price
IOC Immediate-or-cancel: fill what you can now, cancel the rest
FOK Fill-or-kill: fill the whole quantity now or cancel everything
GTC Good-till-cancelled: rests on the book until cancelled
Resting order An unfilled order sitting on the book
Aggressive / passive Aggressive crosses the spread; passive rests
Partial fill An order filled in pieces against multiple counterparties
Position Net quantity held (signed)
Long / short Positive / negative position
Realized vs unrealized P&L Locked-in vs mark-to-market
T+2 settlement Cash and securities exchange 2 business days after the trade
Self-trade prevention Don't let a client trade against themselves
Price-time priority Best price first, then earliest order at that price

8. HackerRank IDE Practical Tips

  • Confirm you can run code first — write a main that prints "hello" and run it before getting deep.
  • Keep everything in one file unless they say otherwise; nested static classes are fine and idiomatic in HackerRank.
  • Use System.out.println for quick smoke tests; promote to assertions later if time allows.
  • Java import shortcut: import java.util.*; is acceptable in interviews.
  • If the IDE lags, type slower — don't fight it.
  • Watch the timer but don't stare at it. At ~10 mins left, summarize what's done and what you'd do next.

9. Final 5-Minute Wrap-up Script

When time runs out, the engineer asks "how would you have solved it if you had more time?" Have a confident answer ready. Template:

"I'd extend this in three ways. First, correctness — I'd add property-based tests around invariants like 'total bought quantity equals total sold quantity across all trades'. Second, production-readiness — I'd switch to BigDecimal for prices, add structured logging on every state transition, and emit metrics for trade count and book depth. Third, scalability — partition by ticker, event-source state, and put the matching engine on a single dedicated thread per partition (LMAX-style) so we get deterministic ordering without locks."

That answer alone hits Java, testing, observability, distributed systems, and messaging — every "preferred qualification" on the JD.


10. Day-Of Checklist

  • [ ] Quiet room, water, notepad.
  • [ ] Test camera + mic 10 minutes early.
  • [ ] HackerRank link opens — log in early.
  • [ ] Have the JD open in a tab to refer to if asked "why this team?".
  • [ ] Open a blank scratch file for diagrams (orderbook sketch, position table).
  • [ ] First 60 seconds: greet, confirm format ("60 minutes, 1–2 problems, you'd like me to talk through my thinking — anything else I should know?"), then dive in.

You've got this. Talk more than you type.