Guide contents

Lesson 71 of 71

Low-Latency, Networking, Fintech, and Trading

Short answers for interview questions on sections 17 to 20 of the course: networking and IO, low-latency code, fintech correctness, and trading systems. The lesson ends with coding problems, design scenarios, spot-the-bug questions, and a rapid-fire round across the whole course. Code blocks run on JDK 25.

Group Lessons
IO and files Streams, Readers, and Files
Sockets and NIO TCP, UDP, Multicast, and the HTTP Client, NIO Channels and Selectors
Binary protocols Binary Protocols and Encoding
Low-latency code Allocation-Free and Cache-Friendly Code, Agrona, Aeron, SBE, and Chronicle
Fintech correctness Money, BigDecimal, and Rounding, Idempotency, Ledgers, and Reconciliation
Trading systems Market Data, Orders, and FIX, Project: Limit Order Book, Project: Market Data Handler, Project: Matching Engine

IO and files

1. What does InputStream.read(byte[]) return, and what is the common bug?

The number of bytes read, which may be fewer than the array length, or -1 at end of stream. Code that assumes a full read processes garbage at the end of the array. Loop until the count is reached, or use readNBytes.

2. Bytes or characters: where does a charset enter?

InputStream and OutputStream carry bytes. Reader and Writer carry characters, decoded or encoded with a Charset. A wrong charset produces wrong text, not an exception, so the charset is always passed explicitly.

3. Why buffer, and what is lost if a buffered writer is not closed?

Buffering groups many small reads or writes into fewer system calls; the default JDK buffer is 8192 bytes. Data still in an unflushed buffer is lost when the program exits. try-with-resources closes and flushes it.

4. Follow-up: what must be closed when using Files.lines?

The returned stream. Files.lines, Files.list, and Files.walk are lazy and hold an open file handle until the stream is closed.

5. What does FileChannel.transferTo give over a copy loop?

The copy runs without a user-space byte array, which the kernel can do as a direct file-to-socket or file-to-file copy. It can transfer fewer bytes than asked, so callers loop on the returned count.

6. Why do low-latency and security reviews reject Java serialization?

Deserialization creates objects of classes named by the input and skips constructors of non-record classes, so untrusted input can reach dangerous code paths. It is also slow and verbose. Explicit binary formats or an ObjectInputFilter allow list replace it.

Sockets and NIO

7. TCP or UDP for a market data feed, and what does each force the application to do?

TCP is a reliable ordered byte stream with no message boundaries, so the application frames messages. UDP keeps datagram boundaries but can lose, duplicate, and reorder, so the application numbers messages and recovers gaps. Exchange feeds use UDP multicast for fan-out and a TCP or request channel for recovery.

8. What is TCP_NODELAY and why do trading clients set it?

It turns off Nagle's algorithm, which holds small writes until earlier data is acknowledged. Combined with delayed ACK on the peer, Nagle can delay a small order by tens of milliseconds. It defaults to false and is set per socket, on both sides.

9. Follow-up: are SO_RCVBUF and SO_SNDBUF exact?

No. They are requests; Linux doubles and caps them. Programs read the value back. The receive buffer is set before connecting so TCP window scaling can use it.

10. A client sees connect delays of several seconds under load. What is the cause?

The accept queue (backlog) is full because the server does not call accept fast enough. The kernel drops new SYNs, and clients retransmit after a timeout of about a second, then longer. Fixes: accept faster, raise the backlog and the kernel limit, add acceptor threads.

11. Why do large UDP datagrams hurt?

A datagram above the path MTU is fragmented in IP. If any fragment is lost, the whole datagram is lost. Feeds keep datagrams below the MTU.

12. What does a selector loop return from non-blocking calls?

accept returns null when no connection is pending. read returns 0 when no data is available and -1 at end of stream. write may write fewer bytes than asked, including 0.

13. Why does a selector loop spin at 100% CPU with no traffic?

Usually OP_WRITE is registered permanently. A socket is writable almost always, so select returns at once. Register OP_WRITE only while there is unsent data and remove it when the buffer drains. The other cause is not removing keys from selectedKeys(), which the selector never clears.

14. Where does per-connection state live in a selector server?

In the SelectionKey attachment: the read buffer with partial frames and the queue of unsent data. Partial reads are assembled with compact.

15. Explain the Netty threading model in two sentences.

Each channel is bound to one event loop thread, which runs every handler in that channel's pipeline, so handlers need no locks. Blocking an event loop stalls every channel on it, so blocking work goes to another executor.

16. What does a shared HttpClient need?

One instance per application, because it is thread-safe and pools connections. Every request needs a connect timeout and a request timeout; without them a dead peer holds the caller indefinitely.

Binary protocols

17. Why binary over text for order and market data messages?

Fixed-width binary fields sit at known offsets and are read without scanning, parsing, or allocation. Text has variable length and needs parsing per field.

18. What is endianness, and what does ByteBuffer default to?

The byte order of a multi-byte number. ByteBuffer defaults to big-endian; SBE defaults to little-endian, which matches x86. The order is part of the protocol specification; a mismatch silently reads another number.

19. How does a varint work, and why ZigZag?

A varint stores 7 bits per byte and uses the top bit as a "more bytes follow" flag, so small values take one byte. A negative long has the top bits set and takes ten bytes. ZigZag maps 0, -1, 1, -2, ... to 0, 1, 2, 3, ..., so small magnitudes stay short. The cost: fields lose fixed offsets.

20. What is a flyweight decoder?

An object that wraps a buffer and an offset and reads fields with absolute get calls at fixed offsets. One instance is re-wrapped per message, so decoding allocates nothing. It is valid only while the buffer still holds that message.

21. How does SBE support schema evolution?

New fields are appended to the end of a message block. The header carries blockLength, templateId, schemaId, and version; an old decoder uses blockLength to skip fields it does not know.

22. What does a sequence number on each message give?

A gap (higher than expected) reveals loss; the receiver requests a resend or waits for a snapshot. A lower number reveals a duplicate, which is dropped. A/B line arbitration uses the same check to take the first copy of each message from two redundant feeds.

Low-latency code

23. How is allocation per operation measured?

com.sun.management.ThreadMXBean.getThreadAllocatedBytes(threadId) before and after a warmed-up loop, divided by the iteration count. The goal on a hot path is 0 bytes per operation in steady state.

24. Why does allocation matter more for p99.9 than for the mean?

Allocation fills the young generation; each young collection pauses or slows some requests. Fewer allocated bytes means fewer collections, which removes the outliers rather than shifting the average.

25. Follow-up: escape analysis removes allocations. Why still measure?

Escape analysis applies only when the object does not escape and the code is inlined. A change in inlining, a larger method, or a different call site removes the optimization without warning. Measurement catches that.

26. Where does boxing hide on a hot path?

Map<Long, Long> keys and values, merge arguments, iterators, varargs, and lambdas that capture. Long.valueOf caches only -128..127. Primitive collections (fastutil Int2LongOpenHashMap.addTo) update array slots in place without allocation.

27. Object pools: benefits and risks?

A pool reuses long-lived holders and removes per-message allocation. The risks are use-after-release (a stale reference reads another order's data) and leaks (objects never returned). Strict ownership rules are required.

28. How is logging done on an allocation-free path?

Append values into a reused, pre-sized StringBuilder or byte buffer, and hand the bytes to a background writer. String.format and string concatenation allocate on every call.

29. What does busy spinning buy and cost?

A spinning thread with Thread.onSpinWait() (a PAUSE hint on x86) wakes with no scheduler latency. It costs a full core. Thread affinity (OpenHFT Affinity or taskset, since the JDK has no API) and isolated cores keep the OS from running other work there.

30. Why can a single-writer variable use lazySet instead of CAS?

CAS resolves races between writers. With exactly one writer there is no race, so a release store (lazySet, setRelease) is enough to publish the value to readers, and it avoids the full fence of CAS.

31. What are Agrona's OneToOneRingBuffer and ManyToOneRingBuffer?

SPSC and MPSC ring buffers of variable-length records, each with an 8-byte header, stored in one buffer of power-of-two capacity plus a trailer of counters. write returns false when full; the caller picks the back-pressure policy. tryClaim, commit, and abort let the producer encode in place without a copy.

32. What is an idle strategy?

The policy for a thread with no work: busy spin, yield, park, or backoff (spin, then yield, then park). It trades CPU use for wake-up latency. AgentRunner calls doWork() in a loop and passes the work count to the idle strategy.

33. Aeron, SBE, Chronicle Queue, Netty: what is each for?

Aeron: low-latency messaging over UDP and IPC through a media driver, with back pressure as negative return codes. SBE: generated flyweight codecs. Chronicle Queue: a memory-mapped persisted log with independent tailers; Chronicle Map: an off-heap shared map. Netty: TCP gateways such as FIX sessions.

Money and BigDecimal

34. Why not double for money?

double is binary floating point and cannot represent most decimal fractions, so 0.1 + 0.2 is not 0.3. Errors accumulate over many operations. Money uses BigDecimal or long minor units.

35. Which BigDecimal constructor is wrong?

new BigDecimal(0.1), which copies the exact binary value of the double. new BigDecimal("0.1") and BigDecimal.valueOf(0.1) (which goes through Double.toString) give 0.1.

36. Explain scale and precision.

Value = unscaled value × 10^-scale. Precision is the number of digits in the unscaled value. add keeps the larger scale; multiply adds the scales.

37. equals versus compareTo on BigDecimal?

equals compares value and scale, so 2.0 and 2.00 differ. compareTo compares the value only. A HashSet of both holds two elements; a TreeSet holds one.

38. Why does divide throw?

divide(BigDecimal) without a scale or MathContext throws ArithmeticException when the result does not terminate, such as 1/3. Pass a scale and a RoundingMode.

39. Which rounding mode for money?

The one a contract or regulation names. Without one, HALF_EVEN (banker's rounding) avoids the upward bias of HALF_UP when many values are rounded. setScale without a mode throws if digits would be lost.

40. Follow-up: how many minor units does a currency have?

Currency.getDefaultFractionDigits(): JPY 0, USD 2, BHD 3. Hard-coding 2 breaks for those currencies.

41. When is long minor units better than BigDecimal?

On hot paths: a long does not allocate and arithmetic is a single instruction. Overflow is guarded with Math.addExact and Math.multiplyExact, which throw instead of wrapping.

Idempotency and ledgers

42. What is an idempotency key?

A client-generated id per logical request. The server stores the result under the key and returns the stored result on a retry, with no new effect. A reuse of the key with a different request body is rejected.

43. Why is if (!map.containsKey(k)) map.put(k, ...) wrong on a ConcurrentHashMap?

Check-then-act is two operations. Two threads can both see the key missing and both run the payment. putIfAbsent or computeIfAbsent makes one caller win.

44. Follow-up: how do concurrent retries wait for the first result?

Store a CompletableFuture per key with putIfAbsent. The winner completes it; the others join it. On failure, remove(key, future) clears the entry so a later retry can run.

45. Is exactly-once delivery possible?

In practice it is at-least-once delivery plus deduplication. The dedup record (processed message id) and the effect are committed in one transaction or under one lock, so a crash between them cannot apply the effect twice or record it without applying it.

46. What is double-entry bookkeeping as code?

A transaction is a list of postings whose amounts sum to zero; a transaction that does not is rejected at construction. The sum of all balances is then always zero.

47. How is a wrong ledger entry corrected?

The journal is append-only. A correction is a reversal entry plus a new correct entry. Balances are derived from the journal, so history and audit stay intact.

48. How do sealed types help a payment state machine?

The states are a sealed interface of records, so a switch over them is checked for exhaustiveness at compile time. Duplicate events for a state already reached are no-ops.

49. What does reconciliation report?

Per line: missing on our side, missing on their side, and amount mismatches. It walks sorted ids from both ledgers. Comparing only totals hides offsetting errors.

Trading domain and FIX

50. Why are prices integer ticks?

Integer arithmetic is exact, comparisons are cheap, and a long creates no objects. The tick size converts to a decimal price at the edges.

51. Define spread and mid. Can a bid at or above the ask persist?

Spread = ask − bid; mid = (bid + ask) / 2. A crossing bid cannot persist on a venue, because the matching engine trades it immediately.

52. IOC versus FOK?

IOC fills what it can at once and cancels the rest. FOK fills completely at once or cancels everything.

53. Which order states are terminal?

Filled, cancelled, and rejected. Pending new, new, and partially filled are live.

54. Follow-up: why keep cumulative notional rather than a running average price?

Recomputing an average on every fill rounds each time, and the error drifts. Average price = cumulative notional / cumulative quantity, computed when needed.

55. What do pre-trade risk checks do?

Before an order leaves the firm: quantity limits, price collars around a reference price, and position limits that count open orders at full size, as if they all fill.

56. Describe the FIX message layout.

tag=value fields separated by SOH (0x01). 8 (BeginString), 9 (BodyLength), and 35 (MsgType) come first; 10 (CheckSum) is last. BodyLength counts bytes after the 9= field up to the 10= field. CheckSum is the sum of all bytes before 10= modulo 256, as three digits.

57. Follow-up: does the FIX checksum protect against tampering?

No. It detects accidental corruption only. Integrity and authentication come from TLS or the network.

58. What happens on a FIX sequence gap?

The receiver sees a MsgSeqNum higher than expected and sends a ResendRequest. The peer resends or sends a SequenceReset for administrative messages.

Order books, market data, and matching

59. Design a limit order book. Simplest correct version first.

Two sides of price levels; each level is a FIFO queue of orders (price-time priority). Simplest: TreeMap<Long, ArrayDeque<Order>> per side with a reverse comparator for bids, and a map from order id to order. firstKey() is the best price. Operations are O(log L).

60. Follow-up: how to make it O(1)?

Arrays of levels indexed by price - minPrice for a bounded price band, intrusive doubly linked lists of orders so cancel unlinks in O(1), a cached best index that moves by a short scan when a level empties, and a running quantity per level for depth.

61. Follow-up: does a modify keep queue priority?

Reducing quantity keeps priority. Increasing quantity or changing the price loses it: the order goes to the back of its (new) level.

62. Why must a handler never apply an incremental after a gap?

The lost messages may have changed levels that later messages do not mention. The book is unknown until a snapshot. Recovery buffers incrementals, loads the snapshot, and replays only messages with a sequence number above the snapshot's.

63. Follow-up: LEVEL messages versus delta messages on replay?

A LEVEL message carries the new total for a price, so applying it twice gives the same result (idempotent). A delta adds or removes quantity, so applying one the snapshot already contains double-counts. That is why replay skips messages at or below the snapshot sequence.

64. What is the trade price when orders cross?

The resting (maker) order's price. The incoming (taker) order receives any price improvement.

65. How are market orders implemented?

As limit orders at the extreme price that never rest; the remainder is cancelled. Venues add price bands so a market order cannot sweep an empty book.

66. Why must FOK check before trading?

Trades cannot be undone. The engine sums available quantity at acceptable prices first, and only then trades or cancels.

67. Why does a matching engine run on one thread?

One thread owns the book: no locks, and a single total order of events, which makes the engine deterministic. Other threads hand commands over through a ring buffer of pre-allocated slots.

68. What does determinism forbid inside the engine?

Reading clocks, random sources, and hash iteration order. The sequencer stamps times into commands before they reach the engine.

69. How does a matching engine recover from a crash?

It journals each input command before applying it. On restart it replays the journal and produces identical events. Downstream consumers deduplicate re-emitted events by sequence number, and periodic snapshots bound replay time.

Coding problems

70. Encode and decode a ZigZag varint.

Java
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.HexFormat;

public class Varint {
    static void writeVarLong(ByteArrayOutputStream out, long value) {
        long v = (value << 1) ^ (value >> 63);          // ZigZag
        while ((v & ~0x7FL) != 0) {
            out.write((int) ((v & 0x7F) | 0x80));        // 7 bits + continuation bit
            v >>>= 7;
        }
        out.write((int) v);
    }

    static long readVarLong(ByteBuffer in) {
        long v = 0;
        int shift = 0;
        while (true) {
            byte b = in.get();
            v |= (long) (b & 0x7F) << shift;
            if ((b & 0x80) == 0) break;
            shift += 7;
            if (shift > 63) throw new IllegalArgumentException("varint too long");
        }
        return (v >>> 1) ^ -(v & 1);                    // undo ZigZag
    }

    public static void main(String[] args) {
        long[] values = {0, -1, 1, 300, -300, Long.MAX_VALUE, Long.MIN_VALUE};
        for (long value : values) {
            var out = new ByteArrayOutputStream();
            writeVarLong(out, value);
            byte[] bytes = out.toByteArray();
            long back = readVarLong(ByteBuffer.wrap(bytes));
            System.out.println(value + " -> " + HexFormat.of().formatHex(bytes)
                    + " (" + bytes.length + " bytes) roundtrip=" + (back == value));
        }
    }
}
Output
0 -> 00 (1 bytes) roundtrip=true
-1 -> 01 (1 bytes) roundtrip=true
1 -> 02 (1 bytes) roundtrip=true
300 -> d804 (2 bytes) roundtrip=true
-300 -> d704 (2 bytes) roundtrip=true
9223372036854775807 -> feffffffffffffffff01 (10 bytes) roundtrip=true
-9223372036854775808 -> ffffffffffffffffff01 (10 bytes) roundtrip=true

>> (arithmetic shift) in ZigZag spreads the sign bit; >>> in the loop shifts in zeros so the loop ends. The decoder caps the shift so a corrupt input cannot loop forever.

71. Compute and validate a FIX checksum and body length.

Java
import java.nio.charset.StandardCharsets;

public class FixChecksum {
    static final char SOH = '\u0001';

    static String build(String body) {
        String head = "8=FIX.4.4" + SOH + "9=" + body.getBytes(StandardCharsets.US_ASCII).length + SOH;
        String withoutTrailer = head + body;
        return withoutTrailer + "10=" + checksum(withoutTrailer) + SOH;
    }

    static String checksum(String s) {
        int sum = 0;
        for (byte b : s.getBytes(StandardCharsets.US_ASCII)) sum += b & 0xFF;
        return String.format("%03d", sum % 256);
    }

    static boolean valid(String msg) {
        int idx = msg.lastIndexOf(SOH + "10=") + 1;
        String expected = msg.substring(idx + 3, idx + 6);
        return checksum(msg.substring(0, idx)).equals(expected);
    }

    public static void main(String[] args) {
        String body = "35=D" + SOH + "34=7" + SOH + "11=ORD1" + SOH + "55=ABC" + SOH
                + "54=1" + SOH + "38=100" + SOH + "44=10.25" + SOH;
        String msg = build(body);
        System.out.println(msg.replace(SOH, '|'));
        System.out.println("valid: " + valid(msg));
        String corrupted = msg.replace("38=100", "38=900");
        System.out.println("corrupted valid: " + valid(corrupted));
    }
}
Output
8=FIX.4.4|9=46|35=D|34=7|11=ORD1|55=ABC|54=1|38=100|44=10.25|10=154|
valid: true
corrupted valid: false

The checksum covers every byte up to and including the SOH before 10=. The body length counts the bytes after the 9= field up to that same point. Production code computes both over the byte buffer without building strings.

72. Split an amount into N parts or by weights without losing cents.

Java
import java.util.Arrays;

public class Allocate {
    static long[] split(long totalCents, int parts) {
        long[] out = new long[parts];
        long base = totalCents / parts, remainder = totalCents % parts;
        for (int i = 0; i < parts; i++) out[i] = base + (i < remainder ? 1 : 0);
        return out;
    }

    static long[] byWeights(long totalCents, long[] weights) {
        long weightSum = Arrays.stream(weights).sum();
        long[] out = new long[weights.length];
        long allocated = 0;
        for (int i = 0; i < weights.length; i++) {
            out[i] = Math.multiplyExact(totalCents, weights[i]) / weightSum;  // round down
            allocated += out[i];
        }
        for (int i = 0; allocated < totalCents; i = (i + 1) % weights.length) {
            out[i]++;                                                          // distribute leftover cents
            allocated++;
        }
        return out;
    }

    public static void main(String[] args) {
        long[] three = split(10_000, 3);
        System.out.println(Arrays.toString(three) + " sum=" + Arrays.stream(three).sum());
        long[] weighted = byWeights(10_000, new long[] {1, 1, 1});
        System.out.println(Arrays.toString(weighted) + " sum=" + Arrays.stream(weighted).sum());
        long[] fees = byWeights(101, new long[] {50, 30, 20});
        System.out.println(Arrays.toString(fees) + " sum=" + Arrays.stream(fees).sum());
        System.out.println("double 0.1 + 0.2 == 0.3: " + (0.1 + 0.2 == 0.3));
    }
}
Output
[3334, 3333, 3333] sum=10000
[3334, 3333, 3333] sum=10000
[51, 30, 20] sum=101
double 0.1 + 0.2 == 0.3: false

Each part rounds down, then the leftover cents go one at a time to parts in a fixed order. The order is part of the specification (largest weight first, or first account), so the result is reproducible.

73. Write an idempotent payment handler that is safe under concurrent retries.

Java
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class IdempotentHandler {
    record Request(String key, long amountCents) {}
    record Result(String paymentId, long amountCents) {}

    private final ConcurrentHashMap<String, CompletableFuture<Result>> results = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, Request> bodies = new ConcurrentHashMap<>();
    final AtomicInteger charges = new AtomicInteger();

    Result handle(Request req) {
        Request first = bodies.putIfAbsent(req.key(), req);
        if (first != null && !first.equals(req)) throw new IllegalArgumentException("key reused with different body");
        var mine = new CompletableFuture<Result>();
        var existing = results.putIfAbsent(req.key(), mine);
        if (existing != null) return existing.join();       // a retry waits for the first attempt
        try {
            int n = charges.incrementAndGet();                // the side effect, run once
            Result r = new Result("pay-" + n, req.amountCents());
            mine.complete(r);
            return r;
        } catch (RuntimeException e) {
            results.remove(req.key(), mine);                  // let a later retry run again
            bodies.remove(req.key(), req);
            mine.completeExceptionally(e);
            throw e;
        }
    }

    public static void main(String[] args) throws Exception {
        var handler = new IdempotentHandler();
        var req = new Request("k-1", 2_500);
        var barrier = new CyclicBarrier(8);
        try (ExecutorService pool = Executors.newFixedThreadPool(8)) {
            var futures = new java.util.ArrayList<Future<Result>>();
            for (int i = 0; i < 8; i++) futures.add(pool.submit(() -> { barrier.await(); return handler.handle(req); }));
            var ids = new java.util.TreeSet<String>();
            for (var f : futures) ids.add(f.get().paymentId());
            System.out.println("distinct results: " + ids);
        }
        System.out.println("charges: " + handler.charges.get());
        try {
            handler.handle(new Request("k-1", 9_999));
        } catch (IllegalArgumentException e) {
            System.out.println("rejected: " + e.getMessage());
        }
    }
}
Output
distinct results: [pay-1]
charges: 1
rejected: key reused with different body

Eight concurrent calls with the same key produce one charge. In a real service the key, the request hash, and the result live in the database, written in the same transaction as the payment.

74. Write the core of a price-time matcher for an incoming buy limit order.

Java
import java.util.*;

public class Matcher {
    static final class Order {
        final long id, price; long qty;
        Order(long id, long price, long qty) { this.id = id; this.price = price; this.qty = qty; }
    }

    final TreeMap<Long, ArrayDeque<Order>> asks = new TreeMap<>();                        // lowest first
    final TreeMap<Long, ArrayDeque<Order>> bids = new TreeMap<>(Comparator.reverseOrder()); // highest first

    void restAsk(Order o) { asks.computeIfAbsent(o.price, p -> new ArrayDeque<>()).addLast(o); }

    void buyLimit(Order taker) {
        while (taker.qty > 0 && !asks.isEmpty() && asks.firstKey() <= taker.price) {
            ArrayDeque<Order> level = asks.firstEntry().getValue();
            Order maker = level.peekFirst();                   // time priority inside the level
            long fill = Math.min(taker.qty, maker.qty);
            System.out.println("trade taker=" + taker.id + " maker=" + maker.id + " qty=" + fill + " px=" + maker.price);
            taker.qty -= fill;
            maker.qty -= fill;
            if (maker.qty == 0) level.pollFirst();
            if (level.isEmpty()) asks.pollFirstEntry();
        }
        if (taker.qty > 0) {
            bids.computeIfAbsent(taker.price, p -> new ArrayDeque<>()).addLast(taker);   // DAY: rest the remainder
            System.out.println("rest bid id=" + taker.id + " qty=" + taker.qty + " px=" + taker.price);
        }
    }

    public static void main(String[] args) {
        var m = new Matcher();
        m.restAsk(new Order(1, 101, 50));
        m.restAsk(new Order(2, 100, 30));
        m.restAsk(new Order(3, 100, 40));   // same price, later in time than order 2
        m.restAsk(new Order(4, 103, 10));
        m.buyLimit(new Order(9, 101, 130));
        System.out.println("best ask=" + m.asks.firstKey() + " best bid=" + m.bids.firstKey());
    }
}
Output
trade taker=9 maker=2 qty=30 px=100
trade taker=9 maker=3 qty=40 px=100
trade taker=9 maker=1 qty=50 px=101
rest bid id=9 qty=10 px=101
best ask=103 best bid=101

Order 2 fills before order 3 (same price, earlier time), both before order 1 (worse price). Every trade prints the maker's price, so the taker that bid 101 gets 100 on part of its quantity. The remainder rests at 101; order 4 at 103 does not cross. IOC would cancel the remainder instead of resting it; FOK would first check that enough quantity exists at acceptable prices.

Scenario and design questions

75. A UDP market data handler loses packets during busy periods. How is it diagnosed and fixed?

  1. Confirm where loss happens: the handler's gap counter versus kernel counters (netstat -su receive buffer errors, NIC drop counters). Kernel or NIC drops mean the reader thread does not drain the socket fast enough.
  2. Make the receive path do less: the reader thread only copies datagrams into a ring buffer (Agrona OneToOneRingBuffer or a Disruptor), and a separate thread decodes and builds the book. Decoding uses flyweights and allocates nothing, so no young collection pauses the reader.
  3. Raise SO_RCVBUF (read the value back, since the kernel caps it) and the kernel maximum, so bursts are absorbed.
  4. Pin the reader to an isolated core with a busy-spin idle strategy.
  5. Correctness on loss: never apply an incremental after a gap. Mark the book stale, buffer, recover from a snapshot or the A/B line, and replay above the snapshot sequence. Publish gap and recovery counts as metrics.

76. After a release, p99.9 latency doubled while the median did not change. What is the investigation?

  1. Compare allocation per operation before and after (getThreadAllocatedBytes in a test, or JFR allocation events). A new boxing site, a lambda, a log statement with string concatenation, or an iterator commonly adds allocation, and allocation shows up in the tail first.
  2. Check GC logs for more frequent or longer young pauses, and safepoint logs for time-to-safepoint.
  3. Check JIT changes: a method that grew past the inlining limit, deoptimization storms, or missing warmup in the new code path. JFR and async-profiler show where the extra time is spent.
  4. Check the environment: changed JVM flags, container CPU limits, a moved thread pin, or a new co-located process.
  5. Fix the cause and add a test that fails on allocation per operation above 0 and a benchmark that reports p99.9, so the regression cannot return silently. Latency is compared as percentiles from an HdrHistogram with coordinated omission handled, not as averages.

77. Customers report duplicate payments. What is the root cause and the design fix?

Typical causes: a client retried after a timeout and the server had no idempotency key; the server checked "already paid?" and then charged, with two threads passing the check; or a consumer received a message twice (at-least-once) and the dedup record was written separately from the effect.

Fix:

  • Require an idempotency key per logical payment, generated by the client and reused on every retry.
  • Insert the key with a unique constraint (or putIfAbsent in memory) before charging, so one attempt wins; concurrent retries wait for the stored result.
  • Store the request hash with the key and reject reuse with a different body.
  • Commit the dedup record and the ledger entries in one transaction.
  • Correct the duplicates already made with reversal entries, never by deleting journal rows, and add reconciliation against the payment provider that reports per-line mismatches.

78. Design the threading of an order gateway from FIX session to matching engine.

Netty event loops terminate FIX sessions: decode, validate checksum, body length, and sequence numbers, and run pre-trade risk. Each session writes commands into a multi-producer ring buffer. A sequencer stamps sequence numbers and times and journals each command. One engine thread consumes, matches, and publishes execution reports and market data through outbound ring buffers. Nothing on the engine thread blocks, allocates, or reads the clock.

79. How is a matching engine made highly available?

The primary journals each command before applying it and replicates the journal to a standby, which applies the same commands deterministically and holds an identical book. On failover the standby continues from the last replicated sequence; clients and downstream consumers deduplicate by sequence number.

80. Two ledgers disagree by a total of zero. Is everything fine?

No. Offsetting errors (one payment missing, another duplicated with the same amount) sum to zero. Reconciliation compares per transaction id and reports missing-on-each-side and amount mismatches.

Spot the bug

81. Spot the bug.

Java
import java.io.*;

public class PartialRead {
    static class SlowStream extends InputStream {                  // returns at most 3 bytes per read
        private final byte[] data; private int pos;
        SlowStream(byte[] data) { this.data = data; }
        public int read() { return pos < data.length ? data[pos++] & 0xFF : -1; }
        public int read(byte[] b, int off, int len) {
            if (pos >= data.length) return -1;
            int n = Math.min(Math.min(len, 3), data.length - pos);
            System.arraycopy(data, pos, b, off, n); pos += n; return n;
        }
    }

    public static void main(String[] args) throws IOException {
        byte[] message = "ORDER:42".getBytes();
        byte[] buf = new byte[8];
        new SlowStream(message).read(buf);
        System.out.println("buggy: " + new String(buf).replace('\0', '.'));
        byte[] fixed = new SlowStream(message).readNBytes(8);
        System.out.println("fixed: " + new String(fixed));
    }
}
Output
buggy: ORD.....
fixed: ORDER:42

read(byte[]) returned 3, and the code ignored the count. Sockets return partial reads in the same way. readNBytes (or a loop on the returned count) reads the full message.

82. Spot the bug.

Java
import java.math.BigDecimal;
public class Price {
    public static void main(String[] args) {
        BigDecimal fee = new BigDecimal(0.1);
        System.out.println("buggy: " + fee);
        System.out.println("buggy equals new BigDecimal(\"0.1\"): " + fee.equals(new BigDecimal("0.1")));
        BigDecimal fixed = new BigDecimal("0.1");
        System.out.println("fixed: " + fixed + ", valueOf: " + BigDecimal.valueOf(0.1));
    }
}
Output
buggy: 0.1000000000000000055511151231257827021181583404541015625
buggy equals new BigDecimal("0.1"): false
fixed: 0.1, valueOf: 0.1

new BigDecimal(double) keeps the binary approximation with dozens of digits. Build money values from String or BigDecimal.valueOf.

83. Spot the bug.

Java
import java.math.BigDecimal;
import java.util.*;

public class DedupAmounts {
    public static void main(String[] args) {
        List<BigDecimal> amounts = List.of(new BigDecimal("2.0"), new BigDecimal("2.00"), new BigDecimal("2"));
        Set<BigDecimal> hash = new HashSet<>(amounts);
        Set<BigDecimal> tree = new TreeSet<>(amounts);
        System.out.println("HashSet size: " + hash.size());
        System.out.println("TreeSet size: " + tree.size());
        System.out.println("equals: " + amounts.get(0).equals(amounts.get(1))
                + ", compareTo == 0: " + (amounts.get(0).compareTo(amounts.get(1)) == 0));
    }
}
Output
HashSet size: 3
TreeSet size: 1
equals: false, compareTo == 0: true

BigDecimal.equals compares scale, so the hash-based set keeps three "different" amounts. Normalize scale (for example setScale(2) in a Money constructor) or compare with compareTo.

84. Spot the bug.

Java
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class WireOrder {
    public static void main(String[] args) {
        ByteBuffer sender = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);   // spec: little-endian
        sender.putLong(0, 1_000L);
        byte[] wire = sender.array();

        ByteBuffer buggy = ByteBuffer.wrap(wire);                                    // default order
        System.out.println("buggy qty: " + buggy.getLong(0));
        ByteBuffer fixed = ByteBuffer.wrap(wire).order(ByteOrder.LITTLE_ENDIAN);
        System.out.println("fixed qty: " + fixed.getLong(0));
        System.out.println("default order: " + ByteBuffer.wrap(wire).order());
    }
}
Output
buggy qty: -1728537831980138496
fixed qty: 1000
default order: BIG_ENDIAN

ByteBuffer.wrap starts big-endian. The order of a buffer is not copied from the sender; every decoder sets it from the protocol specification.

85. Spot the bug.

Java
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

public class FlipBug {
    public static void main(String[] args) {
        ByteBuffer buf = ByteBuffer.allocate(16);
        buf.put("PING".getBytes(StandardCharsets.US_ASCII));
        System.out.println("buggy remaining to send: " + buf.remaining());   // would send 12 zero bytes
        buf.flip();                                                          // limit = position, position = 0
        System.out.println("fixed remaining to send: " + buf.remaining());
        byte[] out = new byte[buf.remaining()];
        buf.get(out);
        System.out.println("sent: " + new String(out, StandardCharsets.US_ASCII));
    }
}
Output
buggy remaining to send: 12
fixed remaining to send: 4
sent: PING

After writing into a buffer, flip() sets the limit to the written length and the position to 0. Without it, a channel write sends the unused tail. After a partial write, compact() moves the unsent bytes to the front for the next fill.

86. Spot the bug.

Java
import java.util.*;

public class GapHandler {
    static final Map<Long, Long> book = new TreeMap<>();
    static long expected = 1;
    static boolean stale = false;

    static void buggy(long seq, long price, long deltaQty) {
        if (seq != expected) System.out.println("gap at " + seq + ", expected " + expected);
        book.merge(price, deltaQty, Long::sum);      // applied anyway
        expected = seq + 1;
    }

    static void fixed(long seq, long price, long deltaQty) {
        if (seq < expected) return;                  // duplicate
        if (seq > expected) stale = true;            // gap: wait for a snapshot
        expected = seq + 1;
        if (!stale) book.merge(price, deltaQty, Long::sum);
    }

    public static void main(String[] args) {
        long[][] feed = {{1, 100, 50}, {2, 100, -50}, {4, 100, -10}, {4, 100, -10}}; // seq 3 lost, seq 4 duplicated
        for (long[] m : feed) buggy(m[0], m[1], m[2]);
        System.out.println("buggy book: " + book);
        book.clear(); expected = 1;
        for (long[] m : feed) fixed(m[0], m[1], m[2]);
        System.out.println("fixed book: " + book + " stale=" + stale);
    }
}
Output
gap at 4, expected 3
gap at 4, expected 5
buggy book: {100=-20}
fixed book: {100=0} stale=true

The buggy handler logs the gap but applies the delta, and treats the duplicate as another gap and applies it too, so the book shows a negative quantity. Message 3 may have added quantity that message 4 removed. The fixed handler drops the duplicate, marks the book stale at the gap, and stops publishing until a snapshot arrives.

87. Spot the bug.

Java
public class FillPrice {
    public static void main(String[] args) {
        long[][] fills = {{100, 3}, {101, 3}, {102, 3}};      // {priceTicks, qty}
        long avgBuggy = 0, qty = 0;
        for (long[] f : fills) {
            avgBuggy = (avgBuggy * qty + f[0] * f[1]) / (qty + f[1]);   // integer running average
            qty += f[1];
        }
        long notional = 0, cum = 0;
        for (long[] f : fills) { notional += f[0] * f[1]; cum += f[1]; }
        System.out.println("buggy avg ticks: " + avgBuggy);
        System.out.println("fixed: notional=" + notional + " cum=" + cum
                + " avg=" + java.math.BigDecimal.valueOf(notional)
                .divide(java.math.BigDecimal.valueOf(cum), 4, java.math.RoundingMode.HALF_EVEN));
    }
}
Output
buggy avg ticks: 100
fixed: notional=909 cum=9 avg=101.0000

The running average truncates on every fill, and the error accumulates. Keeping cumulative notional and cumulative quantity and dividing once gives the exact average.

Mixed rapid-fire

88. record fields: final or not? Final; a record is a shallowly immutable carrier with generated equals, hashCode, toString, and accessors. (Records and Sealed Types)

89. Why must hashCode change when equals does? Equal objects must have equal hash codes, or hash collections lose them. (Object Contracts: equals, hashCode, and Comparison)

90. What does type erasure remove? Type arguments at runtime; List<String> and List<Integer> share one class. (Type Erasure)

91. PECS? Producer extends, consumer super. (Generic Types and Wildcards)

92. When does a HashMap bucket become a tree? When a put adds a node to a chain that already holds 8 entries (TREEIFY_THRESHOLD) and the table has at least 64 slots (MIN_TREEIFY_CAPACITY); with a smaller table it resizes instead. A tree shrinks back to a list at 6 entries. (HashMap Internals)

93. Are streams lazy? Intermediate operations run only when a terminal operation starts the pipeline. (Streams and Collectors)

94. What does volatile give? Visibility and ordering for that variable, not atomicity of read-modify-write. (Visibility and Reordering)

95. What is false sharing? Two threads writing different variables on the same cache line, which bounces the line between cores. (Cache Coherence, False Sharing, Branch Prediction, and NUMA)

96. Virtual threads for CPU-bound work? No benefit; they help with many blocking tasks, not computation. (Virtual Threads in Depth)

97. What is the ABA problem? A CAS succeeds because a value returned to its old value, although it changed in between. (Atomics and CAS)

98. Why does the Disruptor pre-allocate its ring? Entries are reused, so publishing allocates nothing and entries stay close in memory. (The Disruptor Pattern)

99. What is a safepoint? A point where all Java threads are stopped so the JVM can perform operations such as some GC phases and deoptimization. (Safepoints, Warmup, and Deoptimization)

100. What does escape analysis enable? Scalar replacement of objects that do not escape a compiled method, so they are not allocated. (Escape Analysis, Loop Optimizations, and Generated Assembly)

101. G1 versus ZGC in one line? G1 is the default with pause-time goals; ZGC does most work concurrently for very short pauses at some throughput cost. (Serial, Parallel, Epsilon, and GC Tuning)

102. Why use JMH instead of a timing loop? It handles warmup, dead-code elimination, and constant folding that make naive loops measure nothing. (Benchmarking with JMH)

103. What is coordinated omission? A load generator that waits for slow responses sends fewer requests during stalls and under-reports tail latency. (Measuring Latency: Percentiles and Coordinated Omission)

104. Direct ByteBuffer versus heap? Direct memory is outside the heap, so channel IO avoids a copy; allocation is slower and freed only through GC or a cleaner. (ByteBuffer, Direct Buffers, and Memory-Mapped Files)

105. What replaces Unsafe for off-heap memory? The Foreign Function and Memory API: Arena and MemorySegment. (The Foreign Function and Memory API)

106. Why are page faults a latency problem? The first touch of a page traps into the kernel; pre-touching memory (-XX:+AlwaysPreTouch) moves that cost to startup. (Virtual Memory, Page Faults, and perf)

107. How is a race made reproducible in a test? Force the interleaving with a latch or CyclicBarrier between check and act, and assert on the result. (Testcontainers and Testing Concurrent Code)

Interview points

  • Partial reads and partial writes are normal for streams and channels; code loops on the returned count and frames TCP messages.
  • TCP_NODELAY, socket buffer sizes, and backlog are the socket settings asked about most; each has a symptom (Nagle delays, drops, connect stalls).
  • Binary protocols use fixed offsets, an explicit byte order, sequence numbers, and appended fields for evolution; varints trade offsets for size.
  • Low-latency code is measured: 0 bytes per operation after warmup, percentiles from a histogram, and a pinned busy-spinning thread where the latency budget needs it.
  • Money uses BigDecimal from strings or long minor units, explicit rounding modes, compareTo for value comparison, and explicit remainder distribution.
  • Duplicate effects are prevented by an idempotency key, an atomic claim, and a dedup record committed with the effect.
  • Ledgers are double-entry, append-only, corrected by reversal, and reconciled per line.
  • A market data handler never applies updates past a gap; it recovers from a snapshot and replays above its sequence.
  • A matching engine is a single-threaded deterministic state machine with price-time priority, maker-price trades, and a journal for recovery.

Summary

  • Groups cover IO, sockets and NIO, binary protocols, low-latency code, money, idempotency and ledgers, the trading domain and FIX, and order books and matching.
  • Coding problems: ZigZag varint, FIX checksum and body length, cent-exact allocation, an idempotent handler, and a price-time matcher.
  • Scenarios: packet loss in a feed handler, a p99.9 regression, duplicate payments, gateway threading, engine availability, and reconciliation.
  • Spot-the-bug questions: ignored read counts, new BigDecimal(double), BigDecimal.equals in sets, byte order, a missing flip, applying updates past a gap, and a running average.
  • The rapid-fire round links one question to each major topic of the earlier sections.