Interview guides · AlphaGrep Securities
AlphaGrep Securities Software Engineer Interview Guide
AlphaGrep Securities Software Engineer interview guide: C++ internals, OS concurrency, order book design, DSA questions and a 7-day plan.
The 60-second brief
Candidates describe AlphaGrep Securities as a quantitative trading firm. Candidates report that the work covers order management systems, market data feeds, execution engines and high-frequency trading algorithms. Day-to-day work, as reported, spans new execution-engine features, latency-driven rewrites of older code and keeping data pipelines dependable, with each engineer owning a component end to end and occasionally pairing with quantitative researchers. The listed requirements are modern C++, data structures and algorithms, operating systems (threads, memory, processes), system design and low-latency programming. Python, FPGA or hardware-software co-design, and trading-industry experience are listed as nice to have.
This guide is for Software Engineer candidates preparing for AlphaGrep Securities. It covers the two reported stages (an online coding assessment, then technical rounds) and gives question-specific guidance for each of the twelve reported questions: C++ language internals, threads versus processes, heap implementation, the Python GIL, order book design, a two-stream order update class with expiry callbacks, time series forecasting, makefiles and link order, and the linked-list, graph and tree/DP problems. It ends with losing points, a 7-day plan, behavioral prompts and an FAQ.
Candidates report a short loop of about 2-4 weeks: an online coding assessment on a platform such as HackerRank, then a series of technical interviews held remotely or onsite. The number of technical rounds can vary with the team's needs.
Focus areas
- C++
- Operating Systems (OS) fundamentals
- Data Structures & Algorithms (DSA)
Overview
The reported questions fall into four groups. Language and runtime internals: the difference between a C++ reference and a pointer, why an empty struct has a nonzero size, how a heap works and what its operations cost, and the Python Global Interpreter Lock. Operating systems and tooling: threads versus processes, and how makefiles and library link order work. Design: an order book that matches buy and sell orders, a class that merges order updates from two streams and fires a timeout callback, and a time series forecasting algorithm. Algorithms: reversing a linked list, finding the friend group with the most shared interests, and general tree, graph and dynamic programming problems.
Most of these are questions about what happens beneath the language. A good answer to the reference-versus-pointer question mentions rebinding, nullability, lifetime extension of temporaries and how a compiler lowers a reference. A good answer to the empty-struct question covers distinct addresses and the empty base optimization. Candidates also report memory management (smart pointers, unique_ptr, manual heap allocation), templates, perfect forwarding, variadic templates, the memory layout of std::vector and writing custom iterators. Review each of these until you can explain the layout and the cost of each operation, not just the syntax.
The systems side, as reported, is OS, computer architecture and networking: caching, mutexes, spin locks and what the hardware does when your code runs. Tie those to the design questions. An order book is a data-structure problem first (sorted price levels, FIFO queues inside a level, an id index for cancels), and the two-stream class is a concurrency and ordering problem (sequence numbers, deduplication, a timer structure, never firing a callback while holding a lock). Prepare to state complexities and to explain where allocation, locking and cache misses would hurt latency.
Python appears in two ways: as a nice-to-have skill for scripting and data analysis, and through the reported GIL question. Prepare at least one solid Python answer about the GIL even if you plan to code in C++. FPGA work and trading experience are listed as nice to have, not required, so put your preparation time into C++ internals, OS fundamentals, the order book and core algorithms.
The AlphaGrep Securities interview process
Candidates report two stages over roughly 2-4 weeks: an online coding assessment that acts as a first filter, then technical interviews held remotely or onsite. The number of technical rounds can vary with the team's needs.
Online Coding Assessment
Candidates report that the process opens with an online coding assessment on a platform such as HackerRank, used to screen technical skill before any live interview. The reported questions are not tied to a specific stage, so prepare for this one by practising algorithm problems in an assessment-style editor. Treat it as a correctness exercise. Code in the language you are fastest and safest in (C++ and Python both appear in the listed requirements), parse input exactly as specified, and before submitting, test empty input, a single element, duplicates and the largest size the constraints allow. Choose the algorithm from the constraints: when n reaches the hundreds of thousands, an O(n^2) pass will not finish.
What to demonstrate
- Working code that passes the platform's test cases
- Choosing data structures and algorithms whose complexity fits the input constraints
- Edge cases in input handling and at boundary sizes
How to prepare
- Solve a mix of list, graph, tree and DP problems in a HackerRank-style editor where you read stdin and write stdout yourself.
- Write union-find (path compression plus union by size), BFS and an iterative DFS from memory so you can use them without looking anything up.
- Before each submission, run empty, single-element and maximum-size inputs yourself and check for overflow.
- Keep a C++ starting template ready: ios::sync_with_stdio(false) together with cin.tie(nullptr), '\n' instead of endl so output is not flushed on every line, long long for sums and counts, and an iterative DFS for deep inputs.
Technical Rounds
After the assessment, candidates report a series of technical interviews, remote or onsite, with C++ core subjects and system design reported as the main hurdles at this stage. The reported topics are C++, OS fundamentals, data structures and algorithms, trading and order-matching design, and Python language features. Candidates also report that their first approach gets challenged, and that project discussions serve as a starting point before the questions go deep. Prepare to explain how your code maps onto memory and the OS, to talk through trade-offs while you code, and to back every design choice with a complexity or a concrete latency cost such as an allocation, a lock or a cache miss.
What to demonstrate
- C++ internals: memory management and smart pointers, templates, perfect forwarding, variadic templates, vector layout, custom iterators
- OS and concurrency: threads and processes, mutexes versus spin locks, thread safety, caching
- Design of trading components such as an order matching system
- Explaining trade-offs out loud while writing code
How to prepare
- Write a one-page C++ internals sheet covering reference versus pointer, sizeof rules and padding, the empty base optimization, the std::vector layout and growth, and the costs of unique_ptr and shared_ptr. Then explain each item aloud.
- Build a small limit order book in C++ with add, cancel and match: a sorted map of price levels, an intrusive FIFO list per level, and a hash map from order id to node. State the complexity of every operation.
- Implement the two-stream order update class with sequence-number deduplication, a min-heap of expiries and a callback that fires outside the lock.
- Rehearse short spoken answers on threads versus processes, mutex versus spin lock, false sharing, TCP versus UDP multicast, the GIL, and make dependency and link order.
Where candidates lose points
Answering the C++ internals questions with syntax only: 'a reference cannot be null' or 'an empty struct is 1 byte', with no reason behind it.
For each language question, prepare the why and the mechanism: distinct object addresses and the empty base optimization for empty structs, reseating, lifetime extension and how compilers lower references, vector's three-pointer layout and iterator invalidation on reallocation. Practise saying each answer aloud until the mechanism comes before you have to be asked for it.
Designing the order book as one sorted container of orders, with cancels done by linear scan and no answer for partial fills.
Learn the standard shape: sorted price levels, a FIFO queue within each level, and an id-to-node hash map so cancel is O(1). Walk a crossing order through partial fills across several levels, and state the complexity of add, cancel, match and best-price lookup before talking about gateways or journaling.
Firing the expiry callback while holding the lock in the two-stream class, or ignoring sequence numbers so duplicate and stale updates overwrite good prices.
Keep a last-applied sequence number per order and drop anything not newer than it. Collect expired orders under the lock, then release it before invoking callbacks. Use a monotonic clock and a heap or multimap keyed by expiry, and say how a refreshed order invalidates its old timer entry.
Claiming the GIL makes Python code thread-safe, or answering threads versus processes without saying what is shared.
State that the GIL serialises bytecode execution, not your read-modify-write sequences, so counter += 1 still needs a lock even if a quick test happens to print the right total. For threads versus processes, list exactly what is shared (address space, heap, file descriptors) and what is private (stack, registers, TLS), then give costs and the IPC options.
Diving into code on the friends-and-interests problem without settling what 'group' and 'in common' mean, or submitting assessment code that fails on empty or deep inputs.
Spend the first minutes on definitions and tie-breaks, then commit to connected components plus intersection (or say which variant you chose). On the assessment, run empty, single-element and maximum-size inputs yourself, and use iterative traversal where recursion depth could reach n.
Questions & practice
What is the difference between a reference and a pointer in C++?
Medium · Technical C++ and Systems Programming
Approach
- Start with the semantics. A reference is an alias that must be bound when it is created and can never be reseated. A pointer is an object that holds an address: it can be null, reassigned, incremented and pointed to by another pointer.
- Cover the consequences: no null references in well-formed code (you only get one by dereferencing a null pointer, which is undefined behavior), no reference arithmetic, and sizeof(ref) reports the referent's size while sizeof(ptr) is the pointer size.
- Explain lowering. When a reference needs storage (as a member or a parameter that is not inlined), compilers usually implement it as a pointer, so the runtime cost is the same. The differences are guarantees the compiler enforces, not speed.
- Mention lifetime and value categories. Binding a const T& (or a T&&) to a temporary extends the temporary's lifetime. Rvalue references enable move semantics, and in a template T&& is a forwarding reference used with std::forward.
- End with the usage rule: references for non-null, non-reseatable parameters and operator overloads; pointers (or std::optional / smart pointers) for optional, reseatable or owning cases. Warn that a reference can still dangle if it outlives its object.
Follow-up
- Can you put references in a std::vector? If not, what do you use instead (std::reference_wrapper)?
- What is sizeof a struct that holds a single int& member on a 64-bit system, and why?
- What happens if you return a reference to a local variable?
Can you explain the difference between threads and processes?
Medium · Technical C++ and Systems Programming
Approach
- Give the core split. A process has its own virtual address space, page tables, file descriptor table and credentials. Threads in one process share the address space, heap, globals and open files, but each has its own stack, registers and thread-local storage.
- Compare costs. Creating a process (fork with copy-on-write) is more expensive than creating a thread. Switching between threads of the same process skips the address-space switch, so TLB and cache state survive better. On Linux both are tasks created by clone() with different sharing flags.
- Compare isolation and communication. A crash or memory corruption in one thread brings down the whole process. Processes stay isolated but need IPC: pipes, sockets, or shared memory via shm_open/mmap. Threads communicate through memory, so you need mutexes, atomics and memory ordering.
- Tie it to low-latency work: pin hot threads to cores, keep each thread's mutable data on separate cache lines to avoid false sharing, and choose a spin lock only when critical sections are very short and the thread owns its core, otherwise a mutex that sleeps.
Follow-up
- What happens when a multithreaded process calls fork()? Which threads exist in the child, and what about mutexes held by other threads?
- How would you share a market data feed between two processes with minimal latency?
- When is a spin lock worse than a mutex?
Why does an empty struct have a specific size in C++?
Medium · Technical C++ and Systems Programming
Approach
- Say the rule: every complete object has sizeof at least 1, so an empty struct has size 1 on common compilers. The reason is that distinct objects must have distinct addresses, and pointer arithmetic over an array of empty structs needs a nonzero stride.
- Contrast with C, where an empty struct is not standard (GCC accepts it as an extension with size 0). This difference shows you know why C++ chose its rule.
- Explain the empty base optimization: an empty base class subobject may take zero bytes, so struct D : Empty { int x; } is usually sizeof(int). It does not apply when the empty base has the same type as the first member, since the two would share an address.
- Name the usual library technique: standard library types store stateless allocators, comparators and unique_ptr deleters for free through EBO, typically via a compressed pair or tuple (libstdc++ and MSVC still do this). C++20 [[no_unique_address]] lets an empty member take no space as well, but libc++ adopted it only recently and MSVC ignores the standard attribute (it has its own msvc:: spelling).
- Extend to padding and alignment if asked: struct { char c; int i; } is typically 8 bytes. Ordering members from largest to smallest alignment reduces padding.
Follow-up
- What is sizeof a class with no data members but one virtual function, and why?
- How does std::unique_ptr with a stateless deleter stay the same size as a raw pointer?
- Reorder the members of a struct you are given to minimise its size and explain the result.
How do you implement a heap, and what are the complexities of insert, delete, and find operations?
Medium · Technical C++ and Systems Programming
Approach
- Use an array-backed binary heap. For index i (0-based) the children are 2i+1 and 2i+2 and the parent is (i-1)/2. The heap property is that every parent compares no worse than its children.
- Insert: append, then sift up while the node beats its parent, O(log n). Extract-top: move the last element to the root, pop the back, sift down by swapping with the better child, O(log n). Peek is O(1).
- Find on an arbitrary value is O(n), because the heap has no ordering between siblings. Deleting an arbitrary element is O(log n) only if you know its index, which requires a position map (an indexed heap). Without one, use lazy deletion: mark the element and skip it when it surfaces.
- Building a heap from n elements bottom-up (heapify from n/2-1 down to 0) costs O(n), not O(n log n), because most nodes sit near the bottom and sift down only a short distance.
- In C++, mention std::priority_queue and make_heap/push_heap/pop_heap, the comparator direction (std::less gives a max-heap), and edge cases: popping an empty heap, duplicate keys, and a comparator that is not a strict weak ordering.
Follow-up
- Why is bottom-up heap construction O(n)? Walk through the sum.
- How would you support cancelling an order by id in a heap of resting orders?
- Would a 4-ary heap be faster than a binary heap in practice, and why?
Explain the Global Interpreter Lock (GIL) in Python and its impact on performance.
Medium · Technical C++ and Systems Programming
Approach
- Define it. In CPython, the GIL is a mutex that lets only one thread execute Python bytecode at a time. It protects interpreter state and keeps reference counting simple and fast without per-object locks.
- Impact: CPU-bound pure-Python threads do not scale across cores and can slow down from contention. I/O-bound threads still help, because blocking calls release the GIL. C extensions such as NumPy can release it during heavy native work.
- Explain the switching: the interpreter asks the running thread to drop the GIL after a switch interval (sys.getswitchinterval, 5 ms by default) or at a blocking call. In CPython 3.10+ the drop request is only honoured at certain points such as backward jumps and calls, which is why a tight counter += 1 loop rarely shows a lost update in practice.
- Workarounds: multiprocessing or ProcessPoolExecutor for CPU-bound work (at the cost of pickling and memory), asyncio for many concurrent I/O tasks, and native extensions that release the GIL (pybind11 gil_scoped_release). Mention that Python 3.13 ships an optional free-threaded build (PEP 703).
- Correct a common error: the GIL does not make your code thread-safe. counter += 1 is a separate load, add and store, so the race is allowed by the language even when a given CPython build hides it, and on the free-threaded build it shows up readily. Shared state still needs a threading.Lock.
Follow-up
- Is list.append safe to call from several threads? Is checking a dict and then inserting into it safe?
- You have a C++ pricing library called from Python threads. How do you get real parallelism?
- Why can multiprocessing be slower than a single thread for small tasks?
Reverse a linked list.
Medium · Algorithms and Data Structures
Approach
- Iterative solution: keep prev = nullptr and curr = head. Each step saves next = curr->next, points curr->next at prev, then advances prev and curr. Return prev as the new head. O(n) time, O(1) extra space.
- Recursive solution: reverse the rest, then set head->next->next = head and head->next = nullptr. It is also O(n) but uses O(n) stack, which can overflow on a long list. Say so, and prefer the iterative version.
- Test the edge cases aloud: an empty list, a single node and two nodes. Make sure the old head's next ends up null so the result has no cycle.
- In C++, decide who owns the nodes. With raw pointers this is just relinking. With std::unique_ptr links, use std::move carefully at each step, and be aware that destroying a long unique_ptr chain recursively can itself overflow the stack.
Follow-up
- Reverse the list in groups of k, leaving any final partial group as it is.
- Check whether a linked list is a palindrome in O(1) extra space.
- How does the code change for a doubly-linked list?
Given a graph of friends with different interests, determine which group has the most interests in common.
Medium · Algorithms and Data Structures
Approach
- Clarify the two ambiguous words. 'Group' most naturally means a connected component of the friendship graph; if it means a clique, the problem becomes maximum clique, which is NP-hard, so confirm. 'Interests in common' could mean interests shared by every member (set intersection) or interests shared by at least two members (a frequency count).
- Find the groups with union-find over the friendship edges (near-linear, with path compression and union by size) or with BFS/DFS from each unvisited node, O(V + E).
- Score each group. For the intersection, start from the smallest member's interest set and intersect it with the others using hash sets. For the at-least-two version, count interests in a hash map per component. If the interest universe is small, use bitsets: intersection becomes a bitwise AND. Total cost is O(V + E + sum of interest-list sizes).
- Settle edge cases and ties up front: isolated people (does a one-person group count?), people with no interests, components with an empty intersection, and a deterministic tie-break such as larger group size, then smallest id.
Follow-up
- Friendships arrive as a stream. How do you keep the answer current?
- Return the shared interests themselves, not only the count.
- What changes if 'group' means everyone within two hops of a given person?
Solve problems related to trees, graphs, and dynamic programming.
Medium · Algorithms and Data Structures
Approach
- Trees: recursive DFS that returns a small tuple (height and best path for diameter, min and max for BST validation), BFS for level order, LCA via parent pointers or recursion. In C++, switch to an explicit stack when depth can reach the input size, so a degenerate tree does not overflow the call stack.
- Graphs: BFS for unweighted shortest paths, Dijkstra with a binary heap at O((V + E) log V) for non-negative weights, Bellman-Ford when edges can be negative, Kahn's algorithm for topological order and cycle detection in DAG problems, union-find for connectivity.
- Dynamic programming: state the subproblem, the transition, the base cases and the iteration order before writing code. Know the standard families: 0/1 knapsack, longest increasing subsequence (O(n log n) with patience sorting), edit distance, interval DP and DP on trees.
- Optimise after the solution is correct. Reduce a 2D table to rolling rows when each row depends only on the previous one, keep a parent table if you must reconstruct the answer, and state time and space for both versions.
Follow-up
- Reconstruct the actual path or subsequence from your DP table, not only its length.
- Your Dijkstra gives wrong answers on one test. What input property would cause that?
- Cut your DP's memory from O(n*m) to O(m). What do you lose?
Denormalise tenant onto revisions and backfill it live
Hard · denormalisation · online migration · backfill
resource_revision (revision_id, resource_id, version, actor_user_id, change_kind, patch, request_id, created_at) has 400M rows and no tenant column; tenant_id lives only on resource. Two reads need it: a tenant-scoped audit feed ordered by created_at DESC, and an offboarding purge. Both join back to resource today. Justify adding tenant_id to resource_revision against those two reads, name the anomaly the copy introduces and the constraint that prevents it, then give the ordered migration for a live table taking 1.2k writes/second — the lock each step takes, how the backfill is batched, and where each step stops being reversible. PostgreSQL 16.
Approach
- Justify from the access path rather than from taste. Without the column, the audit feed either scans resource_revision by created_at and discards other tenants' rows, or resolves the tenant's resource_ids first and probes with them — both proportional to the tenant's whole history rather than to one page. With (tenant_id, created_at DESC, revision_id DESC) it is a seek that stops at 50 rows, and the purge becomes a ranged delete instead of a join.
- Name the cost exactly: a second copy of a fact can disagree with the first. Make the disagreement unwritable rather than documented — add UNIQUE (resource_id, tenant_id) on resource so it can serve as a foreign-key target, then FOREIGN KEY (resource_id, tenant_id) REFERENCES resource (resource_id, tenant_id) on the revision table. A revision can then only ever carry its parent's tenant.
- Step one, expand: ALTER TABLE resource_revision ADD COLUMN tenant_id BIGINT NULL, with no default, so it is a catalogue change and no rewrite. It still needs ACCESS EXCLUSIVE for an instant, and that instant queues behind the longest open transaction on the table while every later query queues behind it — set lock_timeout to 2s and retry rather than wait.
- Step two, dual-write: deploy the writer that populates tenant_id on every new revision while reads still use the join. Reversible by redeploying the previous build, because nothing reads the column yet.
Follow-up
- The backfill is half finished and a rollback is required. What state is the table in, and what does the previous build do with a half-populated column?
- How do you verify the backfill actually finished, given rows are still being inserted while it runs?
Hold a per-tenant active cap against concurrent creates
Hard · write skew · isolation levels · constraints
A tenant on the standard plan may hold at most 50 resources with status='active'. The create handler runs SELECT count(*) FROM resource WHERE tenant_id = $1 AND status = 'active', compares to 50, then inserts. Two creates arrive 3 ms apart on different instances and the tenant lands at 51. Name the anomaly, say whether PostgreSQL 16 READ COMMITTED or REPEATABLE READ prevents it and why, then give an implementation that holds the cap at READ COMMITTED with the exact statements. Finally, say what changes when the cap is 'at most one running export per tenant' on job_run.
Approach
- Name it: write skew. The two transactions read an overlapping set and write disjoint rows, so there is no row-level conflict for the engine to detect and each commit is individually legal.
- Rule out the levels precisely. READ COMMITTED takes a fresh snapshot per statement and takes no lock on the counted rows, so both see 49. PostgreSQL's REPEATABLE READ is snapshot isolation: it removes non-repeatable reads and phantoms within the snapshot but still admits write skew, because the anomaly is not a re-read of a changed row, it is a read of a set that a concurrent transaction invalidates. Only SERIALIZABLE closes it, by tracking the read dependency and aborting one transaction with SQLSTATE 40001 — a guarantee that exists only if the application re-runs the whole transaction from the read.
- Convert the set predicate into a single-row conflict: keep tenant.active_resource_count and run UPDATE tenant SET active_resource_count = active_resource_count + 1 WHERE tenant_id = $1 AND active_resource_count < 50 in the same transaction as the INSERT. Zero affected rows is the cap, returned as 409. The row lock serialises the decision at any isolation level, and contention is bounded to one tenant's row — which is also the fair-scheduling unit, unlike a global counter that would convoy every tenant behind one row.
- State the cost you just took on: a counter is a second source of truth that can drift, so every path that changes status must adjust it inside the same transaction, and a periodic reconciliation has to exist, with resource_revision as the authority for what the count should have been.
Follow-up
- A resource moves from archived back to active. Which statements change, and what breaks if the counter update and the status change land in different transactions?
- The cap becomes plan-dependent and a plan can change mid-month. Where does the number 50 live, and who reads it?
Design a trading system that matches buy and sell orders in an order book.
Medium · System Design and Engineering
Approach
- Scope it. Orders are limit and market orders (optionally IOC/FOK) with new, cancel and modify messages. The rule is price-time priority. Outputs are fills, acknowledgements and market data updates. Process each instrument single-threaded, so matching is deterministic.
- Book structure per side: a sorted map from price to level (bids descending, asks ascending), each level an intrusive doubly-linked FIFO of orders with a cached total quantity, plus a hash map from order id to node. For a bounded tick range, an array indexed by price tick with a best-price pointer gives O(1) level access.
- Matching loop: for an incoming buy, while quantity remains and the best ask is at or below the limit price, fill against the head of that level, emit trades, remove fully filled orders, and drop levels once they are empty. Rest any remainder at its limit. Cancel is O(1) through the id map. Adding at a new price costs O(log P) in the map.
- Around the core: a gateway with validation and risk checks, a sequencer that stamps every input, an append-only journal so a crash is recovered by replay, and publishers for execution reports and book updates. Shard by instrument to scale.
- Latency details: preallocate order nodes from a pool so the hot path never calls malloc, avoid locks by feeding the matching thread through a single-producer queue, and keep level data compact for the cache.
Follow-up
- A modify changes quantity or price. When does the order keep its queue position, and when does it lose it?
- How do you recover the book exactly after the matching process crashes?
- How would you publish book updates: full snapshots or incremental deltas, and how does a late subscriber catch up?
Develop a class to handle order updates from two different streams, ensuring price consistency and implementin
Medium · System Design and Engineering
Develop a class to handle order updates from two different streams, ensuring price consistency and implementing a timeout callback for expired orders.
Approach
- Pin down the semantics first: whether the two streams are redundant copies of one feed or independent sources, whether updates carry a per-order sequence number or exchange timestamp, and what price consistency means when the streams disagree. State your rule: the highest sequence number wins, and a disagreement at the same sequence number gets logged as an error.
- State: an unordered_map from order id to a record holding price, quantity, last applied sequence and expiry time. Apply an update only if its sequence is newer, which deduplicates the second copy and drops stale out-of-order updates in O(1) average time.
- Expiry: keep a min-heap (or std::multimap) keyed by expiry time with the order id and a generation counter. On each poll or tick, pop entries whose time has passed, skip any whose generation no longer matches (the order was refreshed or cancelled), erase the order and invoke the timeout callback. Use std::chrono::steady_clock, not wall time.
- Concurrency: if each stream runs on its own thread, either serialise them through queues into one event-loop thread (no locks on the state) or guard the state with a mutex. Either way, collect expired orders under the lock and fire callbacks after releasing it, so a callback that calls back into the class cannot deadlock.
- Edge cases to name: an update for an unknown order, an update arriving after expiry, one stream stalling while the other runs ahead, and cleanup so memory does not grow without bound.
Follow-up
- How would you unit-test the timeout path without sleeping? (Inject a clock.)
- One stream lags the other by thousands of messages. What does your class hold in memory, and is that bounded?
- What changes if the callback must run on the caller's thread?
Explain how make files work and the concept of order dependency for libraries.
Medium · System Design and Engineering
Approach
- Explain make's model: a rule names a target, its prerequisites and a recipe (indented with a tab). Make builds a dependency graph and reruns a recipe only when the target is missing or older than a prerequisite, compared by modification time.
- Show the working parts: variables (CXX, CXXFLAGS), pattern rules such as %.o: %.cpp, automatic variables $@, $< and $^, and .PHONY for targets that are not files. For header tracking, compile with -MMD -MP and add -include $(OBJS:.o=.d) so make actually reads the generated dependency files; without that line the .d files are written but header changes still do not trigger rebuilds. Note that make -j is only correct when every real dependency is declared.
- Link order: with static archives, GNU ld scans inputs left to right and pulls an object from a .a only to resolve symbols that are still undefined at that point. Libraries must therefore come after the objects that use them, and a library must come before the libraries it depends on: g++ main.o -lfoo -lbar when foo calls bar.
- Cover the exceptions: circular dependencies between archives (list one twice, or wrap them in -Wl,--start-group ... --end-group), and --as-needed affecting shared libraries in the same way. Explain the typical symptom: an 'undefined reference' error even though the library is on the command line.
Follow-up
- The build passes with make -j1 but fails with make -j8. What is wrong, and how do you find it?
- How do you find out why make rebuilt a target (make --trace or -d)?
- Static or dynamic linking for a latency-sensitive binary: what do PLT calls and lazy binding cost, and how do you avoid them?
Every query on one table stalls for forty seconds mid-deploy
Medium · lock contention · ddl · migrations
During a release on PostgreSQL, every query touching resource times out for about 40 seconds and then recovers with no intervention. The release ran one migration, ALTER TABLE resource ADD COLUMN archived_reason TEXT, and the migration log shows it completing in 6 ms. Unrelated tables showed no change in error rate. Explain how a 6 ms statement caused a 40-second stall, give the ordered checks you would run on a live system to confirm it, and give the migration procedure that prevents a repeat.
Approach
- Separate the statement's duration from the lock's duration. ADD COLUMN with no default is a catalogue-only change and genuinely runs in milliseconds, but it requires ACCESS EXCLUSIVE, and it cannot acquire that until every transaction already touching the table has finished.
- Account for the queueing, which is the part that surprises people. A lock request that is waiting blocks later requests for conflicting modes behind it rather than letting them overtake, so one long-open transaction holds the DDL and the DDL holds all the traffic. The stall length is set by the longest open transaction, not by the size of the change.
- Confirm on a live system in this order: pg_stat_activity for that table ordered by xact_start, looking for the oldest transaction and specifically for state = idle in transaction; then pg_locks where granted = false to find the waiter; then join them on pid to name blocker and blocked. pg_blocking_pids() does that join for you and is the fastest single call.
- Prevent rather than merely time it better. Set lock_timeout to a second or two on the migration session so the DDL abandons the queue after a bounded wait and is retried, instead of holding it for as long as the oldest transaction lives. Be exact about what that buys: queries arriving during the wait still queue behind the pending ACCESS EXCLUSIVE request, so each attempt costs them up to one lock_timeout of added latency. The outage goes from 40 seconds to about one second per attempt, not to zero. Also run migrations away from deploy-time peaks, and put a statement timeout and an idle-in-transaction timeout on the analytics role that opens the long transactions.
Follow-up
- The same release also wants NOT NULL on that column. What is the sequence that gets there without a long lock?
- Your lock_timeout retry fails ten times in a row because the analytics transaction is always open. What do you change?
Suggested practice checklist
The plan groups the reported questions by topic, starting with C++ and OS internals, moving through core data structures, algorithms, the order book and the two-stream class, and ending with build tooling and a full mock.
Day 1 — C++ object model: references, pointers, object size and templates
- Write a one-page comparison of references and pointers: reseating, nullability, sizeof, lifetime extension of temporaries, rvalue and forwarding references. Then say it aloud without notes.
- Compile small structs and print sizeof: an empty struct, an empty base plus an int, a compressed-pair style holder for a stateless deleter, a member marked [[no_unique_address]], and char/int/double members in different orders. Explain every number.
- Write out std::vector's layout (begin, end, capacity end), what push_back does on growth, and which operations invalidate iterators.
- Write a variadic, perfectly forwarding factory in the style of make_unique, and a custom forward iterator over a linked list of order book price levels. Note the cost of unique_ptr versus shared_ptr (control block, atomic reference count).
Deliverable: A reference-versus-pointer sheet, a sizeof table with an explanation for each struct, a vector layout note with its invalidation rules, and a working forwarding factory plus custom iterator.
Day 2 — OS, concurrency, networking and the GIL
- Write a table of what threads share and what processes keep separate, with creation cost, context-switch cost and IPC options. Include what fork() does in a multithreaded process.
- Implement a spin lock with std::atomic_flag and compare it with std::mutex under 2 and 8 contending threads. Then measure two threads incrementing counters on the same cache line versus padded to separate 64-byte lines, and write down what false sharing costs.
- Write a Python script where two threads update a shared counter by reading it into a local, calling time.sleep(0), then writing it back, so the lost update shows up; fix it with a threading.Lock. Note that a plain counter += 1 loop rarely loses updates on CPython 3.10+ but the race is still allowed, and use this to explain the GIL and its workarounds aloud.
- Write short notes on networking for trading: TCP versus UDP multicast for market data and order entry, Nagle's algorithm and TCP_NODELAY, and where kernel bypass fits.
Deliverable: A threads-versus-processes table, a spin lock versus mutex benchmark with a false-sharing measurement, a Python script that exposes the read-modify-write race, and a one-page networking note.
Day 3 — Core structures: heap and linked list in C++
- Implement an array-backed binary heap with push, pop, top and bottom-up heapify, then add an index map so you can delete by id in O(log n).
- Reverse a linked list iteratively and recursively, then reverse in groups of k and check a palindrome list in O(1) space.
- Write the complexity of every operation you implemented, including why heapify is O(n).
Deliverable: A tested indexed-heap class, four linked-list functions with edge-case tests, and a complexity sheet.
Day 4 — Graphs, trees and DP in assessment conditions
- Solve the friends-and-interests problem twice, once with union-find and once with BFS, for both the intersection and the at-least-two definitions.
- Solve one tree, one shortest-path and one DP problem in a HackerRank-style editor, reading stdin and writing stdout, under your own time limit.
- Rewrite one recursive tree solution iteratively and cut one DP to rolling-array memory.
Deliverable: Two working versions of the friends-and-interests solution, three solved assessment-style problems, and an iterative traversal plus a space-reduced DP.
Day 5 — Order book matching engine
- Implement a limit order book in C++: a std::map of price levels per side, an intrusive FIFO per level, an id-to-node map, and add, cancel and match with partial fills.
- Write test cases for an order that crosses three levels, a cancel of the middle order in a level, and a market order against an empty side.
- Outline the full system on one page: gateway, risk checks, sequencer, matching thread per instrument, journal for replay, and market data publisher.
Deliverable: A tested order book with complexities noted for each operation, plus a one-page system outline.
Day 6 — Two-stream order updates and online forecasting
- Implement the two-stream order update class: per-order sequence dedup, an expiry min-heap with generation counters, an injected clock, and callbacks fired after releasing the lock.
- Write tests that cover duplicate updates, out-of-order updates, an update after expiry, and a callback that calls back into the class.
- Implement O(1)-per-point EMA, rolling mean and Welford variance over a ring buffer, and list the forecasting edge cases (warm-up, gaps, outliers, look-ahead).
Deliverable: A tested two-stream class, a rolling-statistics module, and a one-page forecasting answer with its edge cases.
Day 7 — Build tooling and a full mock
- Write a Makefile for a small project with pattern rules, -MMD -MP header dependencies plus a -include $(OBJS:.o=.d) line so the generated files are read, and two static libraries that depend on each other. Touch a header to confirm the rebuild, then break the link order on purpose and fix it.
- Run a full mock: one assessment-style coding problem, then spoken answers to reference versus pointer, threads versus processes, the GIL and empty struct size.
- Re-explain the order book and two-stream designs aloud from a blank page, and note every point where you hesitated.
Deliverable: A working Makefile with a written explanation of header tracking and link order, a recorded mock session, and a list of weak spots to review on the day of the interview.
Project deep dives
Candidates report that a project on your resume serves as a starting point, and that the follow-up questions go into how and why you built it. Prepare project stories that hold up under technical questioning: the architecture, the alternatives you rejected, the latency or reliability numbers you measured, and a bottleneck you investigated on your own.
- Walk me through the architecture of a project on your resume. Why did you choose that design over the alternative you considered?
- Tell me about a time you cut latency or raised throughput in a system. How did you measure it before and after?
- Describe a concurrency bug you found: the symptom, how you reproduced it and the fix.
- Tell me about a technical bottleneck you had to research on your own, with no one to ask.
- Describe a time you refactored existing code for performance without changing its behavior. How did you prove nothing broke?
How would you design a time series forecasting algorithm, and how do you handle edge cases for efficiency?
Medium · System Design and Engineering
Approach
- Clarify the target: series frequency (ticks or bars), forecast horizon, the per-update latency budget, and whether the model must update online. Start with baselines you can beat: last value, moving average, exponential moving average, then a linear autoregression on lagged values.
- Make updates O(1) per point. An EMA is one multiply-add. A rolling mean keeps a ring buffer and a running sum. Rolling variance uses Welford's update (or windowed sums with periodic recompute) to avoid the catastrophic cancellation of sum-of-squares. Rolling min and max use a monotonic deque.
- Handle the edge cases explicitly: the warm-up period before the window fills, gaps and irregular timestamps (resample, or weight by elapsed time), duplicate or out-of-order points, bad ticks and outliers (clip or use a robust estimator such as a rolling median), and division by a near-zero variance.
- Validate without leakage: walk-forward evaluation, features built only from data available at prediction time, and error tracked over time so you notice when the regime changes and the model degrades.
- State the trade-off: richer models (ARIMA, gradient boosting on lag features) may forecast better, but they cost more per update and drift faster, so justify them against the latency budget.
Follow-up
- How do you compute a rolling median over a window of w points efficiently? (Two heaps with lazy deletion, O(log w) per update.)
- How would you detect that your forecast has stopped working in production?
- Where could look-ahead bias creep into a backtest of this model?
Estimate work you have never done and defend the range
Hard · estimation · backfills · expand-contract
You are asked to estimate a change you have never attempted: add a column to a 100-million-row table, populate it, move reads across, and drop the old shape. Give a range with the assumptions that generate it, including batch size, the signal your backfill throttles on, and wall-clock hours, and name the three unknowns that would move the number most. Then describe a real estimate you gave under comparable ignorance: how you expressed its uncertainty, what you committed to, and how wrong you turned out to be.
Approach
- Decompose into independently deployable steps before estimating anything: add the column nullable, write both shapes, backfill in batches, verify, move reads, stop writing the old shape, drop it. That is four deploys spread over days, and the calendar estimate is dominated by them rather than by the loop's runtime.
- Do the arithmetic aloud for the part that has arithmetic in it: batch size times number of batches times per-batch duration, at a write rate the primary can absorb alongside roughly 1.2k writes per second of production traffic. The loop is throttled by replication lag and lock waits, not by how fast it can issue statements.
- Price the schema step by its lock rather than its statement duration. In PostgreSQL an ALTER TABLE taking ACCESS EXCLUSIVE waits for every open transaction on that table while later queries queue behind it, so a millisecond change issued during a thirty-second analytics query stalls that table for thirty seconds. Adding a nullable column with a non-volatile default avoids a rewrite from version 11; a new index wants CREATE INDEX CONCURRENTLY, which cannot run inside a transaction block and leaves an invalid index behind if it fails.
- Express the answer as a range whose endpoints each trace to a stated assumption, then name the cheapest experiment that collapses it, which is almost always running one real batch against the real table and multiplying.
Follow-up
- How do you verify the backfill genuinely finished, given rows written by production traffic while it ran?
- Where does the backfill resume from after a worker is killed mid-batch, and what makes that resume point trustworthy?
Ship under a deadline and bound the debt you chose
Medium · pagination · technical debt · tradeoffs
You have four days to ship a tenant-facing listing endpoint. The version you would defend uses keyset pagination over (tenant_id, status, updated_at DESC, resource_id DESC); the version you can finish uses LIMIT/OFFSET with no matching index. Describe a deadline call you actually made of this shape: what you shipped, what you knowingly deferred, how you bounded the damage with a mechanism rather than an intention, and the specific numeric condition that would force the follow-up. Name who you told and where you wrote it down.
Approach
- Name the deferred failure precisely instead of calling it slow. OFFSET n makes the database produce and discard n rows, so cost grows with page depth; without an index matching the sort, every matching row is read and sorted before the limit applies; and rows inserted between two page fetches shift across the boundary so items are skipped or repeated with nothing in the response to signal it.
- Bound the blast radius with something mechanical rather than a promise: cap maximum page depth, cap page size, restrict the endpoint to one internal caller, or keep it behind a flag. State which failure each cap removes and which it leaves standing.
- Attach a number to the trigger and wire it to an alarm: the first tenant crossing N resources, or the endpoint's p99 crossing its share of the 400 ms budget, so the debt announces itself instead of waiting to be remembered.
- Write it where the next engineer looks, which is the code and the ticket, not a chat message: what was deferred, why, the cap, and the trigger.
Follow-up
- At what page depth does the offset version breach your latency budget, given your page size and row counts?
- What breaks first when you switch to keyset pagination later, and what does a client holding an old page token see?