cswork / libraryOpen practice workspace

Interview guides · BAH Partners

BAH Partners Software Engineer Interview Guide

BAH Partners Software Engineer interview guide: C++ memory, TCP vs UDP, distributed bottlenecks, concurrency, vector indexes, plus a 7-day plan.

The 60-second brief

BAH Partners is a recruitment firm. Candidates describe it as placing engineers with financial and technology-driven organizations across Asia Pacific, so the roles you interview for sit with its client firms. The work behind those roles, as candidates report it, covers search, algorithmic trading platforms and large data-processing pipelines, including web crawling at a scale of hundreds of millions of pages per day. GPU orchestration, vector databases and low-latency backend platforms are the named technology areas. A Software Engineer on these teams works with researchers and traders to turn algorithms into fast production code, and is reported to own a project from design through deployment and maintenance.

This guide is for Software Engineer candidates going through BAH Partners. It covers the five questions candidates report (C++ memory management for low-latency trading, TCP vs UDP, scaling bottlenecks, concurrency in Golang or Java, vector database indexing), the performance and distributed-design areas candidates describe, and how to talk about your own past systems. It does not cover Research Engineering or DevRel loops, which candidates say some clients add as extra rounds.

No fixed round list is reported. Candidates describe a path from early technical screens to deep dives with senior engineers and stakeholders, then architecture-style design discussions, with extra rounds for some specialised roles.

Focus areas

  • Technical Fundamentals and Domain Expertise
  • Software Engineer fundamentals
  • Working through problems out loud

Overview

The reported questions all sit at the boundary between software and hardware: how memory is laid out, how bytes cross a network, what limits a cluster, how threads share state, and how a vector index trades recall against speed. Candidates also describe example scenarios such as a search retrieval pipeline with a latency spike and the cost of data serialization in a high-throughput system. Prepare answers that start from a measurement and end in a trade-off, not a list of buzzwords.

The listed languages are C++, Python, Rust and TypeScript, and candidates report being pushed on language internals: memory models, garbage collection and standard-library behaviour. Pick the language you are strongest in and be able to say what it does with an allocation, a thread and a lock. The concurrency question names Golang or Java, so know at least one of them well enough to write a race and fix it.

Candidates report that a large share of the conversation is about your past projects and the architecture choices in them, plus how you would change those systems at a client's scale. Bring two or three systems you can explain down to the bottleneck, with before and after numbers. The same reports mention explaining technical risk to non-technical stakeholders, so rehearse a plain-language version of each trade-off you rely on.

Because BAH Partners is a placement firm, your resume travels to its client firms. Candidates are advised to agree in advance which firms receive it and to vet the consultant, so settle that before your first technical screen.

Where to focus your preparation

The reported loop is not a numbered list of rounds. Treat the single stage below as the whole preparation target and confirm the actual sequence with your consultant.

Preparation focus

Candidates describe a progression from initial technical screens, through deep dives with senior engineers and stakeholders, to architecture-style design discussions, and say some clients add rounds for specialised roles. No stage names, counts or durations are reported, so confirm them with your recruitment consultant, including whether you will write code and which language track (backend, infrastructure or research) applies. Prepare for the whole range: write correct code quickly in your primary language, explain how it behaves on the hardware, then design and defend a distributed system. The five reported questions (C++ memory for trading, TCP vs UDP, scaling bottlenecks, concurrency in Golang or Java, vector index strategies) are the core of that range.

What to demonstrate

  • Fluency in your primary language (Python, C++, Rust or TypeScript) including internals such as memory models and garbage collection
  • Breaking a large, ambiguous problem into modular components using load balancing, partitioning and fault tolerance
  • A systematic path from a performance symptom to an isolated bottleneck and a measured fix
  • Explaining past projects and the architectural choices inside them

How to prepare

  • Ask your consultant which client firm, language track and stages apply, and whether any stage is live coding
  • Write one-page answers to the C++ memory, TCP vs UDP and vector indexing questions, each ending in a stated trade-off
  • Reproduce a data race in Go or Java, then fix it three ways (lock, atomic, ownership by one thread or channel)
  • Prepare two past systems with a bottleneck, the measurement that exposed it and the number that changed

Where candidates lose points

Answering the C++ memory question with 'use smart pointers' and stopping

Smart pointers address ownership, not latency. Talk about keeping allocation off the hot path: preallocated pools or arenas, contiguous layouts for cache locality, alignment and false sharing, and measuring with a profiler or allocation counters before and after.

Reducing TCP vs UDP to 'UDP is faster' without naming what you give up

Name what UDP drops (ordering, retransmission, congestion control) and what you must rebuild or tolerate, such as gap detection or sequence numbers. Name what TCP costs (head-of-line blocking, retransmit delay, Nagle and delayed ACK interplay) and tie the choice to the data: a stale market tick is worthless, an order is not.

Listing sharding, caching and load balancers for the scaling question without a bottleneck order

Walk the path: single hot partitions, coordination and metadata services, fan-out tail latency, network bandwidth, and slow or failed nodes. For each, say how you would detect it and the fix, and say what breaks first when traffic grows tenfold.

Claiming a performance win with no measurement or number

Practise the sequence from the reported scenarios: reproduce the symptom, measure to isolate the stage (for example a 50ms spike in retrieval), change one thing, re-measure. Give your own results with a before and after figure, not a general claim.

Listing a distributed system on your resume that you cannot unpack

Candidates report deep dives into listed projects. For each, write the scale, the partitioning, the failure you hit and what you would change at a larger scale, and rehearse it aloud until each bottleneck has a cause.

Questions & practice

How do you optimize memory management in C++ for low-latency trading applications?

Medium · Technical Fundamentals and Domain Expert

Approach

  1. Start from the goal: keep allocation off the hot path. new/malloc can take a lock, touch fresh pages and page-fault, so its tail latency is unpredictable. I allocate everything at startup: reserve() containers, fixed-size object pools with an intrusive free list, or an arena such as std::pmr::monotonic_buffer_resource that I reset per batch.
  2. Optimise for cache locality: contiguous std::vector or flat arrays instead of node-based std::map or std::list, struct-of-arrays for the hot fields, and alignas(64) (or std::hardware_destructive_interference_size) on counters that different threads write, so they do not false-share a cache line.
  3. Remove hidden allocations and atomics: std::string beyond the small-string buffer, std::function with large captures, and shared_ptr copies, which do an atomic refcount increment. I use unique_ptr or plain references and string_view on the hot path, and move instead of copy.
  4. At the OS level: pre-fault and lock memory with mlock/mlockall, use huge pages to cut TLB misses, allocate NUMA-local to the pinned thread, and never free on the hot path. Freeing can be deferred to a non-critical thread.
  5. Prove it with measurements: p99/p99.9 latency rather than the mean, perf counters for cache and TLB misses, and a test that fails if the steady-state path allocates, for example by counting calls through a replaced global operator new.

Follow-up

  • How would you design a fixed-size object pool, and what should happen when it runs out?
  • Why can copying a shared_ptr between threads hurt latency even though it never allocates?
  • How would you show that your order-handling path performs zero heap allocations in steady state?

How do you handle concurrency and race conditions in Golang or Java?

Medium · Technical Fundamentals and Domain Expert

Approach

  1. Separate the two problems. A data race is concurrent unsynchronised access where at least one access writes. A race condition is wrong logic that depends on timing, such as check-then-act, and it can happen even when every single operation is synchronised. Then name the shared mutable state and who owns it; confinement and immutability come first.
  2. Java: synchronized or ReentrantLock for compound actions. volatile gives visibility and happens-before ordering, but count++ on a volatile field is still not atomic. AtomicLong works for counters, and LongAdder suits heavy contention. ConcurrentHashMap.compute/merge gives an atomic per-key check-then-act. Thread pools come from ExecutorService.
  3. Go: pass ownership through channels so only one goroutine touches the data, or guard it with sync.Mutex/RWMutex. Use sync/atomic for counters, sync.WaitGroup to join, and context for cancellation so goroutines do not leak.
  4. Avoid deadlock with a single global lock order, short critical sections, and no lock held across I/O or callbacks. In Java, tryLock with a timeout can break a cycle.
  5. Verify it: go test -race reports races that actually execute during the test, not every possible race, so the tests must exercise the concurrent paths. In Java, use stress tests or jcstress, and use thread dumps (jstack) to diagnose a hang.

Follow-up

  • Why is a counter declared volatile in Java still unsafe under concurrent increments?
  • When would you choose a channel over a mutex in Go, and when is a mutex the clearer choice?
  • A service hangs in production. How do you confirm it is a deadlock and find the two locks involved?

Identify the heaviest tenants in a five-minute window under memory pressure

Medium · top-k · heavy hitters · streaming

The edge service handles about 3,000 requests per second across roughly 50,000 tenants, peaking near 9,000. Expose the 50 heaviest tenants by request count over the trailing five minutes so limits can be tightened before one tenant's backfill starves the fleet. You may not retain five minutes of raw records. Give the exact solution and its memory, then the bounded-memory approximation with its error stated as a formula, and say which you would ship and at what tenant cardinality that choice changes.

Approach

  1. Do the exact version first, because it is affordable at this cardinality: a ring of 300 one-second counters per tenant, advanced lazily, is 1,200 bytes of counters per tenant and roughly 60 to 90 MB for 50,000 tenants with overhead. Carry a running total and subtract the bucket you overwrite so a window read is O(1) rather than 300 adds.
  2. Extract the top 50 with a size-k min-heap over the tenant sums: O(d log k) for d tenants, against O(d log d) to sort them all. Maintaining the heap continuously instead of on query requires a tenant-to-heap-index map, because incrementing a count already inside the heap means sifting from a known position, and without that map you rebuild the heap on every request.
  3. State the approximation precisely rather than gesturing at sketches. Misra-Gries with m counters retains every item whose true count exceeds N/(m+1), and each retained count underestimates the truth by at most N/(m+1). With m = 1,000 and N = 900,000 requests in the window the error is roughly 900 requests, which is fine for spotting a tenant sending 50,000 and useless for ranking two tenants 200 apart.
  4. Say what breaks when the window slides: Misra-Gries and Space-Saving are insert-only and cannot be decremented as records age out. The workable construction is one summary per sub-window, say ten seconds, with 30 summaries merged at query time, and the merged error is the sum of the per-summary errors, so the bound degrades linearly in the number of sub-windows.

Follow-up

  • The heaviest tenant is heavy because of one export job rather than user traffic. Should the limiter treat those as the same tenant?
  • Two tenants sit tied at the boundary of the top 50. Does your answer flap, and does the flapping matter?

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

  1. 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.
  2. 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.
  3. 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.
  4. 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?

Stop tag and share joins from fanning out a page

Medium · join fan-out · semi-joins · many-to-many

resource_tag is (resource_id, tag_id) with PK (resource_id, tag_id); resource_share is (resource_id, shared_with_user_id, permission). The tagged-and-shared listing inner-joins resource to both, filters tenant_id, tag_id = ANY($2) and shared_with_user_id = $3, orders by updated_at DESC and takes 50. Pages come back with fewer than 50 distinct resources and the total in the header is far too high. Explain the row multiplication, rewrite both the page query and the count query so each is correct, and name the index each one needs. PostgreSQL 16.

Approach

  1. Do the arithmetic against the predicates that are actually there. An inner join emits one row per matching child row, and both joins are filtered: tag_id = ANY($2) admits only the requested tags, shared_with_user_id = $3 admits one user's share rows. So a resource holding three of the requested tags and shared with $3 once yields three rows, not one — the multiplier is its count of matching tags times its share rows for that single user, and that second factor is 1 unless the table admits duplicate (resource_id, shared_with_user_id) pairs. LIMIT 50 then limits rows rather than resources, and COUNT(*) counts pairs — the header is the product, not the population.
  2. Reject DISTINCT as the fix. It deduplicates after the product has been built, so the planner must materialise and sort the fanned-out set before the LIMIT can apply, and it leaves any SUM or AVG in the same select list wrong.
  3. Rewrite both filters as semi-joins, keeping resource as the only row source: AND EXISTS (SELECT 1 FROM resource_tag rt WHERE rt.resource_id = r.resource_id AND rt.tag_id = ANY($2)) and the same shape against resource_share. A semi-join stops at the first match per resource and preserves the driving index order, so ORDER BY updated_at DESC, resource_id DESC LIMIT 50 still stops after 50 rows.
  4. Count with the same predicates and no join at all: SELECT count(*) FROM resource r WHERE r.tenant_id = $1 AND r.status = 'active' AND EXISTS (...) AND EXISTS (...). Nothing multiplies a resource, so the number is the population.

Follow-up

  • The filter changes from 'any of these tags' to 'all of these tags'. Rewrite it and state what it costs relative to the ANY form.
  • A resource can be shared with the same user twice under different permissions. Does your count change, and should it?

Explain the trade-offs between TCP and UDP in the context of high-frequency data transmission.

Medium · Technical Fundamentals and Domain Expert

Approach

  1. TCP gives a reliable, ordered byte stream with flow and congestion control. The cost is head-of-line blocking: one lost segment holds back every later byte until it is retransmitted, which takes at least a round trip. Nagle's algorithm also batches small writes unless I set TCP_NODELAY.
  2. UDP sends independent datagrams with no ordering, retransmission or congestion control, and it supports multicast, so one send reaches every subscriber. Exchange market-data feeds are commonly published this way, because a late update is often worth less than a lost one.
  3. Build the missing reliability into the application: a sequence number in every message, gap detection on receipt, arbitration between redundant A/B feeds, and recovery through a retransmit request or a snapshot over TCP. Each message must fit in one datagram under the MTU to avoid IP fragmentation.
  4. Choose per flow. For market-data fan-out, staleness hurts more than loss, so UDP multicast fits. For order entry, every message must arrive exactly once and in order, so TCP with TCP_NODELAY plus session-level sequence numbers fits.
  5. For either protocol, size socket receive buffers (SO_RCVBUF) for bursts, because UDP drops silently on overflow. Name kernel bypass as the next step when syscall and copy overhead dominate.

Follow-up

  • How do you detect and recover from a sequence gap in a UDP feed without stalling the messages that came after it?
  • What does TCP_NODELAY buy you, and what does it cost on the network?
  • Would QUIC remove head-of-line blocking for an ordered stream of order messages? Why or why not?

What are the primary bottlenecks when scaling a distributed system to thousands of machines?

Medium · Technical Fundamentals and Domain Expert

Approach

  1. Coordination and metadata: a single leader, scheduler or metadata service becomes the throughput ceiling, and every consensus write costs a quorum round trip. I keep it off the data path, shard the metadata, and let clients cache it with leases.
  2. Fan-out tail latency: if a request touches N servers that are each slow with probability p, the chance that at least one is slow is 1-(1-p)^N. With p = 0.01 and N = 100 that is about 63%. Mitigations are hedged or backup requests, a smaller fan-out, and per-call timeouts.
  3. Skew and hot partitions: hash or consistent hashing with virtual nodes spreads keys, but a single hot key still lands on one node. I split the key, replicate it for reads, or cache it in front.
  4. Network: cross-rack oversubscription, all-to-all shuffles, incast, and N x M connection pools between tiers. Batching, locality-aware placement and connection multiplexing reduce it.
  5. Failure is constant at this size: all-to-all heartbeats grow as O(n^2), so membership uses gossip, and retries need exponential backoff with jitter and idempotent operations to avoid retry storms. I find the actual bottleneck with saturation metrics (utilisation, saturation, errors per resource) before redesigning.

Follow-up

  • How do you stop a retry storm after a partial outage brings a dependency back?
  • Roughly how many keys move when a node joins a consistent-hash ring, compared with hash modulo N?
  • One key suddenly gets a thousand times the traffic of the rest. What do you change first?

Compare the performance characteristics of different vector database indexing strategies.

Medium · Technical Fundamentals and Domain Expert

Approach

  1. Flat (brute-force) search is exact and has no build step, but every query costs O(N*d). It suits small collections and serves as the ground truth when I measure the recall of approximate indexes.
  2. IVF trains k-means centroids (nlist) and assigns each vector to a list. A query scans only the nprobe nearest lists, so raising nprobe trades speed for recall. Inserts are cheap, but recall degrades if the data distribution drifts away from the trained centroids.
  3. HNSW is a layered proximity graph with roughly logarithmic search and high recall at low latency, tuned through M, efConstruction and efSearch. The costs are memory for the neighbour lists on top of the full vectors, random memory access, and awkward deletes (usually tombstones plus rebuilds).
  4. Quantization shrinks memory. Product quantization splits a vector into m sub-vectors, each encoded against 256 centroids, so a vector is stored in m bytes. int8 scalar quantization is 4x smaller than float32. Both lose recall, so I re-rank the top candidates with the full-precision vectors.
  5. Compare indexes on the recall@k versus QPS curve, p99 latency, memory per vector, build time and update/delete support. Handle metadata filters explicitly: post-filtering an ANN result can return fewer than k matches, and pre-filtering can force a near-brute-force scan.

Follow-up

  • How would you support a selective metadata filter, such as one tenant or a date range, without losing recall?
  • How would you measure recall of the production index once it is serving traffic?
  • What changes in your choice when the corpus no longer fits in RAM?

Exports duplicate a row range about once a week

Hard · leases · clock skew · idempotency

Roughly once a week an export writes a file containing a duplicated range of rows. The affected job_run rows show attempt = 1, status = succeeded, one started_at, and a lease_owner naming a different host from the one whose logs show the job starting. Leases last 30 seconds and are heartbeated every 10 from inside the handler; lease_expires_at is computed on the worker and compared against the database's now(). Find the mechanism, and give a fix that holds even if you cannot fix the clocks.

Approach

  1. Start from the fact that eliminates the obvious answer. attempt = 1 means no retry was recorded, so this is not a re-run after failure; two workers ran the same row concurrently and the takeover path never touched the counter. lease_owner naming a host other than the one that started the job is the same statement from the other side.
  2. Enumerate the mechanisms that cause a premature takeover, then find the signal that separates them. Either the lease genuinely expired because the heartbeat did not fire, which is what happens when the heartbeat runs on the handler's own thread and the handler makes a long blocking call, or it only appeared expired because two clocks disagree, since lease_expires_at is written from the worker's clock and evaluated against the database's. The discriminator is the distribution: incidents clustered on the longest exports indict the heartbeat, incidents clustered on one host indict skew. Measure both, and measure each host's offset against the database directly.
  3. Read the reclaim query precisely. In PostgreSQL now() is transaction start time, not statement time, so a reclaimer holding a long transaction compares against an older timestamp than expected; clock_timestamp() is the statement-time function. This is worth ruling in or out before you redesign anything, because it changes which rows look expired.
  4. Remove the second clock rather than trying to synchronise it. Issue and extend the lease in the database, with lease_expires_at = now() + interval '30 seconds' in both the claim and the heartbeat, so exactly one clock is ever compared and worker skew stops mattering to this predicate.

Follow-up

  • The displaced worker has already streamed half the file to object storage. What makes that side effect safe to repeat?
  • You now count takeovers. What alert fires on that counter, and at what threshold?

Suggested practice checklist

Seven days built from the reported question types: first the format and scope, then one day each for memory, concurrency, networking and distributed design with vector indexes, then past-project stories and a mock. Practice-library drills are used where they match.

Day 1 — Pin down the format and your track

  • Ask your consultant for the stages, whether code is written live, the language track (backend, infrastructure, research) and which client firms receive your resume
  • Choose your primary language from C++, Python, Rust and TypeScript and list five internals you can explain (allocation, GC or ownership, threads, standard-library containers, I/O)
  • List the five reported questions and rate your confidence in each from one to five

Deliverable: A written format and consent confirmation from your consultant, your chosen language, and a ranked list of the five questions by weakness.

Day 2 — Memory and low latency in C++

  • Write a spoken answer to the C++ memory question covering preallocation and pools, arenas, avoiding allocation on the hot path, contiguous layouts, alignment and false sharing
  • Benchmark a vector of values against a vector of heap pointers for a scan, and note the difference
  • If your language is not C++, write the equivalent answer for it (allocation and GC pressure in Python, ownership and arenas in Rust)

Deliverable: A one-page memory answer, benchmark numbers for both layouts, and a list of the tools you would use to measure allocations.

Day 3 — Concurrency and race conditions

  • Write a racy counter in Go or Java, reproduce the failure (Go's race detector, or a stress loop in Java) and fix it with a lock, an atomic and single-owner design
  • Write an answer for the concurrency question covering the memory model, happens-before, visibility, deadlock and thread pool sizing

Deliverable: A working racy program with three fixes, a short written concurrency answer, and notes on the debugging drill.

Day 4 — Network I/O: TCP, UDP and serialization

  • Write a comparison of TCP and UDP for market-data style streams covering ordering, loss, congestion control, head-of-line blocking, multicast and Nagle with TCP_NODELAY
  • Describe how non-blocking I/O with an event loop and kernel bypass change the cost per packet
  • Answer the serialization example aloud: how you would measure the overhead and what you would change (binary formats, fewer copies, batching)

Deliverable: A comparison table, a spoken serialization answer, and one recommendation for a latency-sensitive feed with its reasons.

Day 5 — Scaling bottlenecks and vector indexes

  • Write the scaling answer: bottlenecks in order, how to detect each, and the fix, covering hot partitions, coordination, fan-out tail latency and network limits
  • Write a comparison of vector index strategies (exhaustive scan, inverted-file, graph-based and quantised) on recall, latency, memory and update cost
  • Talk through the retrieval latency spike scenario from first measurement to a fix

Deliverable: A bottleneck checklist, an index comparison table with a recommendation per workload, and a recorded walkthrough of the spike scenario.

Day 6 — Past systems and stakeholder communication

  • Write deep-dive notes for two of your past systems: scale, partitioning, failures, what you would change, with measured before and after numbers
  • Rehearse a plain-language explanation of one technical risk for a non-technical stakeholder

Deliverable: Two project deep-dive sheets with numbers, one stakeholder-risk script, and rehearsed outage and deadline stories.

Day 7 — Full mock

  • Answer all five reported questions aloud back to back and note where you stalled or lacked a number
  • Write three questions to ask about the client, the stack and the team's latency or scale targets

Deliverable: A recorded mock with a list of fixes, a solved warm-up drill, and three questions for the interviewers.

Behavioural preparation

No behavioral questions are reported by name. Candidates report discussion of past projects, architectural choices and how you communicate technical risk to non-technical stakeholders, so prepare stories where you owned a performance or scale problem end to end.

  1. Walk me through a system you built, the bottleneck you hit and the measurement that showed it.
  2. Describe a performance problem you isolated: what you measured, what you changed and the number that moved.
  3. Tell me about a time you explained a technical risk to a non-technical stakeholder and what they decided.
  4. Describe a design decision where you chose consistency over availability, or latency over throughput, and what it cost.
  5. Tell me about a project you owned from design through deployment and maintenance, and what you would change at ten times the scale.

Narrate an outage you owned from page to postmortem

Hard · incident response · blast radius · postmortems

Pick an incident you personally drove, ideally one where writes were affected rather than reads. In six to eight minutes: state the symptom as it first appeared on a dashboard, the blast radius you established before you knew the cause, the mitigation you applied and when, the mechanism you eventually proved, and the follow-up that would prevent a repeat. Bring numbers: error rate, tenants affected, minutes to mitigate, minutes to resolve. If you cannot name what you measured, choose a different incident.

Approach

  1. Open on the signal rather than the cause: which metric at which percentile moved, on which service, at what time, so the listener follows the same evidence you had rather than a conclusion you already reached.
  2. Separate mitigation from diagnosis out loud. State what you did to stop the bleeding (flag off, shed traffic, drain a lease, roll back a deploy) and say plainly that you did it before the mechanism was known, because those are two jobs with different deadlines.
  3. Establish blast radius in countable terms: how many tenants, how many writes, and crucially whether the effect was loss or only delay. An append-only revision table or a pending outbox row means the change survived and the projection was merely behind, which is a repair rather than a data-loss incident.
  4. Prove the mechanism instead of asserting it. Name the trace span that grew, the plan that flipped to a sequential scan, the lease that expired, plus one alternative you ruled out and the signal that stayed flat while you ruled it out.

Follow-up

  • What would you do differently in the first five minutes, given the same dashboard and no more information?
  • Which follow-up action did you deliberately not take, and why was dropping it the right call?

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

  1. 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.
  2. 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.
  3. 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.
  4. 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?

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

  1. 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.
  2. 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.
  3. 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.
  4. 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?