Source: Team Lead & Staff Engineer ·
TeamLeadStaffEngineer.md· updated 2026-08-03 · 🔒 secret gistSynced verbatim from gist.github.com/bl9.
Team Lead & Staff Engineer Interview Prep Plan
Table of Contents
Part I — Interview Mechanics
- What Actually Gets Scored
- Self-Assessment: Find Your Gaps First
- The 8-Week Plan
- Track A — Coding
- Track B — System Design (Core)
- Track C — System Design (Your Specialization Edge)
- Track D — Architecture Deep Dive on Past Work
- Track E — Leadership & Behavioral
- Track F — The Rounds People Don't Prepare For
- Hard Loops: Company-by-Company Breakdown
- Above-and-Beyond Differentiators
- Questions to Ask Directors
- Mock Interview Schedule
- Resource List
Part II — Deep Technical Domains
- AI, GenAI, LLM & Agentic Systems
- 15.1 What the 2026 loop tests · 15.2 Model fundamentals · 15.3 Retrieval & RAG · 15.4 Agentic systems · 15.5 Evaluation · 15.6 Serving & inference · 15.7 Cost engineering · 15.8 Safety & governance · 15.9 Classical ML · 15.10 Design questions
- Context, Prompt & Graph Engineering
- 16.1 The distinction · 16.2 Context pipeline · 16.3 Window management · 16.4 Prompting as engineering · 16.5 Graph engineering & GraphRAG
- Advanced Backend: Streaming, Reactive, APIs
- 17.1 Kafka & Flink · 17.2 Java reactive, Reactor, WebFlux, virtual threads · 17.3 GraphQL & Netflix DGS · 17.4 gRPC & binary protocols · 17.5 API decision matrix · 17.6 Distributed transactions
- Real-Time Delivery, CDN, Caching & Edge
- 18.1 WebSockets · 18.2 SSE · 18.3 HTTP caching · 18.4 CDN architecture · 18.5 Application cache layers
- Compute: Serverless, Containers, Kubernetes, OS & Kernel
- 19.1 Serverless · 19.2 Containers & kernel primitives · 19.3 Kubernetes · 19.4 OS & kernel fundamentals
- Storage, Scaling & Data Platform
- 20.1 DynamoDB · 20.2 Cassandra/Scylla · 20.3 MongoDB · 20.4 OpenSearch · 20.5 Relational at scale · 20.6 Lakehouse · 20.7 Decision table
- Security: OAuth, Zero Trust, AppSec & Network
- 21.1 OAuth/OIDC · 21.2 Zero Trust · 21.3 AppSec · 21.4 Vulnerability management & patching · 21.5 SDN/SD-WAN/SASE · 21.6 Compliance
- Frontend & Full-Stack for Leads
- 22.1 React · 22.2 State layers, Redux, thunk vs saga · 22.3 Cross-cutting frontend architecture
- The Team Leadership Operating Playbook
- 23.1 Unblocking · 23.2 Growing people · 23.3 PR review · 23.4 Planning & scheduling · 23.5 Product partnership · 23.6 Promotions & retention · 23.7 Hiring · 23.8 Communication · 23.9 Metrics
- AI as a Cross-Cutting Force
- 24.1 Leadership frame · 24.2 Layer by layer · 24.3 Questions to have answers to
- Rapid-Fire Drill Bank
- Revised Study Calendar (12 Weeks)
- Final Calibration
Part III — Gap Closure
- Observability & SRE (Full Depth)
- 28.1 SLO engineering & burn-rate alerting · 28.2 OpenTelemetry & the three pillars · 28.3 Chaos engineering · 28.4 Load & performance testing · 28.5 Incident management
- Architecture Discipline: DDD, Team Topologies & Decision-Making
- 29.1 Domain-Driven Design · 29.2 Microservices vs modular monolith · 29.3 Team Topologies · 29.4 Decision machinery (DACI, ADRs, C4, one-way doors)
- Delivery Engineering: CI/CD, Testing Strategy & Experimentation
- 30.1 Trunk-based development · 30.2 Feature flags · 30.3 Testing strategy & contract testing · 30.4 Release engineering · 30.5 Experimentation platform
- Standards & Frameworks Reference Card
- DORA · SPACE · DX Core 4 · DevEx · SRE · Well-Architected · 12-Factor · FinOps · Team Topologies · SLSA · NIST CSF · NIST AI RMF/ISO 42001 · WCAG · C4
- Multi-Region, DR & Business Continuity
- The Offer Stage: Closing Above and Beyond
- Updated Weekly Integration
Part IV — Patterns, Contracts, and the Human Layer
- Design Patterns & Code Architecture
- 35.1 SOLID with mature caveats · 35.2 The GoF subset that appears · 35.3 Enterprise patterns (Repository, Unit of Work, ACL) · 35.4 Hexagonal/Clean, CQRS, anti-patterns, refactoring vocabulary · 35.5 How it shows up in interviews
- SLA vs SLO vs SLI — the Contractual Layer
- SLA remedies & fine print · dependency SLA composition · OLAs · per-tenant SLAs · STAR ↔ SCOR mapping
- The Question Playbook, by Interviewer
- 37.1 Recruiter · 37.2 Hiring manager · 37.3 Peers & future reports · 37.4 Director/skip · 37.5 VP/CTO · 37.6 Product partner · 37.7 Bar raiser · 37.8 Universal closers · 37.9 Reverse due-diligence red flags
- Signals of Seasonality — the Unasked-For Essentials
- 38.1 Executive communication (BLUF) · 38.2 Handling unknowns · 38.3 Scar-tissue stories · 38.4 "It depends" done right · 38.5 Whiteboard & remote mechanics · 38.6 Operator literacies (budget, vendors, glue work, former peers, legacy) · 38.7 Follow-up etiquette · 38.8 Drills 53–60
- Deployment & Progressive Delivery (Consolidated Deep Dive)
- 39.1 Strategy matrix (rolling, blue/green, canary, shadow, rings) · 39.2 Traffic-shifting mechanics · 39.3 Automated canary analysis · 39.4 Canary vs A/B · 39.5 State, data & rollback · 39.6 The pipeline as a system · 39.7 Drills 61–66
Part V — Expanded Reference (full depth: code, case studies, primary sources)
- Distributed Systems & Performance Foundations
- 40.1 Latency numbers, Little's Law, queueing, USL, tail-at-scale, benchmarking discipline · 40.2 Paxos, Raft, ZAB — mechanics and production use · 40.3 Replication, consistency models, time (TrueTime/HLC), CRDTs · 40.4 Consistent hashing, jump/Maglev/rendezvous, range partitioning, resharding
- LLM Serving, Inference & the Economics of Tokens
- 41.1 Prefill vs decode · 41.2 KV cache math, GQA, PagedAttention, prefix caching · 41.3 Continuous batching + scheduler implementation · 41.4 FlashAttention, speculative decoding, quantization, parallelism · 41.5 Serving stacks · 41.6 Cost engineering with worked math · 41.7 LLM observability
- Architecture Patterns, Fully Worked
- 42.1 CQRS — the adoption ladder, Level 1 & Level 4 code, projections, the five eventual-consistency fixes, TAO/Venice/Netflix case studies · 42.2 Event Sourcing — event store schema, optimistic concurrency, snapshots, upcasting, crypto-shredding for GDPR, why Kafka isn't an event store · 42.3 Sagas — orchestration vs choreography, compensation code, Temporal/Cadence/Step Functions · 42.4 Transactional Outbox — SQL, Debezium config, polling vs CDC · 42.5 Repository & Hexagonal, concretely
- Worked System Design Answers
- 43.1 Distributed rate limiter (full: algorithms table, Redis Lua, two-tier lease architecture, Stripe/Cloudflare/Envoy references, follow-ups) · 43.2 RAG with document-level access control (full: filtered-ANN recall problem, two-layer authz, contextual chunking, RRF, eval suite, degradation matrix)
- Worked Design Answers — AI & LLM Systems
- 44.1 LLM gateway (routing, escalation, two-tier caching, failover, chargeback) · 44.2 Multilingual semantic search (index topology, language-specific pitfalls, vector scaling math, eval without judgments) · 44.3 Agent platform with sandboxed tools (four security gates, lethal trifecta, loop termination) · 44.4 Eval pipeline gating CI · 44.5 Cost attribution across 40 teams
- Worked Design Answers — The Classics
- News feed · Notifications · Job scheduler · Metrics pipeline · Ad click aggregation · Payments ledger · Ride dispatch · Object storage/file sync · Ticketing under contention · Multi-region active-active KV
- Storage Engines & Databases, Expanded
- 46.1 LSM vs B-tree, compaction strategies, tombstones, RocksDB tuning · 46.2 Postgres MVCC, vacuum, XID wraparound, index types, reading query plans, PgBouncer · 46.3 DynamoDB single-table design · 46.4 Choosing a database
- Retrieval, Search & Ranking, Expanded
- 47.1 Inverted index, Lucene segments, BM25 derivation · 47.2 HNSW/IVF-PQ/DiskANN comparison · 47.3 Multi-stage ranking funnel, LTR, features · 47.4 NDCG/MRR, interleaving, position bias, IPS
- Leadership Scenarios — Full Worked Answers
- Toxic reviewer · Missed commitments · PM overcommits · Impossible date · Duplicate teams · AI review-queue debt · Reliability budget to a VP · Not-ready promotion · Post-reorg morale · Down-leveled offer
- Complete Drill Bank — Answer Key — all 66 drills answered
- Closing Note on Using This Document
- Gap Register & Expansion Queue
Part VI — The Engineering Management Workshop
- Team Architecture & Organizational Design
- 52.1 The Spotify model and why it failed (squads/tribes/chapters/guilds, the four failure modes, three ways to answer) · 52.2 Alternatives compared: two-pizza + STL, Team Topologies, handbook-first, context-not-control, Shape Up, feature crews · 52.3 Microservices & cloud native as org decisions
- Sizing, Estimation & Assigning Work
- 53.1 Planning poker, t-shirt, affinity, GitLab weights, Shape Up appetite, Monte Carlo forecasting · 53.2 The Goodhart critique of story points · 53.3 Breaking down an epic — full worked example · 53.4 Assigning work: skill matrix, bus factor, glue work
- Jira & Azure DevOps — Making the Tool Tell the Truth
- 54.1 Hierarchy, DoR/DoD · 54.2 Config practices per tool (workflows, components, WIP limits, automation, area vs iteration paths, branch policies) · 54.3 Metrics to read and to refuse
- The GitLab Workshop
- Handbook-first · DRI · Iteration & MVC · weights, milestones, async defaults · the honest critique
- Google, Microsoft, Amazon & Netflix — What Each Proved
- Project Oxygen & Project Aristotle · two-pizza/STL/PR-FAQ/six-pagers/bar raiser · stack-ranking reversal & 1ES · context-not-control, informed captain, keeper test
- Management Workshop — Interview Q&A, Multiple Framings
- Seven core questions, each answered three ways: screen / depth / story
- Workshop Drills (67–86)
Part VII — Answers to Everything Left Open
- Questions to Ask — What You're Listening For
- Every question from §12 and §37 with: why it works · the green flag · the red flag · your follow-up · what you do with the answer. Covers hiring manager, director/skip-level, peers and future reports, VP/CTO, the universal closers, and the reverse due-diligence probe table.
- Stated-But-Unexplained — The Answers
- 60.1 Agent determinism & replay (full trace schema, three replay levels, what you lose without it, three-register answer) · 60.2 Idempotency & effectively-once · 60.3 Blast radius & cells · 60.4 Backpressure vs shedding vs admission control · 60.5 Feature stores & point-in-time correctness · 60.6 Sticky sessions starving the canary · 60.7 HNSW delete degradation · 60.8 Composition over inheritance
1. What Actually Gets Scored
Senior interviews test whether you can solve a problem. Staff and TL interviews test whether you can pick the problem, frame it, and get other people to solve it with you. Different bar, different prep.
Shared signals (Staff and TL)
| Signal | What "senior" looks like | What "staff/lead" looks like |
|---|---|---|
| Scope | Owns a service | Owns a problem space across teams |
| Ambiguity | Given a spec, executes | Given a vague goal, produces the spec |
| Judgment | Picks a good option | Explains the 3 options, why the others lose, and the reversal criteria |
| Influence | Convinces their team | Convinces peers/leads who don't report to them |
| Multiplier | Delivers | Others got faster because of them |
| Risk | Handles known risk | Names the risk nobody else named |
Track-specific
Team Lead adds: performance conversations, hiring bar, delivery predictability, morale under pressure, stakeholder/PM negotiation, headcount and prioritization tradeoffs.
Staff adds: technical strategy over 12–24 months, cross-org standard-setting, deprecation/migration leadership, design review authority, "make the hard call in a room of senior people."
The single most common failure
Candidates tell senior-level stories in a staff-level interview. The story is about what they built, not how the org changed. Fix this in Track E before anything else.
2. Self-Assessment: Find Your Gaps First
Do this in one sitting, week 0. Score 1–5, honestly.
Coding
- Can I solve a medium in 20 min, clean, with tests, while talking?
- Can I do it in a shared editor with no autocomplete?
- Have I done a hard graph/DP problem in the last 30 days?
System design
- Can I do capacity math out loud without freezing (QPS, storage, bandwidth, memory)?
- Can I name 3 consistency models and when each is acceptable?
- Can I design for multi-region without hand-waving the write path?
- Can I draw and defend a rollback/migration strategy?
Deep dive
- Do I have a 45-min narrative of my hardest project with numbers, alternatives, and regrets?
- Can I whiteboard my current system from memory at 3 zoom levels?
Leadership
- Do I have 10 stories that cover conflict, failure, influence, mentoring, and prioritization?
- Can I tell any of them in 3 minutes without rambling?
- For TL: can I role-play a performance conversation without getting soft or harsh?
Anything scored 1–3 gets calendar time in the plan below. Anything at 4–5, don't over-study it.
3. The 8-Week Plan
Assumes ~12–15 hrs/week. Compress to 5 weeks by dropping Weeks 3 and 6 and doubling volume.
Week 1 — Foundation + story mining
- Build the story portfolio (Track E). This is the long pole; start it first.
- Fundamentals refresh: latency numbers, CAP/PACELC, quorum math, consistent hashing.
- 10 coding problems (easy/medium warm-up, arrays, hashmaps, two pointers).
- Write your deep-dive narrative outline (Track C).
Week 2 — Design core + coding volume
- 5 classic system designs, written out (rate limiter, URL shortener, notification system, distributed job scheduler, metrics pipeline).
- 15 coding problems: trees, graphs, BFS/DFS, heaps.
- Rewrite 3 stories from Week 1 in the SCOR format (Section 8).
Week 3 — Storage + streaming internals
- Deep dive: LSM vs B-tree, Kafka internals, Cassandra/DynamoDB partitioning, Postgres MVCC and replication.
- 3 designs: news feed, chat/messaging, ad click aggregation.
- 12 coding problems: intervals, binary search, sliding window.
- First mock: system design, external interviewer.
Week 4 — Reliability, scale, multi-region
- Topics: cell-based architecture, blast radius, circuit breakers, backpressure, graceful degradation, SLO/error budgets, idempotency and the outbox pattern.
- 3 designs: payments ledger, ride-hailing dispatch, multi-region active-active KV store.
- 12 coding problems: DP, backtracking, tries.
- Mock: behavioral/leadership.
Week 5 — Your specialization (the differentiator)
- Full Track C content: search, retrieval, ranking, RAG, agentic platforms, vector index scaling.
- Write 2 designs you could teach: "multilingual semantic search at 10K QPS" and "enterprise RAG with eval harness."
- 10 coding problems, mixed hard.
- Mock: deep dive on your own past work.
Week 6 — Leadership under pressure
- Role-play drills: underperformer, two senior engineers in conflict, PM pushing scope, director wants a date you can't commit to.
- Write a real design doc (5 pages) and a written critique of someone else's.
- Practice the "design doc review" and "code review" round formats.
- Mock: people-management round (TL) or cross-org influence round (Staff).
Week 7 — Company-specific
- Pick your top 3 targets. Study their loop format (Section 10), eng blog, open source, public architecture talks.
- One full-loop simulation in a single day: 2 coding + 1 design + 1 behavioral, back to back, no breaks. This tests stamina, which is what actually breaks people.
- Prepare per-company question lists and 30/60/90 plans.
Week 8 — Polish and taper
- Re-run only your weakest two rounds.
- Trim every story to 3 minutes.
- Light coding daily (2 problems, timed) — maintenance, not growth.
- Sleep. Do not cram new material in the last 3 days; it costs recall on the material you already have.
4. Track A — Coding
Staff and TL loops still have coding rounds at Google, Meta, Stripe, Databricks, Uber, and most AI labs. The bar isn't harder problems — it's cleaner code, better tests, and no flailing.
Volume target
- ~70–90 problems total, not 400. Depth over count.
- Distribution: 20% easy (warm-up/speed), 65% medium, 15% hard.
Pattern checklist (don't move on until each is automatic)
- Hash map / frequency counting
- Two pointers, sliding window
- Binary search — including "search on the answer"
- BFS / DFS / topological sort
- Union-find
- Heaps and top-K
- Intervals (merge, sweep line)
- Prefix sums and difference arrays
- Backtracking with pruning
- DP: 1D, 2D, knapsack, LIS, edit distance
- Tries
- Monotonic stack
Staff-specific coding expectations
- Talk about the API before the algorithm. Name types, name the contract.
- Write tests unprompted. Even 3 assertions. This is a differentiator at every level above senior.
- State complexity before coding, verify after.
- Handle the "make it production" follow-up: what breaks at 100× input, where does it go concurrent, what's the failure mode.
- Don't over-abstract. Staff candidates lose points for building a factory when a function was asked for.
Drill format
Timed, 35 min, out loud, in a plain editor. Record yourself once a week and watch it. Painful, effective.
5. Track B — System Design (Core)
5.1 Fundamentals you must have memorized
Latency numbers (order of magnitude is enough):
- L1 ~1 ns, main memory ~100 ns, SSD random read ~100 µs, network round trip same DC ~0.5 ms, cross-region ~50–150 ms, disk seek ~10 ms.
Capacity math you should do in 60 seconds:
- DAU → QPS (peak ≈ 3–5× average)
- Bytes/record × records/day → storage/year
- Working set → cache size → node count
- Read:write ratio → replication and sharding decision
Distributed systems core:
- CAP, and why PACELC is the more useful framing
- Consistency models: linearizable, sequential, causal, read-your-writes, eventual
- Quorum: R + W > N, and what sloppy quorums buy you
- Consensus: Raft leader election, log replication, why you don't want consensus in the hot path
- Consistent hashing + virtual nodes; when range partitioning beats it
- Idempotency keys, exactly-once as "at-least-once + dedupe"
- Outbox pattern, CDC, dual-write problem
- Backpressure, load shedding, admission control
- Rate limiting: token bucket, leaky bucket, sliding window counter, distributed rate limiting
Storage internals:
- LSM tree (write amplification, compaction, bloom filters) vs B-tree
- Kafka: partitions, ISR, consumer groups, rebalancing, retention, compaction
- DynamoDB/Cassandra: partition key design, hot partitions, LWT cost
- Postgres: MVCC, vacuum, index types, logical replication
- Object storage: S3 consistency, multipart, lifecycle, why it's the default data lake
Reliability:
- SLI/SLO/error budget, and how error budget changes release policy
- Circuit breaker, bulkhead, timeout + retry with jitter, retry storms
- Graceful degradation tiers (what do you turn off first)
- Blast radius and cell-based architecture
- Blue/green, canary, shadow traffic, feature flags, dark launch
Migration and rollout — heavily weighted at staff level, rarely prepared:
- Dual-write + backfill + read-verify + cutover + cleanup
- Strangler fig pattern
- Reversibility: what's the rollback at each phase
- How do you prove correctness during migration (shadow compare, sampled diffing)
5.2 Classic designs to write out (not just read)
- Distributed rate limiter
- Notification/fan-out system
- News feed (push vs pull vs hybrid)
- Chat / messaging with delivery guarantees
- Distributed job scheduler with exactly-once semantics
- Metrics/observability pipeline (ingest → aggregate → query)
- Ad click aggregation (dedupe, late events, watermarks)
- Payments ledger with idempotency and double-entry
- Ride-hailing dispatch (geo-indexing, matching)
- Object storage / file sync (Dropbox-style)
- Multi-region active-active KV store
- Ticketing/inventory with contention (Ticketmaster-style)
5.3 The staff-level design interview structure
Most candidates do requirements → boxes → done. Do this instead:
- Clarify and scope (5 min). Who's the user, what's the SLA, what's out of scope. Write the non-functional requirements as numbers.
- Capacity math (3 min). Out loud. This alone separates candidates.
- API contract (3 min). Before any boxes.
- Data model + partitioning key (5 min). The partition key choice is where most designs live or die.
- High-level architecture (10 min).
- Deep dive on the hardest 1–2 components (15 min). Ask the interviewer which one they want.
- Failure modes, degradation, operations (7 min). What pages, what's the runbook, what's the blast radius.
- Tradeoffs and what you'd do differently at 10× (5 min).
Say out loud, at least twice: "The alternative here was X; I'm not choosing it because Y; if Z changed I'd revisit." That sentence is the staff signal.
6. Track C — System Design (Your Specialization Edge)
Search, retrieval, ranking, and agentic systems are your moat. Most interviewers can't go deep here, which means you control the room — but only if you've rehearsed it.
6.1 Search & retrieval
- Inverted index internals, segment merges, refresh vs flush, near-real-time search
- OpenSearch/Elasticsearch: shard sizing math, replica strategy, hot/warm/cold tiers, index lifecycle, alias-based blue/green reindex
- Query understanding: tokenization, analyzers per language, stemming vs lemmatization, CJK segmentation, transliteration
- Multilingual: shared vs per-language index, cross-lingual embeddings, language detection failure modes, script normalization
- Relevance: BM25, learning-to-rank, hybrid (lexical + dense) with reciprocal rank fusion
- Evaluation: NDCG, MRR, recall@k, offline/online correlation, judgment collection
6.2 Vector search
- HNSW: graph structure,
MandefConstruction/efSearchtradeoffs, memory formula (roughlyd × 4 bytes × Nfor float32 plus graph overhead), why deletes are expensive - IVF-PQ vs HNSW: memory vs recall vs build time
- Billion-scale: sharding by ID vs by cluster, routing, replica fan-out, cold start
- Filtered vector search: pre-filter vs post-filter and the recall cliff
- Index rebuild strategy without downtime
6.3 Recommendation systems
- Multi-stage: candidate generation → light ranker → heavy ranker → re-ranking/diversity/business rules
- Feature store: online/offline parity, point-in-time correctness, training/serving skew
- Embedding freshness, two-tower architectures, ANN retrieval for candidates
- Cold start (user and item), exploration vs exploitation, bandits
- Feedback loops and popularity bias — name this unprompted, it's a strong signal
- Evaluation: offline replay, interleaving, A/B tests, guardrail metrics, novelty/diversity metrics
6.4 RAG and LLM systems
- Chunking strategies and why fixed-size is usually wrong
- Hybrid retrieval + cross-encoder reranking, latency budget allocation
- Context assembly, citation grounding, hallucination guardrails
- Eval harness: golden set, LLM-as-judge with its failure modes, regression gates in CI
- Serving: KV cache, continuous batching, speculative decoding, quantization tradeoffs
- Cost model: tokens per request × price, caching layers, model routing (cheap model first, escalate)
6.5 Agentic platforms
- Tool routing and schema design, why tool descriptions are the real prompt
- Sandboxing and permission boundaries for tool execution
- Multi-step orchestration, retries, and loop-termination guarantees
- Observability: trace per step, token/cost attribution, replay
- Eval: task-level success rate, not per-step accuracy
- Failure containment: what does the agent do when the tool is down or the output is malformed
6.6 Designs to have ready to whiteboard
- Multilingual semantic search over 500M docs, 10K QPS, p99 < 200 ms
- Personalized recommendation serving with a 50 ms budget
- Enterprise RAG with document-level access control
- Online A/B testing and interleaving platform
- LLM gateway with routing, caching, rate limiting, and cost attribution
- Agent execution platform with sandboxed tools and full tracing
7. Track D — Architecture Deep Dive on Past Work
Usually 45–60 minutes on a system you built. Underprepared by almost everyone, and it's the round directors weight most heavily.
Prepare three zoom levels of your primary system
- 30 seconds: what it does, who uses it, what it's worth.
- 5 minutes: the architecture, the two hardest problems, the outcome with numbers.
- 45 minutes: component-level detail, data flow, failure modes, the incident, the migration, the thing you'd redo.
The checklist your narrative must hit
- What was the business problem and how was success measured
- What were the constraints (latency, cost, team size, deadline, legacy)
- Which alternatives you seriously evaluated and the specific reason each lost
- What you got wrong and what it cost
- What you'd change with what you know now
- Where you were the decision-maker vs where you influenced someone else's decision
Numbers to have memorized
QPS, p50/p99, data volume, cost, team size, timeline, and the delta on the metric you moved. If you don't have exact numbers, use defensible estimates and say they're estimates.
The trap
Interviewers will poke at a design decision to see if you get defensive. The winning move: "That's a fair criticism — here's the constraint that drove it, and here's what I'd need to change to do it your way." Confidence without brittleness.
8. Track E — Leadership & Behavioral
8.1 Build a story portfolio, not answers to questions
Write 10–12 stories. Tag each with the competencies it covers. In the interview you map the question to a story, not to a memorized answer.
Stories you need:
- The hardest technical decision you owned
- A decision you reversed
- A project that failed or got cancelled
- Conflict with a peer or another team
- Disagreement with your manager or a director
- Influencing without authority across teams
- Mentoring someone from struggling to strong
- Prioritization: what you chose not to do and why
- A major incident you led
- A migration or deprecation you drove
- Delivering under an unreasonable deadline
- Tech debt vs feature pressure
Team Lead adds: 13. Handling an underperformer 14. A hiring decision (including one you got wrong) 15. Managing team morale through a bad quarter 16. Pushing back on a stakeholder's scope or timeline
8.2 Use SCOR, not STAR
STAR under-serves staff-level stories because it has no slot for judgment.
- S — Situation: context and constraints, 20 seconds
- C — Complication: why it was actually hard, what made it non-obvious
- O — Options: the 2–3 real alternatives and why each lost
- R — Result + Reflection: the number, and what you'd do differently
The O is the whole game. It's what makes a story sound like judgment instead of a resume bullet.
8.3 Calibration rules
- 3 minutes per story. Time yourself. Nobody has ever lost an offer for being too concise.
- Use "I" for your decisions and "we" for team execution. Getting this ratio wrong reads as either credit-stealing or passenger.
- Every story needs one number.
- If a story has no failure or regret in it, it isn't a staff story.
8.4 Team Lead role-play drills (practice out loud, with a person)
- Your strongest engineer is toxic in code review. Handle it.
- An engineer has missed three commitments in a row. Run the conversation.
- A PM promises a date to a customer without asking you. Respond.
- A director asks you to cut testing to hit a date. Respond.
- Two senior engineers deadlocked on a framework choice, and it's blocking the sprint.
- You inherit a team with low morale after a reorg. First 30 days?
For each: state your first move, the information you'd gather, and the line you wouldn't cross. That three-part structure makes vague answers concrete.
8.5 Staff-specific influence drills
- Two teams are building the same thing. Neither wants to stop. You have no authority over either.
- You believe the org's chosen architecture is wrong. You've been overruled once already.
- You need three teams to adopt a standard that makes their next quarter slower.
9. Track F — The Rounds People Don't Prepare For
- Design doc review. Given a 4-page doc, critique it live. Practice: write one, then critique a real RFC (Rust RFCs, Kubernetes KEPs, Kafka KIPs are all public and excellent training material).
- Code review round. Given a PR, find correctness, security, and maintainability issues. Practice on open-source PRs. Prioritize your comments — blocking vs nit — that prioritization is the signal.
- Debugging round. A repo with failing tests; find and fix. Practice: clone something unfamiliar, break it, fix it under time.
- Incident simulation. Live "the graphs look like this, what now." Practice the loop: stop the bleeding → mitigate → diagnose → communicate → postmortem. Say "I'd mitigate before I diagnose" out loud; many candidates skip straight to root cause.
- Take-home + presentation. Scope small, ship clean, write a README with tradeoffs and what you'd do with more time. The README is what gets graded at staff level.
- Product/business sense. "How would you prioritize this roadmap?" Have an opinion about your users and your metrics.
10. Hard Loops: Company-by-Company Breakdown
Formats shift; verify with your recruiter. This is the shape as of the most recent public reporting.
Google — Staff (L6) / TLM
- 2 coding rounds (still LeetCode-style, medium-hard), 1–2 system design, 1 "Googleyness & Leadership."
- L6 requires demonstrated cross-team scope. Hiring committee reads a packet — your interviewers must be able to write down your scope. Give them quotable lines.
- Design rounds go deep on data modeling and failure handling.
- Prep weight: coding 30%, design 40%, leadership 30%.
Meta — E6 / Engineering Manager
- IC: 2 coding (two problems per 45-min round — speed matters), 1 system design, 1 "Jedi" behavioral.
- EM: system design, people management, project retrospective, sometimes coding.
- E6 needs org-level impact stories. "I built X" is E5. "I changed how three teams do X" is E6.
- Prep weight: coding speed is the top failure cause; drill 2-problems-in-45-min explicitly.
Amazon — Principal (L7) / SDM
- Leadership Principles dominate. Expect 10–16 LP questions across the loop, plus a Bar Raiser.
- Every LP answer needs data. "Dive Deep" means they will ask for the number three layers down.
- Also: system design, and for L7 a "technical vision" discussion.
- Prep weight: build 2 stories per LP, 16 LPs. This is the single most story-intensive loop in the industry.
Stripe
- Integration round (build against a real API), debugging round (fix a broken repo), system design, and a values/behavioral round.
- Practical over algorithmic. Your editor fluency and debugging method are visible.
- Prep weight: debugging and pragmatic coding 50%.
Netflix — Senior/Staff
- Culture-memo alignment is a real filter: context not control, freedom and responsibility, "keeper test."
- Senior-heavy org, so expect very deep technical conversation and expect to justify autonomy.
- Prep weight: deep dive 40%, culture 30%, design 30%.
Databricks / Snowflake / Confluent
- Hard distributed systems internals plus genuinely hard coding.
- Expect questions on query execution, storage formats, consistency, and concurrency.
- Prep weight: storage/streaming internals 50%.
Uber / Lyft / DoorDash
- High-scale system design with real domain constraints (geo, matching, dispatch, pricing).
- Coding is medium-hard, design is the differentiator.
Shopify (Toronto)
- "Life Story" interview — a structured walk through your career. Prepare a narrative arc, not a list of jobs.
- Pair programming in a real repo, technical deep dive.
OpenAI / Anthropic / AI labs
- Practical coding, often agentic (use the tools, work in a real codebase), take-home with a follow-up deep dive, and a strong emphasis on judgment and safety-mindedness.
- They test whether you can move fast in unfamiliar code. Practice: contribute a real fix to an open-source repo you've never seen, timed.
Nvidia / Cisco / systems companies
- Performance, concurrency, memory, and profiling. Expect C++/systems questions if the role is close to the metal.
11. Above-and-Beyond Differentiators
These are what make a director remember you.
- Bring an artifact. A one-page architecture diagram or a design doc you can share (sanitized). Almost nobody does this.
- Write a 30/60/90 plan for their team and send it after the onsite. For a TL role this is close to decisive. Structure: first 30 = listen and map (name what you'd read, who you'd meet, what you'd measure), 60 = one visible fix, 90 = a strategy proposal.
- Know their public engineering output. Their eng blog, conference talks, open source. Reference it specifically: "In your post on X you mentioned Y — how did that land?"
- Speak in operational metrics. p99, error budget, DORA metrics, cost per query, on-call load. Leads who talk about cost and toil sound senior instantly.
- Name your own weaknesses before they find them. "The gap in my background is Z; here's how I'd close it in the first quarter."
- Have a technical opinion. Not a hot take — a defended position on something in their domain. Directors hire people with a point of view.
- Send a follow-up that adds value, not thanks. One paragraph continuing the design discussion, or the diagram you sketched, cleaned up.
- Prepare a diagnostic question set — the questions you'd ask the team in week one. Sharing this signals you already think like the lead.
12. Questions to Ask Directors
Skip the generic ones. These probe real things and signal seniority.
About the role
- What does success look like at 6 months, and who decides?
- Is this role backfilling someone, or is it new scope? What happened to the previous lead?
- What's the split between technical direction and people management you actually expect?
About the team
- What's the team's biggest source of toil right now?
- How much of the roadmap is committed vs discretionary?
- Where does the team's on-call pain come from?
About the org
- How do technical decisions that span teams get made here?
- What's the last significant architectural decision, and how was it reached?
- What's the thing about this org you'd change if you could?
About you as a manager (ask the director)
- How do you like to be disagreed with?
- What's your escalation threshold — when do you want me to bring you in?
- What did the last person who succeeded in a role like this do differently?
The high-leverage closer
- "Is there anything in my background that gives you hesitation? I'd rather address it now than leave it open."
13. Mock Interview Schedule
Real mocks with real humans, not solo practice. Minimum 6 before your first real onsite.
| Week | Mock type | Source |
|---|---|---|
| 3 | System design | Paid platform or a senior peer |
| 4 | Behavioral/leadership | Peer who's a manager |
| 5 | Deep dive on your own work | Someone outside your domain |
| 6 | People-management role-play (TL) or influence round (Staff) | Manager friend |
| 7 | Full loop, one day | Mixed |
| 8 | Weakest round, repeat | Best available |
Rules: record everything. Watch at 1.5×. Note filler words, rambling, and the moment you lost the interviewer. Fix one thing per mock.
14. Resource List
System design
- Designing Data-Intensive Applications — Kleppmann (chapters 5–9 are the core)
- Database Internals — Petrov (for storage-engine depth)
- Company engineering blogs: Uber, Netflix, Meta, Discord, Cloudflare, Stripe
- Public design docs: Kubernetes KEPs, Kafka KIPs, Rust RFCs (for the design-review round)
Coding
- NeetCode 150 for pattern coverage
- Timed sessions in a plain editor, not an IDE
Leadership
- The Staff Engineer's Path — Reilly (the definitive staff-scope framing)
- Staff Engineer — Larson
- An Elegant Puzzle — Larson (for TL/EM org questions)
- The Manager's Path — Fournier (TL chapter specifically)
Your domain
- HNSW paper (Malkov & Yashunin), FAISS docs
- Elasticsearch/OpenSearch internals docs on shards, segments, and lifecycle
- Recent RAG and agent evaluation literature — pick 5 papers and know them well enough to critique
PART II — Deep Technical Domains
15. AI, GenAI, LLM & Agentic Systems
15.1 What the 2026 loop actually tests
The content shifted hard. Classical ML theory (backprop, CNNs, gradient descent) is now maybe a quarter of technical rounds; the rest is RAG architecture, evaluation, agents, and production concerns. The four things a modern loop probes that a 2022 loop never did:
- System design on top of a model you don't control
- Judgment about retrieval and evaluation
- Whether you can build and debug the integration by hand
- Production sense: cost, latency, and the quiet ways these systems fail
Definitions are free now. Judgment is what's being bought.
15.2 Model fundamentals you must hold cold
- Transformer: attention (Q/K/V), multi-head, positional encoding (RoPE, ALiBi), why context length is quadratic in attention and what mitigates it (FlashAttention, sliding window, sparse attention)
- Decoder-only vs encoder-decoder vs encoder-only (and why BERT-family still wins for reranking)
- Tokenization: BPE, why token counts differ across languages (directly relevant to multilingual work — non-Latin scripts can cost 2–4× the tokens)
- Prefill vs decode: prefill is compute-bound, decode is memory-bandwidth-bound. This single distinction explains most serving architecture.
- KV cache: size formula ≈
2 × layers × heads × head_dim × seq_len × batch × bytes_per_param. Know that it, not weights, is what actually limits your batch size. - MoE: sparse activation, expert routing, why parameter count ≠ compute cost
- Fine-tuning ladder: prompting → few-shot → RAG → LoRA/QLoRA → full SFT → preference tuning (RLHF, DPO). Know the cost and the "when."
- Quantization: FP16/BF16 → FP8 → INT8 → INT4; GPTQ, AWQ, GGUF. Quality/latency/memory tradeoffs.
- Distillation: use production traffic from a large model to train a small one. The best cost lever nobody mentions.
- Sampling: temperature, top-p, top-k, min-p, repetition penalty, and why greedy isn't always right
- Structured output: JSON mode, constrained decoding / grammar-based sampling, tool-call schemas
15.3 Retrieval & RAG (beyond the basics)
Chunking
- Fixed-size is the default and usually wrong. Know: recursive character splitting, semantic chunking, structure-aware (headings, code blocks), parent-document retrieval, late chunking (embed the full doc, then pool per chunk), contextual retrieval (prepend an LLM-generated summary of the doc to each chunk)
- Overlap tradeoff: recall vs index bloat vs duplicate context
Retrieval
- Hybrid: BM25 + dense, fused with Reciprocal Rank Fusion (
1/(k+rank), k≈60). Know why RRF beats score normalization. - Query transformation: rewriting, decomposition into sub-queries, HyDE (embed a hypothetical answer), step-back prompting
- Multi-hop retrieval and when it's needed vs when it's just latency
- Late interaction (ColBERT) — better recall, much higher index cost
- Access-control-aware retrieval: filter at query time, never post-filter (post-filtering leaks existence and destroys recall)
Reranking
- Cross-encoder rerankers on top-k (k=50–200) from first-stage retrieval
- LLM rerankers: better, slower, expensive; use for the top 10
- The latency budget conversation: retrieval 30 ms, rerank 80 ms, generation 800 ms — know where your budget actually goes
Failure modes to name unprompted
- Retrieval succeeded, generation ignored it (groundedness failure)
- Retrieval failed silently and the model confabulated confidently
- Chunk boundaries split the answer
- Stale index vs live source of truth
- "Lost in the middle" — relevant context in the middle of a long window gets underweighted
15.4 Agentic systems
Patterns
- ReAct (reason → act → observe loop), Plan-and-Execute (decompose upfront, then run), Reflexion (self-critique and retry), router/dispatcher, supervisor with sub-agents, handoff/swarm
- Single agent with good tools beats multi-agent in most production cases. Have the opinion; it's a maturity signal.
Architecture components
- Planner, tool registry, executor, memory store, trace/observability layer
- MCP (Model Context Protocol) as the emerging standard for tool exposure — know it, know that tool descriptions are prompt surface area
Failure modes to design against (name these in the interview)
- Tool misuse → strict input validation, sandboxed execution
- Infinite loops → max iterations, total token budget, cycle detection
- Prompt injection via tool output → sanitize before re-injection, treat all tool output as untrusted
- Hallucinated tool calls → validate against the registered schema, reject and retry
- Scope creep / unintended side effects → permission scoping per tool, human-in-the-loop for destructive actions
- Non-idempotent retries → idempotency keys on every side-effecting tool, compensating actions for rollback
Memory
- Short-term (conversation buffer), long-term (vector or structured store), episodic (past task traces), semantic (facts/entities)
- Compaction and summarization strategies; when to drop vs summarize vs offload to a sub-agent
Determinism and debuggability
- Full trace per step: inputs, tool calls, outputs, tokens, cost, model version
- Replay capability — you cannot debug agents without it
- Checkpointing for long-running workflows
15.5 Evaluation (the round most candidates fail)
- Separate retrieval eval from generation eval. Recall@k and NDCG for retrieval; groundedness/faithfulness and answer relevance for generation. Conflating them is the classic mistake.
- Golden set: 100–500 hand-labeled examples covering the head and the ugly tail. Build it before you build the system.
- LLM-as-judge: cheap and scalable, but know its biases — position bias, verbosity bias, self-preference bias. Mitigate with pairwise comparison, randomized order, and rubric-based scoring with explicit criteria.
- Pointwise vs pairwise vs reference-based grading; when each is appropriate
- Agent eval is task-level success rate, not per-step accuracy. Also: trajectory efficiency (steps to completion), cost per successful task.
- Regression gates in CI: eval suite runs on prompt/model/retrieval changes; block merge on regression beyond a threshold
- Online: A/B, user thumbs, escalation rate, task abandonment
- Red teaming: jailbreaks, injection, PII leakage, refusal calibration
- Drift: model version changes underneath you. Pin versions, re-run evals on every provider update.
15.6 Serving & inference infrastructure
- Continuous (in-flight) batching — the core throughput lever
- PagedAttention / vLLM: KV cache paging, prefix caching for shared system prompts
- Speculative decoding: draft model + verification
- Tensor parallelism (within a node) vs pipeline parallelism (across nodes); when you need either
- Metrics that matter: TTFT (time to first token), TPOT (time per output token), throughput (tokens/sec), GPU utilization, queue depth
- GPU memory math: weights + KV cache + activations + fragmentation. Know how to size a deployment.
- Autoscaling GPUs: cold start is minutes, not seconds. Provisioned floor + queue + graceful degradation.
- Serving stacks: vLLM, TGI, SGLang, TensorRT-LLM; managed (Bedrock, Vertex, Azure OpenAI)
- Common design question: "Design an inference batching system for a single GPU handling up to 100 synchronous requests, maximizing utilization under latency constraints." Practice this one specifically — it's asked verbatim at multiple labs.
15.7 Cost engineering
- Unit economics: cost per request = (input tokens × input price) + (output tokens × output price), amortized over cache hit rate
- Levers, ranked by impact: prompt caching → model routing (cheap first, escalate on low confidence) → context trimming → semantic caching → distillation → batch API
- Track cost per feature, per team, per customer. Attribution is a real engineering problem.
- Budget guardrails: per-tenant token quotas, circuit breakers on spend, alerting on cost-per-request drift
- The lead-level version: token spend is now a line item leadership asks about. Being fluent here differentiates you immediately.
15.8 Safety, guardrails, governance
- OWASP LLM Top 10: prompt injection, insecure output handling, training data poisoning, model DoS, supply chain, sensitive info disclosure, insecure plugin design, excessive agency, overreliance, model theft
- Direct vs indirect prompt injection. Indirect (via retrieved docs, tool output, web content) is the dangerous one and the one interviewers probe.
- Output handling: treat model output as untrusted input. Never eval it, never pass it to a shell, escape before rendering.
- PII: detection and redaction on ingress and egress; data residency for model calls
- Excessive agency: scope tool permissions to the minimum; require confirmation for irreversible actions
- Governance: model cards, approved-model registry, audit logging of prompts and outputs, retention policy
- Regulatory awareness: EU AI Act risk tiers, sector rules (this matters a lot in legal/financial domains)
15.9 Classical ML you still need
- Feature engineering, leakage (especially temporal leakage), point-in-time correctness
- Class imbalance, calibration (Platt, isotonic), threshold selection by business cost
- Metrics: precision/recall/F1, AUC-ROC vs AUC-PR (PR for imbalanced), NDCG for ranking
- Training/serving skew, feature store as the fix
- Model lifecycle: registry, versioning, shadow deploy, canary, rollback
- Drift: data drift vs concept drift, detection (PSI, KS test), retraining triggers
15.10 AI design questions to rehearse
- Design a RAG system for customer support with document-level access control
- Design an LLM gateway: routing, caching, rate limiting, cost attribution, failover across providers
- Design an inference batching system maximizing GPU utilization under a latency SLA
- Design an agent platform with sandboxed tool execution and full replay
- Design an evaluation pipeline that gates prompt and model changes in CI
- Design a multilingual semantic search system with LLM-based query understanding
- Design a system to detect and mitigate hallucination in production
- Design cost attribution for LLM spend across 40 teams
16. Context, Prompt & Graph Engineering
16.1 The distinction that matters
Prompt engineering is how you ask. Context engineering is what the model knows, sees, and remembers at the moment it acts. The 2026 framing: prompt engineering is table stakes; context engineering is the multiplier. Prompts control interaction design (format, tone, reasoning strategy, decomposition). Context controls knowledge infrastructure (definitions, entity relationships, access policies, lineage, freshness).
The symptom that means you've outgrown prompting: your prompt template has grown past ~2,000 tokens of business rules and exceptions, and each new edge case adds a paragraph. That's knowledge encoded in the wrong place.
16.2 The context pipeline
- Source curation — which systems are authoritative, and what's explicitly stale
- Retrieval strategy — how the right slice reaches the model at the right time
- Ranking — freshness, authority, proximity to the task
- Compression — summarization, extraction, dropping the irrelevant
- Assembly — ordering (put critical instructions at the start and end, not the middle), delimiters, structure
- Feedback — measure what context was actually used and prune what wasn't
16.3 Context window management
- Budget the window explicitly: system instructions / tools / retrieved context / history / output reserve. Write the budget down.
- "Lost in the middle" — recall degrades for content in the middle of long contexts. Order matters more than most people assume.
- Context rot: more context is not better. Past a point, added context reduces accuracy.
- Compaction strategies: rolling summarization, hierarchical summarization, structured state extraction (keep a JSON state object rather than raw history)
- Sub-agent isolation: give each sub-agent only the slice it needs. Prevents context pollution and cuts cost.
- Tool output truncation with a "fetch more" affordance rather than dumping everything
16.4 Prompt engineering as an engineering discipline
- Version prompts in git, not in a database field
- Prompts get tests. A prompt change without an eval run is an unreviewed deploy.
- Techniques worth knowing precisely: few-shot (and how example selection matters more than example count), chain-of-thought, self-consistency (sample n, majority vote), decomposition, role assignment, output schema constraints
- Anti-patterns: negative instructions ("don't do X" underperforms "do Y"), stacked contradictory rules, examples that conflict with instructions
- Prompt injection defense at the prompt layer is weak. Defense belongs at the architecture layer.
16.5 Graph engineering
When a graph beats a vector index
- Multi-hop reasoning ("which customers are affected by an outage in the service that depends on X")
- Relationship-heavy domains: org structures, supply chains, citations, entitlements, fraud rings
- When explanation matters — a traversal path is auditable, a cosine similarity isn't
Core concepts
- Property graph (Neo4j, Cypher) vs RDF/triple store (SPARQL); Gremlin/TinkerPop as the portable traversal language
- Entity resolution and deduplication — the hardest part of any real knowledge graph
- Ontology/schema design, and the governance problem of who owns it
- Traversal cost, supernode problem, index-free adjacency
- Graph databases at scale: Neo4j, Amazon Neptune, TigerGraph, JanusGraph; graph queries on top of relational (recursive CTEs) when the graph is small
GraphRAG
- Build a graph from documents (entity + relation extraction with an LLM), cluster into communities, generate community summaries
- Query time: local search (entity neighborhood) vs global search (community summaries) — know when each applies
- Cost reality: graph construction is expensive and brittle. Have an opinion on when it's worth it (high-value, stable corpora with relational questions) and when it isn't (large, churning document sets with lookup-style questions).
- Hybrid: vector retrieval for candidates, graph traversal for expansion and verification
Agent graphs (different thing, same word)
- LangGraph-style state machines: nodes, edges, conditional routing, cycles
- Checkpointing for resumability, human-in-the-loop interrupts
- DAG vs cyclic: cycles allow retry/reflection, and require explicit termination conditions
- Why an explicit graph beats a free-form agent loop in production: it's testable, observable, and bounded
17. Advanced Backend: Streaming, Reactive, APIs
17.1 Streaming systems
Kafka (depth beyond the basics)
- Exactly-once: idempotent producer (PID + sequence numbers) + transactions +
read_committedconsumers. Know that it's exactly-once within Kafka, not end-to-end. - ISR,
min.insync.replicas,acks=all, unclean leader election and the durability tradeoff - Log compaction vs retention; compacted topics as changelog/state
- Partition count as a scaling and ordering decision; ordering is per-partition only
- Consumer group rebalancing: eager vs cooperative sticky; static membership to avoid rebalance storms
- Consumer lag as the primary health metric; lag-based autoscaling (KEDA)
- KRaft (ZooKeeper removal), tiered storage
- Schema Registry: Avro/Protobuf/JSON Schema, compatibility modes (backward, forward, full, transitive) — a real design decision
Flink / stream processing
- Event time vs processing time vs ingestion time
- Watermarks: how late data is handled, allowed lateness, side outputs for the truly late
- Windows: tumbling, sliding, session, global
- State backends (heap vs RocksDB), checkpointing (Chandy-Lamport), savepoints for upgrades
- Exactly-once sinks via two-phase commit
- Kafka Streams vs Flink vs Spark Structured Streaming — know the tradeoff (operational simplicity vs power vs batch/stream unification)
Patterns
- CDC with Debezium; the dual-write problem and the outbox pattern as its fix
- Backfill and reprocessing: the Lambda vs Kappa architecture argument, and how you actually replay
- Dead letter queues, poison pill handling, retry topics with backoff tiers
- Idempotent consumers — required, always
17.2 Java reactive & Spring
Reactive Streams spec
Publisher/Subscriber/Subscription/Processor- Backpressure via
request(n)— this is the whole point; if you can't explain demand signalling, you don't know reactive - Backpressure strategies: buffer, drop, latest, error
Project Reactor
MonovsFlux; cold vs hot publishers- Operators:
map/flatMap/concatMap/flatMapSequential(concurrency and ordering differ — a classic interview question) publishOnvssubscribeOn— which part of the chain moves threads- Schedulers:
parallel,boundedElastic,single,immediate - Context propagation (
Context/ContextView), and why MDC logging breaks in reactive - Error handling:
onErrorResume,onErrorMap,retryWhenwith backoff - Testing with
StepVerifierandVirtualTimeScheduler
Spring WebFlux
- Netty event loop model; never block on an event-loop thread — this is the #1 production bug in reactive Spring
- WebFlux vs Spring MVC: when reactive actually wins (high-concurrency I/O-bound, streaming, many slow downstreams) and when it's pure cost (CPU-bound, simple CRUD, small team)
- R2DBC vs JDBC; reactive Redis, Mongo, Cassandra drivers
WebClientvsRestClient; connection pooling and timeouts
Virtual threads (Loom, JDK 21+) — have a strong opinion here
- Virtual threads give you scalability with blocking-style code. For many services, this removes the main reason to adopt reactive.
- Pinning:
synchronizedblocks and native calls pin a virtual thread to a carrier thread. UseReentrantLock. - Reactive still wins for: true streaming semantics, backpressure across a network boundary, complex async composition.
- The lead-level answer: "For a new service on JDK 21+, I'd default to virtual threads and structured concurrency. I'd choose reactive only for streaming with real backpressure requirements." That framing lands well.
17.3 GraphQL & Netflix DGS
- Schema design: nullability as a contract, interfaces vs unions, pagination (Relay cursor connections), error handling (errors array vs union result types)
- N+1 and DataLoader — batching and per-request caching. Expect to be asked to explain the fix in detail.
- Federation (Apollo Federation v2): subgraphs,
@key, entity resolution across services, supergraph composition,@requires/@provides/@external - Netflix DGS: annotation-driven (
@DgsComponent,@DgsQuery,@DgsData), codegen from SDL,DgsDataLoader, Spring Boot integration, instrumentation hooks, federation support - Security & cost control: query depth limiting, complexity/cost analysis, persisted queries (APQ), disabling introspection in prod, field-level authorization
- Caching is the hard part — no HTTP cache semantics. Solutions: persisted queries with GET + CDN, response cache keyed on query hash + variables + auth context,
@cacheControlhints - Subscriptions over WebSocket (graphql-ws) or SSE
- When not to use GraphQL: internal service-to-service (use gRPC), simple CRUD with one consumer, file uploads, when your team has no schema governance capacity
17.4 gRPC & binary protocols
- Protobuf wire format: field numbers as the contract, varint encoding, why you never reuse a field number,
reserved - Compatibility rules: adding optional fields is safe, changing types isn't, renaming is fine (names aren't on the wire)
- HTTP/2: multiplexing, header compression (HPACK), flow control, and why head-of-line blocking still exists at the TCP layer (hence HTTP/3 and QUIC)
- Four call types: unary, server streaming, client streaming, bidirectional
- Deadlines and cancellation propagation — gRPC does this properly and REST usually doesn't. Big talking point.
- Interceptors for auth, tracing, retries; metadata as the header equivalent
- Load balancing: client-side (
pick_first,round_robin), lookaside LB, xDS/Envoy integration. Note that L4 load balancers break gRPC because of long-lived HTTP/2 connections — a favorite gotcha question. - gRPC-Web and Connect for browser clients; grpc-JSON transcoding via Envoy for REST compatibility
- Alternatives and when: Thrift (legacy, Meta), Avro (schema-in-data, Kafka-friendly), Cap'n Proto / FlatBuffers (zero-copy, very low latency), MessagePack (schemaless binary JSON)
- Schema governance:
buffor lint and breaking-change detection in CI. Mentioning this signals you've run this at scale.
17.5 API decision matrix (have this ready)
| Need | Choose |
|---|---|
| Public API, broad client support, cacheable | REST + HTTP caching |
| Internal service-to-service, low latency, strong typing | gRPC |
| Many client-driven shapes, mobile bandwidth constraints, aggregation across services | GraphQL (federated) |
| Streaming server→client | SSE (simple) or gRPC server streaming |
| Bidirectional real-time | WebSocket or gRPC bidi |
| Event-driven decoupling | Kafka / event bus |
17.6 Distributed transaction patterns
- Saga: orchestration (central coordinator, easier to reason about) vs choreography (event-driven, less coupling, harder to debug)
- Compensating transactions and why they're not rollbacks
- Outbox and inbox patterns
- Event sourcing + CQRS: append-only log, projections, snapshots, replay. Know the costs — schema evolution of events is brutal, and most teams don't need it.
- Idempotency keys as a first-class API concept
18. Real-Time Delivery, CDN, Caching & Edge
18.1 WebSockets
- Handshake: HTTP Upgrade → 101 Switching Protocols; frames, masking, ping/pong keepalive
- Scaling: connections are stateful, so you need either sticky routing or a shared pub/sub backplane (Redis, NATS, Kafka) to fan out across nodes
- Resource math: ~10–50 KB per connection; a single node handles 10k–100k connections depending on buffer tuning. Know
ulimit -n, ephemeral port exhaustion, and conntrack table limits. - LB gotchas: idle timeouts kill connections silently. Configure keepalive shorter than the LB timeout.
- Reliability: reconnect with exponential backoff + jitter, resume via sequence number / last-message-id, at-least-once delivery with client-side dedupe
- Presence and fan-out patterns: room-based sharding, topic routing
18.2 Server-Sent Events
- Unidirectional server→client over plain HTTP, auto-reconnect built in,
Last-Event-IDfor resume - Works with existing HTTP infrastructure (proxies, auth, compression) — the reason it beat WebSockets for LLM token streaming
- HTTP/1.1 6-connection-per-origin limit is the classic gotcha; HTTP/2 multiplexing solves it
- Buffering by intermediate proxies breaks SSE — disable proxy buffering explicitly (
X-Accel-Buffering: nofor nginx)
| WebSocket | SSE | Long polling | WebTransport | |
|---|---|---|---|---|
| Direction | Bidi | Server→client | Both (inefficient) | Bidi |
| Protocol | Custom over TCP | Plain HTTP | HTTP | HTTP/3 (QUIC) |
| Auto-reconnect | Manual | Built-in | N/A | Manual |
| Infra friendliness | Poor | Excellent | Good | Emerging |
| Best for | Chat, collab, games | Token streaming, notifications, progress | Legacy fallback | Low-latency multiplexed streams |
18.3 HTTP caching (know the headers exactly)
Cache-Control:max-age,s-maxage,public/private,no-cache(revalidate) vsno-store(never store),immutablestale-while-revalidateandstale-if-error— the two directives that most improve real-world availability- Validators:
ETag(strong vs weak) +If-None-Match,Last-Modified+If-Modified-Since Varyand whyVary: *orVary: User-Agentdestroys your hit rate- Cache key design: which query params, headers, and cookies participate. Over-inclusive keys are the #1 cause of poor hit rates.
18.4 CDN architecture
- PoP hierarchy, origin shield / tiered caching to protect the origin
- Purge: hard purge vs soft purge (serve stale while refetching) vs surrogate keys / cache tags for group invalidation. Tag-based invalidation is the answer for content with complex dependencies.
- Cache stampede / thundering herd mitigation: request coalescing at the edge, jittered TTLs, probabilistic early expiration (XFetch), lock-and-refresh
- Edge compute: Cloudflare Workers, Lambda@Edge / CloudFront Functions, edge KV. Use for auth checks, A/B assignment, personalization at the edge, header rewriting.
- Personalization vs caching: split the page (cacheable shell + dynamic fragments), or use edge-side includes / streaming SSR
- Security at the edge: WAF, bot management, rate limiting, DDoS absorption via anycast
- Cache poisoning: unkeyed input in the cache key is the vulnerability class to know
18.5 Application caching layers
Layer the answer in interviews — most candidates only name Redis.
- Browser / HTTP cache
- CDN / edge
- API gateway response cache
- In-process cache (Caffeine, Guava) — nanoseconds, but per-instance, so consistency is the tradeoff
- Distributed cache (Redis, Memcached)
- Database buffer pool / materialized views
Patterns: cache-aside (most common), read-through, write-through (consistency, higher write latency), write-behind (throughput, risk of loss), refresh-ahead
Redis specifics
- Eviction policies:
allkeys-lru,volatile-ttl,noeviction— and what happens when you pick wrong - Cluster mode, hash slots, and why multi-key ops break across slots (hash tags fix this)
- Hot key mitigation: client-side local cache, key splitting, read replicas
- Pipelining vs Lua scripts vs transactions
- Persistence: RDB vs AOF, and that Redis is not a database
- Redis vs Memcached: data structures and persistence vs raw simplicity and multithreaded throughput
Invalidation
- TTL-only (simple, stale windows) vs event-driven (correct, complex) vs versioned keys (change the key, never invalidate)
- Negative caching to protect against lookup floods on missing keys
- The cardinal rule to state out loud: cache invalidation bugs are consistency bugs, so choose the staleness you can tolerate before choosing a cache.
19. Compute: Serverless, Containers, Kubernetes, OS & Kernel
19.1 Serverless
- Lambda cold start anatomy: download code → init runtime → run init code → invoke. Reduce with smaller packages, lazy imports, provisioned concurrency, SnapStart (JVM).
- Memory setting controls CPU allocation — undersizing memory to save money often costs more because duration rises
- Concurrency: reserved vs provisioned; account-level limits; throttling behavior and how it interacts with SQS/Kinesis event sources
- VPC-attached Lambdas: ENI setup used to dominate cold start; know the current hyperplane ENI model
- Connection management: never open a DB connection per invocation — RDS Proxy or a data API
- Orchestration: Step Functions (state machine, retries, error handling), EventBridge (routing/schema registry), SQS/SNS fan-out, DLQs everywhere
- Anti-patterns: chatty function-to-function calls, long-running jobs, functions as a monolith, distributed transaction spaghetti
- Decision framework: Lambda for spiky, event-driven, short work. Fargate for containerized services with variable load and no cluster ops. EKS/K8s when you need scheduling control, multi-tenancy, or portability. EC2 for GPU, specialized hardware, or extreme cost optimization at steady scale.
- Cost crossover: serverless wins below roughly 30–40% steady utilization; always-on compute wins above it. Be able to do this math out loud.
19.2 Containers & the kernel underneath
- Namespaces:
pid,net,mnt,uts,ipc,user,cgroup— isolation - cgroups v2 — resource limits (CPU, memory, IO, pids)
- Union filesystems (OverlayFS), copy-on-write, layer caching
- Security primitives: seccomp profiles, Linux capabilities (drop
ALL, add back what you need), AppArmor/SELinux, read-only root filesystem, non-root user - Runtimes:
runc(default),gVisor(userspace kernel, stronger isolation),Firecracker(microVM — what Lambda and Fargate run on),Kata - Image hygiene: multi-stage builds, distroless/scratch base, pinned digests not tags, SBOM generation, vulnerability scanning in CI
19.3 Kubernetes
Control plane
- API server (the only thing that talks to etcd), etcd (Raft, watch semantics), scheduler, controller-manager, kubelet, kube-proxy
- The reconciliation loop is the whole mental model: declared state vs observed state, controllers converging continuously
Scheduling & resources
- Requests vs limits; QoS classes (Guaranteed, Burstable, BestEffort) and eviction order
- CPU limits cause CFS throttling — a top production surprise. Many teams set CPU requests and no CPU limits deliberately. Have an opinion.
- Memory limits cause OOMKill, not throttling — memory is incompressible
- Affinity/anti-affinity, taints/tolerations, topology spread constraints, PodDisruptionBudgets
- Priority classes and preemption
Autoscaling
- HPA (metrics-driven pod count), VPA (right-sizing, conflicts with HPA on the same metric), Cluster Autoscaler vs Karpenter (Karpenter provisions right-sized nodes directly — the modern answer), KEDA (event-driven scaling on queue depth, Kafka lag, etc.)
Networking
- CNI plugins; flat pod network requirement
- Service types (ClusterIP, NodePort, LoadBalancer), kube-proxy modes (iptables → IPVS → eBPF/Cilium and why iptables degrades at scale)
- Ingress vs the Gateway API (Gateway API is the direction of travel)
- NetworkPolicy for east-west segmentation — the zero-trust building block inside the cluster
- Service mesh: sidecar (Istio/Linkerd) vs ambient/sidecarless; mTLS, retries, circuit breaking, traffic splitting. Know the latency and resource cost — mesh is not free.
Stateful & storage
- StatefulSets, PVCs, CSI drivers, StorageClass, volume expansion
- Operators/CRDs for stateful systems; when running a database on K8s is reasonable and when it isn't
Delivery
- Helm vs Kustomize; GitOps with Argo CD or Flux; progressive delivery (Argo Rollouts, Flagger) with canary + automated rollback on SLO breach
Debugging checklist to recite
CrashLoopBackOff → check logs and previous logs, init containers, probe config, missing config/secret
OOMKilled → memory limit, actual usage, JVM heap vs container limit mismatch
Pending → resource requests unschedulable, node selectors, taints, PVC unbound
ImagePullBackOff → registry auth, tag typo, rate limits
Slow but healthy → CPU throttling, noisy neighbor, probe misconfiguration, DNS (ndots:5 causing 5 lookups per query — a classic)
19.4 OS & kernel fundamentals
- Process vs thread vs coroutine; context switch cost (~1–5 µs); scheduler basics (CFS, and EEVDF in newer kernels)
- Memory: virtual memory, page tables, TLB, page cache, major vs minor faults, huge pages, NUMA locality, swap and why you disable it for latency-sensitive services, the OOM killer and
oom_score_adj - I/O models: blocking, non-blocking, multiplexed (
select/poll/epoll/kqueue), async (io_uring), and zero-copy (sendfile,splice,mmap) - Durability: page cache vs
fsync, write barriers, and why "the write returned" doesn't mean "the data is safe" - Networking stack: three-way handshake, TIME_WAIT,
SO_REUSEADDR, backlog queues (somaxconn, accept queue overflow), Nagle vsTCP_NODELAY, congestion control (CUBIC vs BBR), receive/send buffer autotuning, MTU and PMTU discovery, ephemeral port and conntrack exhaustion - eBPF: safe in-kernel programs for observability (
bpftrace, bcc), networking (Cilium), and security (Tetragon, Falco). Knowing eBPF is a strong staff-level signal. - Performance methodology: USE (Utilization, Saturation, Errors) for resources; RED (Rate, Errors, Duration) for services. State the method before the tool.
- Tooling:
perf+ flame graphs,strace,ltrace,tcpdump,ss,vmstat,iostat,pidstat,bpftrace - JVM in containers (if relevant): container-aware heap sizing, GC choice (G1 default, ZGC/Shenandoah for low pause), JFR, async-profiler, why
-Xmxat 100% of the container limit gets you OOMKilled
20. Storage, Scaling & Data Platform
20.1 DynamoDB
- Access patterns first, schema second. Say this before anything else; it's the answer they're listening for.
- PK/SK design, composite keys, single-table design and its real tradeoff (query efficiency vs comprehension cost)
- GSIs (eventually consistent, own capacity, projections matter) vs LSIs (strongly consistent, 10 GB per partition limit, must be created at table creation)
- Sparse indexes as a query pattern
- Limits: 400 KB item, 10 GB per partition key for LSI, 1 MB per query result page
- Hot partitions and adaptive capacity; write sharding by suffixing the key
- Capacity: on-demand vs provisioned + auto-scaling; throttling and exponential backoff with jitter
- Transactions (
TransactWriteItems, 100-item limit, 2× cost), condition expressions for optimistic concurrency - Streams → Lambda for CDC; TTL for expiry; PITR; global tables (multi-region active-active, last-writer-wins — know that conflict resolution is not configurable)
- Cost model: RCU/WCU, and that a badly modeled table costs 10× a well-modeled one
20.2 Cassandra / ScyllaDB
- Ring topology, vnodes,
NetworkTopologyStrategy, replication factor per DC - Tunable consistency:
ONE/QUORUM/LOCAL_QUORUM/ALL;R + W > RFfor strong consistency;LOCAL_QUORUMis the practical default multi-DC choice - Repair mechanisms: hinted handoff, read repair, anti-entropy repair (and that skipping repair causes resurrection of deleted data past
gc_grace_seconds) - Partition key design; wide partitions and the size ceiling (~100 MB / 100k rows as a rule of thumb)
- Tombstones — deletes create markers, range scans over tombstones cause timeouts. This is the Cassandra gotcha; know it.
- Compaction strategies: STCS (write-heavy), LCS (read-heavy, higher write amp), TWCS (time series)
- LWT (Paxos) is 4× the round trips — use sparingly
- Scylla: C++ rewrite, shard-per-core, same API, much better tail latency
- When Cassandra: high write throughput, multi-DC active-active, known query patterns, linear scale. When not: ad hoc queries, joins, strong transactional needs.
20.3 MongoDB
- Modeling: embed for one-to-few and read-together; reference for one-to-many, unbounded growth, or independent access. 16 MB document limit forces the decision.
- Replica sets: primary election (Raft-like), oplog, write concern (
w:majority,j:true), read concern (local/majority/linearizable/snapshot), read preference, causal consistency sessions - Sharding: shard key is nearly irreversible — evaluate cardinality, frequency, and monotonicity. Monotonic keys create a hot shard; hashed sharding fixes distribution but kills range queries.
- Chunk balancing, jumbo chunks, zone sharding for data residency
- Indexes: compound index ESR rule (Equality, Sort, Range ordering), partial, TTL, text, wildcard; covered queries
- Aggregation pipeline,
$lookuplimitations (no sharded-collection joins in older versions, no true optimizer) - Change streams for CDC; multi-document transactions exist but cost you the reason you picked Mongo
20.4 OpenSearch / Elasticsearch (operational depth)
- Node roles: master, data (hot/warm/cold/frozen), ingest, coordinating, ML
- Shard sizing: 10–50 GB per shard as a working range; over-sharding is the most common cluster killer
- JVM heap: 50% of RAM, under ~32 GB to keep compressed object pointers; the other half goes to the OS page cache for Lucene
- Mapping:
keywordvstext, dynamic mapping explosion,index: falsefor unqueried fields,doc_valuesfor aggregations, disable_sourceonly if you understand the consequences - Search phases: query-then-fetch; deep pagination cost;
search_after+ PIT instead offrom/size;scrollis legacy - Refresh vs flush vs merge;
refresh_intervaltuning for bulk indexing (set to-1during bulk loads) - Zero-downtime reindex: build new index → alias swap → verify → drop old. Rehearse this; it's a common design answer.
- ILM/rollover for time-series, snapshot/restore, cross-cluster replication and cross-cluster search
- Failure modes: circuit breaker trips, thread pool rejections (
search/writequeues), unbalanced shards, mapping conflicts, split-brain (pre-7.x quorum config) - k-NN: Lucene HNSW vs nmslib/faiss engines,
ef_search/ef_construction/m, scalar and product quantization, disk-based vector search for memory relief, filtered kNN and the pre/post-filter recall tradeoff
20.5 Relational at scale
- Postgres: MVCC, tuple bloat, autovacuum tuning, transaction ID wraparound (the incident that takes down unmonitored clusters)
- Index types: B-tree, GIN (JSONB, full text), GiST, BRIN (huge append-only tables), partial and covering (
INCLUDE) indexes - Isolation levels and the anomalies each permits (dirty read, non-repeatable read, phantom, write skew); Postgres
REPEATABLE READis snapshot isolation, which allows write skew — a great depth question - Connection pooling: PgBouncer transaction pooling and what it breaks (prepared statements, session state)
- Partitioning (declarative, by range/list/hash), and partition pruning
- Replication: streaming (physical) vs logical; replica lag and how the app handles read-after-write (sticky-to-primary for N seconds, or read-your-writes tokens)
- Sharding: Vitess, Citus, or application-level; the resharding problem is why you delay this as long as possible
- Online schema change:
pt-online-schema-change/gh-ost(MySQL),CREATE INDEX CONCURRENTLY, expand-contract migration pattern
20.6 Analytics & lakehouse
- Columnar formats: Parquet/ORC, row groups, column pruning, predicate pushdown, compression codecs
- Table formats: Iceberg (snapshot isolation, hidden partitioning, time travel, schema evolution), Delta Lake, Hudi. Iceberg is the default answer in 2026.
- Small file problem and compaction; partition evolution
- Warehouses: Snowflake (micro-partitions, clustering keys, warehouse sizing and auto-suspend for cost), BigQuery (slots, partitioning + clustering), Redshift
- Medallion architecture (bronze/silver/gold), dbt for transformation, incremental models
- Data contracts, quality gates, lineage — increasingly a lead's responsibility because AI systems consume this data
20.7 Storage decision table
| Access pattern | Store |
|---|---|
| Known key, single-digit ms, huge scale | DynamoDB / Cassandra |
| Complex queries, transactions, joins | Postgres / MySQL |
| Flexible documents, evolving schema | MongoDB |
| Full-text + facets + relevance | OpenSearch |
| Semantic / similarity | Vector index (HNSW) |
| Relationship traversal, multi-hop | Graph DB |
| Time series, high write, range queries | Timescale / InfluxDB / Cassandra TWCS |
| Analytics over history | Iceberg on object storage + query engine |
| Ephemeral, sub-ms | Redis |
| Immutable log, replay, fan-out | Kafka |
21. Security: OAuth, Zero Trust, AppSec & Network
21.1 OAuth 2.0 / 2.1 and OIDC
Grants and which are alive
- Authorization Code + PKCE — the only correct choice for any public client (SPA, mobile, CLI). PKCE now recommended for confidential clients too.
- Client Credentials — service-to-service
- Device Authorization — TVs, CLIs, constrained input
- Dead: Implicit (token in URL fragment, leaks), Resource Owner Password Credentials (the app sees the password)
Tokens
- Access token (what you present), refresh token (what you exchange), ID token (OIDC only — an assertion about who, not an authorization to do)
- JWT vs opaque: JWT is stateless and fast but hard to revoke; opaque needs introspection but gives you instant revocation. Common answer: short-lived JWTs (5–15 min) + refresh with rotation.
- Validation checklist: signature against JWKS,
iss,aud,exp,nbf,algallowlist,kidhandling and key rotation - Refresh token rotation with reuse detection — if an old refresh token is replayed, revoke the whole family
- Sender-constrained tokens: DPoP or mTLS-bound tokens. Prevents a stolen bearer token from being usable. This is the "above and beyond" answer.
- Token exchange (RFC 8693) for delegation and on-behalf-of flows in service chains
Vulnerabilities to name
- Redirect URI wildcards / open redirect chaining
- Missing or unbound
state→ CSRF on the callback - Mix-up attacks in multi-IdP setups (validate
issin the response) - Tokens in URLs (referrer leakage, logs, browser history)
alg: noneand algorithm confusion (RS256 verified as HS256 with the public key as the secret)- Overly broad scopes; scope != permission — do authorization in your own layer
Adjacent
- OIDC vs SAML: SAML for legacy enterprise SSO, OIDC for everything new; SCIM for user provisioning/deprovisioning
- Sessions:
HttpOnly,Secure,SameSite=Lax|Strict,__Host-prefix, back-channel logout - Authorization models: RBAC → ABAC → ReBAC (Zanzibar/OpenFGA/SpiceDB). Know when relationship-based access control is the right answer (nested resources, sharing graphs) — this is a strong staff-level signal.
21.2 Zero Trust
- Principles: never trust based on network location; verify explicitly per request; assume breach; least privilege; continuous evaluation
- NIST SP 800-207 model: Policy Decision Point + Policy Enforcement Point, policy engine, trust algorithm; every request is authenticated, authorized, and encrypted
- Identity-aware proxy (the BeyondCorp pattern) replacing the VPN perimeter
- Device posture as an input signal: managed device, patch level, disk encryption, EDR present
- Workload identity: SPIFFE/SPIRE, cloud IAM roles for service accounts (IRSA/Workload Identity). No long-lived static credentials anywhere.
- Microsegmentation: NetworkPolicy in K8s, security groups, service mesh authorization policies
- mTLS everywhere with automated cert rotation
- Just-in-time and just-enough access; break-glass with audit
- ZTNA vs VPN, and SASE as the packaging (SD-WAN + SWG + CASB + ZTNA + FWaaS)
- The honest lead answer: zero trust is a multi-year program, and the sequencing is identity → device → workload → network → data. Say the sequencing.
21.3 Application security
- OWASP Top 10 (web), OWASP API Security Top 10 (BOLA/IDOR is #1 and the one that actually gets exploited), OWASP LLM Top 10
- SSRF and cloud metadata (169.254.169.254) — enforce IMDSv2, egress allowlists, and URL validation that resolves DNS before fetching
- Injection beyond SQL: NoSQL, LDAP, command, template (SSTI), header injection
- Insecure deserialization; XXE; path traversal; mass assignment
- Crypto: never roll your own; AES-GCM for symmetric, envelope encryption with KMS, Argon2id/bcrypt for passwords, constant-time comparison, proper IV/nonce handling
- Supply chain: SBOM (SPDX/CycloneDX), SLSA levels, artifact signing (sigstore/cosign), dependency confusion, typosquatting, lockfile pinning, provenance attestation
- Secrets: Vault or cloud secret manager, dynamic short-lived credentials, rotation automation,
gitleaks/trufflehogin CI, and secrets scanning on history - Pipeline: SAST + SCA + DAST + IaC scanning (Checkov, tfsec) + container scanning (Trivy/Grype) + admission control (OPA Gatekeeper, Kyverno)
- Threat modeling with STRIDE and data flow diagrams. Producing a threat model for a design is a genuine differentiator — almost no candidates offer one unprompted.
21.4 Vulnerability management & patching
- CVE identifies, CVSS scores severity, EPSS predicts exploitation probability, CISA KEV lists what's actually being exploited. The mature prioritization answer is: KEV first, then EPSS × exposure, then CVSS. Saying "we patch all criticals in 7 days" without exposure context is the junior answer.
- Patch SLA tiers keyed to severity and internet exposure and data sensitivity
- Golden base image pipeline: rebuild on upstream CVE, redeploy immutably, never patch in place
- Runtime posture: drift detection, unauthorized process alerts (Falco/Tetragon)
- Zero-day response runbook: inventory (what do we run, where) → exposure assessment → mitigate (WAF rule, feature flag, network block) → patch → verify → communicate. Practice narrating this; incident rounds love it.
- The lead-level point: your patching velocity is bounded by your inventory accuracy. Most orgs fail at step one.
21.5 Network, SDN & SD-WAN
- SDN: separation of control plane and data plane, centralized controller, programmable forwarding (OpenFlow historically; today mostly vendor controllers and eBPF/XDP in the datacenter)
- Overlays: VXLAN, GENEVE, and why overlays exist (multi-tenancy, L2 over L3, mobility)
- SD-WAN: application-aware path selection across MPLS/broadband/LTE, dynamic failover on jitter/loss, centralized policy, zero-touch provisioning. The business case is MPLS cost replacement plus direct-to-cloud breakout.
- SASE: SD-WAN converged with security services delivered from the cloud edge
- Cloud networking: VPC/subnet design, NAT gateways and egress cost, Transit Gateway vs peering, PrivateLink for private service access, VPC endpoints to keep traffic off the internet, egress filtering as a data exfiltration control
- Load balancing layers: L4 (NLB, fast, connection-level) vs L7 (ALB/Envoy, routing, retries, header-based); global with anycast + GeoDNS
- DNS: resolution path, TTL strategy for failover, health-check-based routing, split-horizon, DNSSEC. DNS is the most common cause of "the whole thing is down."
- TLS: 1.3 handshake (1-RTT, 0-RTT with replay risk), cipher suites, cert lifecycle automation (ACME/cert-manager), OCSP stapling, mTLS for service identity
- DDoS: volumetric (L3/4, absorb with anycast + scrubbing) vs application-layer (L7, needs WAF + rate limiting + bot detection)
21.6 Compliance as engineering constraints
SOC 2, ISO 27001, GDPR/PIPEDA, HIPAA, PCI-DSS, data residency. What a lead actually needs: know which controls turn into engineering work (access reviews, audit logging, encryption at rest and in transit, retention and deletion, change management evidence, vendor review) and how to build them once rather than per-audit.
22. Frontend & Full-Stack for Leads
You won't be asked to build a UI, but you will be asked to make architecture decisions and review frontend work credibly.
22.1 React
- Rendering model: reconciliation, the key prop and why index-as-key breaks lists, StrictMode double-invocation in dev
- Hooks: rules of hooks and why (call order),
useEffectdependency traps, cleanup functions,useMemo/useCallbackas targeted optimizations rather than defaults,useReffor non-rendering state - Concurrent React:
startTransition,useDeferredValue, Suspense boundaries, streaming SSR with selective hydration - Server Components: what runs where, the
"use client"boundary, why RSC reduces bundle size, serialization constraints across the boundary - Performance: virtualization for long lists, route-based code splitting, bundle analysis, avoiding context-induced re-render cascades (split contexts or use a store)
- Error boundaries; hydration mismatch causes
22.2 State layers — the question behind the question
The lead-level insight: most state problems are caused by treating server data and UI state as the same thing. They have different lifecycles, different invalidation rules, and different owners.
The ladder (climb only as far as you need):
- Local
useState - Lifted state
- Context (for low-frequency, wide-read values: theme, auth, locale — not for hot state)
- Global client store (Zustand, Jotai, Redux Toolkit)
- Server cache library (TanStack Query, SWR, RTK Query)
Redux specifics (they will ask)
- Store, actions, reducers, immutability; Redux Toolkit + Immer removed most of the boilerplate objection
- Middleware layer: thunk for simple async (fire, await, dispatch), saga for complex orchestration with cancellation, concurrency control, and long-running flows, observable for stream-heavy needs. Default to thunk; reach for saga only when you need cancellation and choreography.
- Normalization with entity adapters; memoized selectors (
reselect) to prevent re-render storms - The honest opinion to voice: a large share of Redux stores in production are hand-rolled server caches. Moving server state to TanStack Query and keeping Redux for genuine UI state usually deletes half the store.
Vue (if the stack is Vue)
- Composition API vs Options API;
refvsreactiveand the unwrapping rules - Reactivity via Proxy in Vue 3 (vs
Object.definePropertyin Vue 2 — explains why Vue 2 couldn't detect array index and property additions) computedvswatchvswatchEffect- Pinia over Vuex; Nuxt for SSR/SSG
- Team-level tradeoff: Vue has a gentler ramp and stronger conventions; React has a deeper hiring pool and ecosystem. Frame it as a hiring and maintenance decision, not a taste one.
22.3 Cross-cutting frontend architecture
- Rendering strategy matrix: CSR (app-like, SEO-irrelevant) / SSR (dynamic + SEO) / SSG (static content) / ISR (mostly static, periodic refresh) / streaming SSR (large pages, perceived speed)
- Core Web Vitals: LCP (loading), INP (interactivity — replaced FID), CLS (stability). Know RUM vs lab measurement and that field data is what ranks.
- Micro-frontends and Module Federation: legitimate when independent teams deploy independently at real scale; usually a net cost otherwise. Having the skeptical-but-informed take is the senior signal.
- Design systems and tokens; accessibility (WCAG 2.2 AA, semantic HTML, keyboard navigation, focus management, ARIA only when semantics fail)
- Frontend security: XSS and why
dangerouslySetInnerHTMLneeds sanitization, CSP with nonces, CSRF and SameSite, clickjacking headers, third-party script risk - Build: Vite/esbuild/Turbopack, monorepos (Nx, Turborepo), incremental builds
- Testing: unit → component (Testing Library, test behavior not implementation) → E2E (Playwright) → visual regression. Have a stance on the ratio.
- Streaming AI UIs: SSE token rendering, optimistic states, "thinking" indicators, partial markdown parsing, citation rendering, and undo for agent-initiated actions
23. The Team Leadership Operating Playbook
This is where TL and Staff candidates most often sound generic. Everything below should become a concrete story or a concrete practice you can describe in 90 seconds.
23.1 Unblocking — your highest-leverage daily activity
- Classify every blocker: information (find the answer), decision (make it or force it), dependency (escalate or route around), skill (pair or reassign). Each has a different fix; treating them all the same is why teams stay stuck.
- Track decision latency. If a decision has sat more than 48 hours, that's your failure, not the team's.
- The 24-hour rule: nobody is blocked overnight without a named owner and a next action.
- Escalation is a tool, not a failure. Have an explicit ladder and use it early rather than heroically absorbing the delay.
23.2 Growing people
- Maintain a skill matrix: each engineer × each competency (domain, systems design, code quality, communication, ownership, mentoring). Assign work to close gaps deliberately, not by who's free.
- Stretch assignments at roughly 70% known / 30% new. More than that and you're setting up a failure.
- Delegation ladder: do it → do it and tell me → propose then do → decide and inform → own it entirely. Move people up one rung at a time and say out loud which rung they're on.
- Feedback with SBI (Situation, Behavior, Impact): specific, timely, behavioral. Praise publicly, correct privately.
- 1:1s: their agenda first, career every fourth one, never a status meeting. If your 1:1s are status updates you've lost the only private channel you have.
- Growth plans mapped to the next level's rubric, with named artifacts as evidence, not adjectives.
- Underperformance sequence: name the gap early → clarify expectations in writing → measurable plan with support → review → decide. The rule is no surprises at review time. If someone is surprised, that's a management failure.
23.3 PR review as a leadership lever
- Review SLA: first response within 4 business hours. Publish it. Review latency is usually the largest hidden cost in cycle time.
- Small PRs: under ~400 lines. Review quality collapses past that; large PRs get rubber-stamped.
- Comment taxonomy — adopt prefixes:
blocking:/suggestion:/nit:/question:/praise:. Cheap to introduce, disproportionately improves review culture, and removes the ambiguity that causes friction between seniors. - Automate everything mechanical: formatter, linter, import order, coverage thresholds. Humans should never comment on style.
- Review order: correctness → design and boundaries → tests → readability → nits. Say this ordering out loud in an interview.
- Rotate reviewers to spread context and prevent single-owner bottlenecks; pair-review for onboarding.
- Author obligations: PR description explaining why, test evidence, risk and rollback note, screenshots for UI.
- Anti-patterns to name: rubber-stamping, bikeshedding, the one-person gate, review used as territorial defense, and "LGTM" on a 2,000-line PR.
- AI-era shift: when AI generates a large share of the code, the bottleneck moves from writing to reviewing, and review queues back up in month two of adoption. Concrete counters: require authors to explain AI-generated code as their own, label AI-assisted PRs, monitor review queue depth and merge time as first-class metrics, and raise test coverage requirements on generated code.
23.4 Planning, estimation & scheduling
- Capacity math: headcount × available days − on-call − interviews − support rotation − meetings − holidays. Commit to 60–70% of that. Teams that commit to 100% miss every time.
- The three-bucket budget: features / reliability + tech debt / keep-the-lights-on. Publish the split (a common healthy target is 60/25/15) and defend the middle bucket explicitly with data, not vibes.
- Estimation: prefer historical cycle time distribution over story points. Forecast with percentiles ("85% confidence we finish by the 22nd"), not single dates. This one habit makes you sound more senior than almost anything else.
- Break down until the largest item is under a week; anything bigger is unestimated risk in disguise.
- Dependency mapping across teams and explicit critical path; renegotiate early, not at the deadline.
- Scope management: cut scope, not quality. Have the "what would we drop" conversation at the halfway mark, not the week before.
- On-call: sustainable rotation (never fewer than 6 people), page budget (if you're paging more than ~2×/week per person, fix the system), interrupt-shield rotation so the rest of the team gets flow time, and a toil budget with a cap.
- Roadmap sequencing: what unlocks what, what's reversible vs one-way-door, what can be a spike instead of a commitment.
23.5 Product partnership
- Your job in discovery is to supply options with costs, not verdicts: "A is two weeks; B is six but makes C nearly free; here's what I'd pick and why."
- Own the non-functional requirements. PMs almost never write latency, availability, or cost targets — if you don't, nobody will.
- Translate platform investment into product language: "this cuts our checkout p99 by 300 ms, which historically moves conversion by X."
- Know the product's north star metric and how your systems affect it. A lead who can't name it looks disconnected.
- Push back with data and alternatives, never with "that's not possible."
23.6 Promotions, calibration & retention
- Promotions are won two quarters early by assigning work that generates evidence at the target level. Waiting until packet season is how good engineers get stuck.
- Build the packet continuously: scope, impact with numbers, cross-team evidence, partner quotes, artifacts (design docs, incident reports, mentorship outcomes).
- Calibration: you'll argue for your person in a room of peers. Come with artifacts and comparisons to the rubric, not adjectives.
- For someone not ready: be specific about the gap and the timeline. Vagueness here is the cruelest thing a lead does.
- Retention: know each person's actual motivator (scope, learning, money, title, flexibility, teammates). Recognize before they ask. Most regretted attrition is visible three months out if you're paying attention.
23.7 Hiring
- Write the scorecard before the loop; interview to it; give written feedback within 24 hours
- Structured, consistent questions across candidates — otherwise you're measuring rapport
- Know how to say no to a "fine" candidate and articulate why. Bar defense is a lead responsibility.
- Onboarding: named buddy, a shipped change in week one, a 30/60/90 with explicit success criteria
23.8 Communication & meetings
- Written-first culture: design docs, RFCs, ADRs (architecture decision records). Decisions that aren't written down get relitigated.
- Meeting audit: no agenda, no meeting; no decision, shorter meeting. Replace status meetings with async updates.
- Managing up: a monthly one-pager to your director with progress, risks named early, and asks. Directors remember the leads who surface risk before it becomes news.
- Incident comms: fixed cadence updates, audience-appropriate detail, no speculation, and a blameless postmortem with action items that have owners and dates.
23.9 Metrics a lead should watch
- DORA: deployment frequency, lead time for changes, change failure rate, failed deployment recovery time, rework rate
- The 2026 caveat you should raise unprompted: DORA alone is now considered insufficient in AI-heavy teams. Where AI generates a large share of committed code, deployment frequency and lead time become misleading; recent industry data shows AI adoption improving throughput while degrading stability (higher change failure rates). Pair DORA with AI attribution, code durability, and a quality guardrail.
- DevEx/SPACE: flow state, cognitive load, satisfaction, interruption count
- Review queue depth, time-to-first-review, merge time
- Pages per person per week, alert noise ratio, toil percentage
- Escaped defect rate, incident MTTR, repeat-incident rate
24. AI as a Cross-Cutting Force
The section that makes you sound like you're operating in 2026 rather than 2022. Every topic above changes when a model is in the loop. Be able to speak to each.
24.1 The leadership frame
The current data is genuinely two-sided, and saying so is the credible position:
- Teams with high AI adoption report meaningfully better productivity, roadmap time, and satisfaction than ad hoc adopters.
- At the same time, AI adoption correlates with higher change failure rates — throughput up, stability down. DORA's own reporting has flagged this direction.
- Adoption is a change management problem, not a tooling problem. Uniform usage across a team is the hard part; individuals go rogue and gains don't compound.
Your interview answer should be: adopt aggressively, instrument honestly, and pair every speed gain with a quality guardrail. Name the risk that AI raises output volume while lowering the average reviewer's understanding of the code. Almost nobody says this, and it reads as judgment rather than enthusiasm.
24.2 Layer by layer
Coding & review — The bottleneck moves from writing to reviewing. Invest in tests, type systems, contracts, and static analysis, because those are the checks that scale when volume rises.
Testing — AI is good at generating edge cases and property-based tests; humans still own assertions about business rules. Mutation testing becomes more valuable because coverage numbers inflate cheaply.
System design — An LLM in the request path changes everything: latency budgets go from milliseconds to seconds, cost per request becomes variable, and outputs become non-deterministic. Design consequences: aggressive timeouts, streaming to mask latency, semantic caching, model fallbacks, and a graceful degradation path to a non-AI experience. Say "what happens when the model provider is down" before they ask.
APIs — Tool and function schemas are now a public API surface consumed by models. Version them, document them for model consumption, and treat MCP servers with the same rigor as any external API. Tool descriptions are prompt surface: a sloppy description is a production bug.
Data — Retrieval quality is a data quality problem. Lineage, freshness, ownership, and access control become model-visible, which means data governance stops being a compliance checkbox and becomes a correctness requirement.
Search & recsys — Hybrid retrieval, LLM-based query understanding, LLM reranking on the top-k, generated summaries as a new surface. Each of these needs new evaluation; your existing NDCG harness doesn't measure whether the generated summary was faithful.
Frontend — Streaming token UIs over SSE, partial rendering, latency masking, thinking states, citation UI, confidence display, and undo for agent-initiated actions. New UX primitives, new accessibility questions.
Security — Prompt injection is the new XSS, and indirect injection (via retrieved documents, tool output, scraped pages) is the dangerous variant. Treat model output as untrusted input everywhere. Scope agent credentials to least privilege with short-lived tokens. Agent identity becomes a real IAM problem.
Infrastructure — GPU capacity planning, inference autoscaling with minutes-long cold starts, cost per token as an SLO, and provider-diversity as a resilience strategy.
Observability — Traces must carry prompts, model version, token counts, tool calls, latency per step, and cost. Evals belong in CI. Standard error-rate monitoring misses the failure mode that matters: confidently wrong output with a 200 status code.
Incident response — AI failures are silent and probabilistic. You need eval regression alerts and output quality monitoring, not just error rates and latency graphs.
On-call & toil — AI is genuinely good at log triage, correlation, and runbook drafting. Keep human decision authority on mitigation. Automate the reading, not the deciding.
24.3 Questions you should have answers to
- How has AI changed how your team works? (specifics and numbers, not enthusiasm)
- How do you prevent AI from degrading code quality? (the guardrail answer: review capacity, test requirements, AI attribution in metrics)
- How do you measure AI ROI? (token cost vs cycle time vs change failure rate — the three-way tradeoff)
- Where do not you use AI? (having a crisp answer here is the strongest signal in the set)
- How do you handle an engineer whose AI-assisted output they can't explain?
- How would you roll AI tooling out to a skeptical team of 12?
25. Rapid-Fire Drill Bank
Answer each out loud in under 90 seconds. If you can't, that's your study list.
AI/LLM
- Why is prefill compute-bound and decode memory-bandwidth-bound?
- Your RAG answers are confidently wrong. Walk the diagnosis.
- When would you fine-tune instead of improving retrieval?
- How do you evaluate an agent that takes 20 steps?
- How do you defend against indirect prompt injection?
- Cut LLM spend 60% without hurting quality. What's the order of moves?
Context/Graph 7. Your prompt is 3,000 tokens of business rules. What do you do? 8. When does a knowledge graph beat a vector index? 9. How do you budget a 128k context window for an agent?
Streaming/Reactive/APIs
10. Explain exactly-once in Kafka and what it does not cover.
11. flatMap vs concatMap — behavior and when each is wrong.
12. Do virtual threads make WebFlux obsolete?
13. How do you fix N+1 in GraphQL, and why doesn't caching solve it?
14. Why does an L4 load balancer break gRPC?
15. Watermarks in Flink — what problem do they solve?
Caching/Edge 16. Prevent a cache stampede on a hot key. Three approaches. 17. Invalidate cached content with complex dependencies. How? 18. SSE or WebSocket for streaming LLM tokens, and why?
Compute/K8s/OS
19. A pod is healthy but slow. Diagnose in order.
20. Why might removing CPU limits improve latency?
21. Serverless or containers for this workload — walk your math.
22. Where does fsync fit in a durability guarantee?
23. How would you use eBPF to debug intermittent latency?
Storage 24. Design a DynamoDB table for these five access patterns. 25. Why did our Cassandra range query start timing out? 26. Pick a Mongo shard key for this workload and defend it. 27. Reindex OpenSearch with zero downtime. 28. Postgres write skew under REPEATABLE READ — what happens and how do you prevent it?
Security 29. Why PKCE for a confidential client? 30. Design token revocation with stateless JWTs. 31. A critical CVE drops in a library you use. Walk the first 4 hours. 32. Explain zero trust to a director in 60 seconds, then give the sequencing. 33. What's the difference between CVSS and EPSS, and which drives your patching?
Frontend 34. Half our Redux store is server data. What's the migration and why? 35. Our INP is bad. Diagnose. 36. When are micro-frontends worth it?
Leadership 37. Your best engineer's PR comments are demoralizing juniors. First move? 38. Review queue depth doubled after AI tooling rollout. What do you do? 39. Your director wants a date you can't commit to. 40. Make the case for 25% reliability investment to a product-focused VP. 41. Two teams are building the same service. You have no authority over either. 42. An engineer wants promotion; they're one level of scope short. Run the conversation.
26. Revised Study Calendar (12 Weeks)
Part I's 8-week plan covers the interview mechanics. This 12-week version folds in the Part II domains. Run Part I's tracks in parallel throughout — story portfolio and coding never stop.
| Week | Primary focus | Deliverable |
|---|---|---|
| 1 | Story portfolio + fundamentals + deep-dive outline | 6 stories written in SCOR |
| 2 | Distributed systems core + 5 classic designs | 5 written designs |
| 3 | Storage deep dive (§20) — Dynamo, Cassandra, Mongo, Postgres | Storage decision table from memory |
| 4 | Streaming + reactive + APIs (§17) | Kafka/Flink design + gRPC vs GraphQL matrix |
| 5 | AI/LLM/agents (§15) | 3 AI system designs written out |
| 6 | Context/prompt/graph engineering (§16) + evaluation | An eval harness design + context budget doc |
| 7 | Compute, K8s, OS/kernel (§19) | Debug checklist + a real perf investigation writeup |
| 8 | Security (§21) + a threat model of your own system | STRIDE threat model, 2 pages |
| 9 | Caching, CDN, real-time (§18) + frontend (§22) | Rendering + caching decision matrices |
| 10 | Leadership playbook (§23) + role-play drills | 16 stories total; 6 role-plays completed |
| 11 | AI cross-cutting (§24) + company-specific prep (§10) | Per-company question lists + 30/60/90 plans |
| 12 | Full-loop simulations, taper, polish | 2 full loops; every story under 3 minutes |
Weekly constants regardless of focus:
- 8–10 timed coding problems
- 1 mock interview from week 3 onward
- 1 story rewritten or tightened
- 1 design written out longhand, not just read
27. Final Calibration
Three things separate the candidates who get offers at this level from the ones who get "strong but not quite."
- They name the tradeoff before being asked. Every technical statement carries its cost. "I'd use X; it costs Y; I'd revisit if Z."
- They have a point of view and hold it under pressure without being brittle. Directors are hiring someone who will disagree with them productively.
- They talk about people and systems in the same breath. The strongest answer to a technical question at staff/TL level ends with how the team would operate it, review it, and be on call for it.
Everything in this document is in service of those three.
Part I written first; Part II appended with deep technical domains, leadership playbook, and the AI cross-cutting layer. Append further sections as prep progresses.
PART III — Gap Closure: SRE, Architecture Discipline, Delivery & Standards
Added after a coverage audit of Parts I–II. These are the topics most likely to be probed that the earlier parts under-served.
28. Observability & SRE (Full Depth)
28.1 SLO engineering — beyond the definitions
- SLI selection: measure at the point closest to the user (load balancer, not the app), ratio of good events to total events
- SLO targets: derived from user tolerance and business need, not aspiration. 99.9% = 43 min/month of budget; 99.99% = 4.3 min. Know these numbers cold.
- Error budget policy as a contract: budget healthy → ship fast; budget exhausted → freeze features, fund reliability. The policy only works if leadership signed it before the budget ran out. Say that.
- Burn-rate alerting — the answer to "how do you alert on an SLO":
- Alert on rate of budget consumption, not raw error rate
- Multi-window, multi-burn-rate: e.g., page when burn rate ≥ 14.4× over 1 h (budget gone in ~2 days) AND ≥ 14.4× over 5 min (still happening); ticket at 6× over 6 h; low-priority at 1× over 3 days
- Why two windows: the long window confirms significance, the short window confirms it's current — kills both false pages and stale pages
- Composite SLOs for user journeys spanning services; dependency SLOs and the "you can't be more available than your hard dependencies" math (three 99.9% serial dependencies ≈ 99.7%)
- Latency SLOs on percentiles: p99 per-request, and why averaged percentiles are meaningless (you cannot average p99s across instances — aggregate the histograms)
28.2 OpenTelemetry & the three pillars
- OTel as the vendor-neutral standard: API vs SDK vs Collector; OTLP protocol; semantic conventions
- Collector pipeline: receivers → processors (batch, tail sampling, redaction) → exporters. The Collector is where you enforce cardinality limits, sampling policy, and PII scrubbing centrally.
- Traces: spans, context propagation (W3C
traceparent), baggage; head sampling vs tail sampling (keep all errors and slow traces, sample the boring ones — requires buffering at the collector) - Metrics: counters/gauges/histograms; cardinality is the cost model — a label with unbounded values (user ID, request ID) will melt Prometheus. This is the #1 practical observability interview question.
- Exemplars: linking a histogram bucket to an example trace — the modern debugging workflow
- Logs: structured (JSON), trace-ID correlation, sampling noisy logs, cost tiers (hot/warm/cold retention)
- Wide events / observability 2.0 framing (Honeycomb-style): one rich event per request beats three disconnected pillars. Worth having as an opinion.
- Continuous profiling (Parca, Pyroscope, async-profiler) as the emerging fourth signal
28.3 Chaos engineering & resilience validation
- Principles: steady-state hypothesis → inject fault → measure blast radius → automate. Start in staging, graduate to production with guardrails and an abort switch.
- Fault menu: instance kill, AZ failure, dependency latency injection, packet loss, DNS failure, certificate expiry, clock skew, disk full, region evacuation
- Tooling: AWS FIS, Gremlin, Chaos Mesh, Litmus; game days as the organizational practice
- The lead framing: chaos engineering is cheap insurance against the failure modes your architecture claims to handle. "We believe we survive an AZ loss" is a hypothesis until tested.
- DiRT/game day narrative: run one, and have the story — what broke that you didn't expect. That story is interview gold.
28.4 Load & performance testing
- Types: load (expected peak), stress (find the breaking point), soak (leaks and degradation over hours), spike (sudden 10×)
- Tooling: k6, Locust, Gatling, JMeter; distributed load generation
- Methodology: define the SLO first, model realistic traffic (open vs closed workload models — closed models hide queueing collapse; know this distinction, it's a sophisticated signal), ramp gradually, watch saturation not just latency
- Coordinated omission — naive clients under-report tail latency when the system stalls. Naming this marks you as someone who has actually done performance work.
- Capacity planning: headroom targets (run at ≤60–70% at peak), scaling limits inventory, and load tests as regression gates before major launches
28.5 Incident management (formalized)
- Roles: incident commander, ops lead, comms lead, scribe — IC is a coordination role, not the best debugger
- Severity matrix with objective triggers; who can declare (anyone), who can page whom
- Mitigate before diagnose: rollback, feature-flag off, shed load, fail over — in that order of preference
- Status cadence: sev1 every 30 min, written, no speculation
- Blameless postmortem structure: timeline, contributing factors (plural — no single root cause), what went well, action items with owners and due dates, and a review loop that checks whether past action items actually shipped
- Repeat-incident rate as the metric that tells you whether postmortems are theater
29. Architecture Discipline: DDD, Team Topologies & Decision-Making
29.1 Domain-Driven Design (the parts interviews touch)
- Bounded context — the single most useful concept: an explicit boundary within which a model and its language are consistent. Service boundaries should follow bounded contexts, not entities. "We split by noun (users, orders) instead of by context" is the standard microservices failure; being able to say why is the standard staff answer.
- Ubiquitous language: the team and the code use the domain's words. Translation layers at context boundaries (anti-corruption layer) protect your model from an upstream's mess — name the ACL pattern when discussing integrations with legacy systems.
- Context mapping relationships: customer–supplier, conformist, shared kernel, ACL, open host service
- Aggregates: consistency boundaries; one transaction per aggregate; references by ID across aggregates. This maps directly to "how do you keep microservices consistent" — the aggregate boundary is where strong consistency ends and sagas begin.
- Event storming as the workshop technique to discover boundaries — mentioning you'd run one to decompose a monolith is a concrete, credible answer to a vague decomposition question
- Strategic vs tactical DDD: the strategic half (contexts, maps, language) is the valuable half at lead level; the tactical patterns (repositories, value objects) are implementation detail
29.2 Microservices vs modular monolith — the 2026 position
- The pendulum has swung: the defensible default for most teams is a modular monolith with enforced internal boundaries (module APIs, no cross-module DB access), extracting services only when a specific force demands it: independent scaling, independent deploy cadence for separate teams, fault isolation, polyglot needs, or regulatory separation
- Extraction criteria checklist and the strangler-fig mechanics (Part I §5.1) — connect these explicitly
- Distributed systems tax to recite: network failure modes, versioned contracts, distributed tracing, eventual consistency, integration testing pain, on-call fan-out
- Conway's law both directions: your architecture will mirror your org chart, so use the inverse Conway maneuver — reshape teams to get the architecture you want
29.3 Team Topologies (know this cold — it's the current lingua franca of eng-leadership interviews)
- Four team types: stream-aligned (the default — owns a slice of value end to end), platform (reduces cognitive load of stream teams via self-service), enabling (temporarily uplifts capability, then leaves), complicated-subsystem (deep specialist domain)
- Three interaction modes: collaboration (high bandwidth, temporary, for discovery), X-as-a-Service (clean consumption, for stable interfaces), facilitating (coaching)
- Cognitive load as the sizing principle: a team's scope is bounded by what it can hold in its collective head. Too many domains → errors, slow onboarding, burnout. This gives you a principled answer to "how would you split this 15-person team" — split by cognitive load and stream alignment, not by layer.
- Platform-as-product: internal platforms need product management, adoption metrics, and paved roads, not mandates. "Golden path, not golden cage."
- Anti-patterns to name: shared "DevOps team" as a bottleneck, layer-based teams (frontend team / backend team / DB team) forcing every feature through three backlogs, enabling teams that never leave
29.4 Decision-making machinery
- One-way vs two-way doors (make reversible decisions fast, irreversible ones carefully) — attribute it, use it constantly
- DACI/RAPID: Driver, Approver, Contributors, Informed. The point is one named approver — decisions with committee approval don't get made. Offer DACI when asked "how do you resolve cross-team disagreement."
- RFC/design-doc process: async written proposal → comment window → decision recorded. ADRs (architecture decision records) as the lightweight log: context, decision, consequences, status. "Decisions not written down get relitigated" — say it.
- Disagree and commit: the full version — dissent is documented, a revisit trigger is defined, and commitment afterward is genuine
- C4 model for architecture communication: Context → Container → Component → Code. Draw system-design answers at the container level and say you're doing so; it signals structured communication.
- Rubrics for build-vs-buy: total cost of ownership including operations and hiring, differentiation test ("is this our business?"), exit cost, vendor risk
30. Delivery Engineering: CI/CD, Testing Strategy & Experimentation
30.1 Branching & integration
- Trunk-based development as the high-performance default: short-lived branches (<1 day), merge to main continuously, incomplete work behind flags. Long-lived feature branches correlate with slow lead time and merge hell — DORA's research backs this and interviewers know it.
- GitFlow: legacy; defensible only for versioned/shipped software with parallel supported releases
- Merge queue (GitHub merge queue, Bors-style) for high-traffic repos; monorepo vs polyrepo tradeoffs (atomic cross-cutting changes and shared tooling vs build tooling investment — Bazel/Nx/Turborepo)
30.2 Feature flags as a platform capability
- Flag types with different lifecycles: release flags (short-lived, deleted after rollout), ops flags (kill switches, permanent), experiment flags (owned by the A/B platform), permission flags (entitlements)
- Progressive rollout: 1% → 5% → 25% → 50% → 100% with automated rollback on metric regression
- Flag debt is real: stale flags are dead code paths with untested interactions. Policy: every release flag has an owner and an expiry; CI warns on expired flags. Naming flag hygiene is an experienced-operator signal.
- Testing with flags: test both sides of live flags; combinatorial explosion means you prioritize by traffic reality, not exhaustiveness
30.3 Testing strategy (the coherent version)
- Pyramid vs trophy: classic pyramid (many unit, some integration, few E2E) vs Kent C. Dodds' trophy (weight integration tests, since they catch the most bugs per maintenance dollar). Have a position: unit tests for logic and algorithms, integration tests as the workhorse, E2E for a handful of critical journeys only. Flaky E2E suites that everyone retries are worse than no E2E suite — they train the team to ignore red.
- Contract testing (Pact / consumer-driven contracts): consumers publish expectations, providers verify in CI. This is the answer to "how do you test 40 microservices without a full-environment integration suite" — near-mandatory knowledge for a lead in a microservices org. Bi-directional contracts and schema-based alternatives (Protobuf + buf breaking-change checks, OpenAPI diff) for the lighter-weight version.
- Test doubles taxonomy (stub, mock, fake, spy) and the classicist-vs-mockist stance: over-mocked tests that verify implementation rather than behavior are the most common test-suite disease
- Property-based testing (Hypothesis, jqwik) for parsers, codecs, invariant-heavy logic; mutation testing (PIT) to audit whether coverage means anything — both punch above their weight as "above and beyond" mentions
- Test data management: builders/factories over fixtures, ephemeral databases (Testcontainers — name it), golden files for snapshot-style verification
- Non-functional gates in CI: performance budgets, security scans, accessibility checks
30.4 Release engineering
- Deployment strategies matrixed by risk: rolling (default), blue/green (instant rollback, 2× capacity), canary (metric-gated, needs traffic), shadow (risk-free validation, needs idempotent downstream handling)
- Automated canary analysis (Kayenta-style): compare canary vs baseline cohort on SLIs, promote or roll back without a human
- Deploy ≠ release: deployment is moving bits, release is exposing users, decoupled by flags. This one sentence resolves half of all release-process interview questions.
- Database changes ride the expand–contract pattern (expand schema → migrate code → contract) so every deploy stays backward compatible and rollback-safe
- Rollback discipline: every change has a tested rollback or an explicit "roll-forward only" designation with justification; artifact immutability (deploy the same artifact through every environment)
30.5 Experimentation platform
- A/B fundamentals: randomization unit (user vs session vs request — and why crossing units invalidates results), power analysis before launching (know that detecting a 1% lift on a low-traffic surface may take months — saying this prevents the classic underpowered-experiment failure)
- Guardrail metrics alongside success metrics: latency, error rate, retention, revenue-per-user — a win on the target metric that trips a guardrail is a loss
- Pitfalls to name: peeking (sequential testing or fixed horizons as fixes), multiple comparisons, novelty effects, network effects and interference (switchback tests for marketplaces), Simpson's paradox in segment analysis
- Interleaving for ranking changes (from your search background — much more sensitive than A/B for relevance; connect it)
- CUPED / variance reduction as an advanced mention
- Org layer: experiment review, a shared metrics dictionary, and the discipline that ship/no-ship decisions cite the experiment readout
31. Standards & Frameworks Reference Card
The shared vocabulary of eng-leadership conversations. For each: what it measures, when to invoke it, and its known failure mode. Interviewers use these as shorthand — fluency here is table stakes; knowing the critiques is the differentiator.
| Framework | What it is | Invoke when | Failure mode / critique |
|---|---|---|---|
| DORA (five keys) | Deployment frequency, lead time, change failure rate, failed-deployment recovery time, rework rate | Baseline delivery health; before/after for process changes | Team-level comparison is explicitly warned against; gameable; misleading when AI writes 30–70% of code — pair with quality and AI-attribution signals |
| SPACE | Satisfaction, Performance, Activity, Communication, Efficiency — pick metrics across ≥3 dimensions | When someone proposes measuring productivity with a single number | It's a framework for choosing metrics, not a metric set — people cite it without operationalizing it |
| DX Core 4 | Speed, effectiveness, quality, business impact — an opinionated, benchmarkable synthesis of DORA+SPACE+DevEx | Exec reporting; when DORA alone is getting gamed | Newer; benchmarks vendor-dependent |
| DevEx (flow, cognitive load, feedback loops) | Developer-experience lens on productivity | Diagnosing why delivery is slow (interruptions, wait states, tooling friction) | Survey-heavy; needs pairing with system data |
| Google SRE | SLI/SLO/error budgets, toil caps (<50%), blameless postmortems | Reliability conversations; on-call design | Cargo-culted error budgets without the leadership contract behind them |
| AWS Well-Architected | Six pillars: operational excellence, security, reliability, performance efficiency, cost optimization, sustainability | Architecture reviews — use the pillars as a review checklist | Checklist compliance ≠ good architecture; vendor-flavored |
| 12-Factor App | Config in env, stateless processes, disposability, dev/prod parity, logs as streams | Assessing an app's cloud-readiness; container migration | Dated in places (e.g., its config story predates secret managers); still the shared baseline vocabulary |
| FinOps (inform → optimize → operate) | Cloud financial ops: visibility/allocation → rightsizing/commitments → continuous governance | Cost conversations; unit economics (cost per request/customer/token) | Becomes a reporting function with no engineering teeth if leads don't own unit costs |
| Team Topologies | Four team types, three interaction modes, cognitive-load-bounded team scope | Org design, platform strategy, "how would you restructure this" | Applied as labels without changing interaction modes |
| SLSA | Supply-chain integrity levels for build provenance | Supply-chain security posture; artifact signing roadmap | Levels adopted on paper without verifying provenance at deploy time |
| NIST CSF 2.0 | Govern, Identify, Protect, Detect, Respond, Recover | Structuring a security program conversation with leadership | High-level; needs mapping to concrete controls |
| NIST AI RMF / ISO 42001 | AI risk management framework / AI management-system standard | AI governance questions — increasingly asked in regulated industries; directly relevant to legal-tech | Early-maturity; audits vary widely |
| WCAG 2.2 AA | Accessibility conformance target | Frontend quality bars; legal exposure (ADA, AODA — note AODA applies in Ontario) | Treated as launch-gate checklist instead of design input |
| C4 model | Four zoom levels for architecture diagrams | Any whiteboard moment — narrate which level you're drawing | None serious; just underused |
| RFC 2119 keywords | MUST/SHOULD/MAY in specs | Writing API contracts and internal standards | Trivial, but using it correctly in docs reads as rigor |
How to deploy these in interviews: never recite a framework as an answer. Use it as scaffolding — "I'd look at this through the Well-Architected reliability pillar: …" — then immediately get concrete. Framework name-dropping without specifics is a negative signal; framework + critique + concrete application is a strong one.
32. Multi-Region, DR & Business Continuity
- RTO (how long until service restored) and RPO (how much data you can lose) — every DR conversation starts by extracting these two numbers from the business, because they set the budget. Know the cost curve:
- DR strategy ladder, cheapest to most expensive: backup & restore (RTO hours–days) → pilot light (core data replicated, minimal compute idle) → warm standby (scaled-down full stack) → multi-site active-active (RTO ~0, RPO ~0, 2×+ cost and permanent engineering complexity)
- Active-active realities: conflict resolution (last-writer-wins vs CRDTs vs single-writer-per-key), data residency constraints pinning users to regions, global load balancing (GeoDNS + health checks, anycast), and the write-path question — global strong consistency (Spanner-style, pay latency) vs regional writes with async replication (pay conflicts)
- Failover discipline: untested failover is fiction. Regular game-day region evacuations; runbooks with decision criteria for who declares failover and at what threshold (the hard part is deciding to fail over, not the mechanics)
- Failback is harder than failover — resynchronization and split-brain reconciliation; plan it explicitly
- Dependency audit: your real RTO is the max of your critical dependencies' RTOs — including your identity provider, DNS, and secrets manager, the three everyone forgets
- Backup hygiene: 3-2-1 rule, immutable/air-gapped copies (ransomware), and restore testing — a backup that's never been restored is a hope, not a backup
- Chaos-test the DR claims (ties to §28.3)
33. The Offer Stage: Closing Above and Beyond
Prep doesn't end at the last interview. This stage has the highest ROI per hour of anything in this document.
- Debrief dynamics: offers are calibrated in a packet review. Your interviewers write feedback within a day — the follow-up notes from Part I §11 land before that happens; that's why they matter.
- Level negotiation before compensation negotiation. Level determines the band; arguing dollars within the wrong band is fighting the wrong battle. If you got down-leveled, ask what evidence was missing and whether additional signal (another conversation, a work sample) can revisit it — sometimes it can.
- Components and their flexibility, most to least: sign-on bonus > equity > base > annual bonus target. Multi-year sign-ons can bridge an equity gap.
- Equity literacy: RSUs vs options, vesting schedules and cliffs, refresh policy (ask — a great initial grant with no refreshers inverts by year 3), for private companies: strike price, latest 409A/preferred spread, exercise windows, liquidity outlook
- Competing timelines: it is normal and expected to ask companies to align decision dates. Urgency created by a real competing offer is the strongest negotiation position; manufactured urgency is transparent.
- Negotiate warmly and factually: enthusiasm for the role + specific ask + reason. Adversarial negotiation with your future director is winning the battle and poisoning the well.
- What's negotiable beyond comp: start date, level-review timeline in writing, team placement, conference/education budget, remote arrangements
- Toronto-relevant mechanics: know whether the number is CAD or USD, cross-border payroll structures (some US companies employ in Canada via subsidiaries or EORs with different equity treatment), and that Ontario employment agreements deserve a read of the termination clause before signing — non-competes are largely unenforceable in Ontario but the termination language matters
- References: line them up before offers; brief each on the role and the two things you want emphasized
34. Updated Weekly Integration
Fold Part III into the 12-week calendar (§26) as follows, rather than extending the timeline:
- Week 2 adds §28 SLO math and burn-rate alerting alongside distributed-systems core (they're the same conversation)
- Week 4 adds §30 delivery engineering (trunk-based, flags, contract testing) — it pairs naturally with the streaming/API week's CI concerns
- Week 7 adds §28.2–28.4 (OTel, chaos, load testing) to the compute/K8s week — instrument what you deploy
- Week 8 adds §32 DR (it's the availability half of the security week)
- Week 10 adds §29 Team Topologies + DDD + decision machinery to the leadership week — these ARE the leadership vocabulary
- Week 11 adds §31: rehearse invoking two frameworks with critiques per mock answer
- Week 12 adds §33 offer-stage prep once onsites are scheduled
The drill bank (§25) gains these; same 90-second rule:
- Design burn-rate alerts for a 99.9% SLO. Why two windows?
- A label explosion took down Prometheus. What happened and what's the policy fix?
- How do you test 40 microservices without a full integration environment?
- Deploy vs release — explain the distinction and what it buys you.
- Split this 15-person team. Walk your reasoning. (Answer with cognitive load + stream alignment, not layers)
- When is a modular monolith the right answer, and what forces an extraction?
- Your experiment won its target metric and tripped a latency guardrail. Ship it?
- The business says "we can't lose any data and can't be down." What do you say next? (Extract RTO/RPO, show the cost curve)
- Walk me through an untested assumption in your current architecture and how you'd chaos-test it.
- You've been down-leveled in an offer. What do you do?
Part III added after coverage audit: SRE/observability depth, architecture and org-design vocabulary (DDD, Team Topologies), delivery engineering, the standards reference card, DR, and offer-stage strategy.
PART IV — Patterns, Contracts, and the Human Layer
35. Design Patterns & Code Architecture
The staff-level framing to internalize before any of the content: patterns are vocabulary, not virtue. You get points for naming the pattern a design already implies, for using the shared name to speed up a conversation, and for knowing when a pattern is overkill. You lose points for pattern-driven design — introducing machinery a simpler construct would serve. Interviewers at this level are often testing for the restraint, not the catalog.
35.1 SOLID — with the mature caveats
- S — Single Responsibility: one reason to change. The useful test is "who asks for changes to this module" — if two stakeholders, split it. The failure mode of over-applying it: a codebase of 40-line classes where no behavior lives anywhere.
- O — Open/Closed: extend without modifying — via polymorphism, strategy, composition. Honest caveat: speculative extension points are debt; make things extensible when the second use case arrives, not before.
- L — Liskov Substitution: subtypes must honor the base type's contract (preconditions can't strengthen, postconditions can't weaken). The classic violation to cite:
Square extends Rectangle. In practice this is why you prefer composition over deep inheritance. - I — Interface Segregation: many narrow interfaces over one fat one; clients shouldn't depend on methods they don't use. Directly maps to API design and gRPC service decomposition.
- D — Dependency Inversion: depend on abstractions; high-level policy shouldn't import low-level detail. This is the principle hexagonal architecture is built on — connect them out loud.
- The seasoned add: SOLID is object-oriented vocabulary. Be able to translate — in functional style, S and D become pure functions and effects-at-the-edges; in Go, small interfaces defined at the consumer.
35.2 The GoF subset that actually appears
Don't memorize 23; hold the ~10 that come up, each with the one-line "when":
- Strategy — swap an algorithm at runtime; the answer to most "if/else on type" smells
- Factory Method / Abstract Factory — creation behind an interface; in modern code mostly subsumed by DI containers
- Builder — many optional parameters, immutable results; standard in Java/fluent APIs
- Adapter — reshape an interface you don't own; the code-level anti-corruption layer
- Facade — one simple front over a messy subsystem; the pattern behind every good SDK
- Decorator — layer behavior without inheritance; how middleware, interceptors, and reader/writer wrappers work
- Observer — event listeners; know its distributed cousin is pub/sub and its failure mode is hidden coupling and ordering assumptions
- Template Method vs Strategy — inheritance vs composition for varying a step; prefer strategy
- Chain of Responsibility — middleware pipelines, servlet filters, gRPC interceptors
- Singleton — name it as an anti-pattern in application code (global state, test hostility); acceptable as managed single instances via a DI container
- Concurrency honorable mentions: producer–consumer, thread pool, future/promise, actor model (Akka/Erlang framing), and immutability as the pattern that makes the rest unnecessary
35.3 Enterprise patterns (Fowler's PoEAA — the interview-relevant slice)
- Repository — collection-like abstraction over persistence; domain code speaks
findByX, not SQL. The nuance that shows experience: with a capable ORM, a generic repository over it is often a pointless extra layer ("repository over Repository"); the pattern earns its keep when it isolates the domain from the persistence model, enables test doubles, or fronts multiple stores. Say the critique. - Unit of Work — track changes, commit atomically; what an ORM session/EntityManager already is
- Specification — composable query predicates as objects; useful when business rules about selection must be reused and combined
- Data Mapper vs Active Record — separation vs convenience; Active Record couples domain to schema and is fine for CRUD apps, painful past that
- DTOs and mapping boundaries — never let persistence entities leak into API contracts; version DTOs, not tables
- Anti-corruption layer — reappears from DDD (§29.1); at code level it's adapters + translators at the boundary of a legacy or third-party model
- Transaction Script vs Domain Model — the honest answer that simple workflows deserve simple procedural code; rich domain models pay off only with rich invariants
35.4 Application architecture styles
- Hexagonal / Ports & Adapters: domain core with ports (interfaces) implemented by adapters (DB, HTTP, queue). The point: the domain is testable without infrastructure, and infrastructure is swappable. Clean and Onion architecture are the same idea with different diagrams — say so; it defuses terminology quizzes.
- The dependency rule: source dependencies point inward, toward policy. Frameworks, DBs, and delivery mechanisms are details at the edge.
- CQRS: separate write model from read model. The ladder of adoption: separate handlers → separate models → separate stores. Most teams need step one; step three plus event sourcing is a specialized commitment (§17.6). Being explicit about the ladder is the senior signal.
- Vertical slice architecture — organize by feature, not by layer; the modern counterpoint to strict layered/clean layouts, and a genuinely good default for product teams. Having both positions and a preference is exactly the kind of opinion interviews reward.
- Anti-patterns to name fluently: anemic domain model (data classes + service-layer procedures pretending to be DDD), god object, big ball of mud, distributed monolith (microservices with synchronous chains and a shared DB — the most expensive anti-pattern of the last decade), golden hammer, premature abstraction (wrong abstraction is costlier than duplication — the "rule of three")
- Resilience patterns (connect back to §5.1 rather than restate): circuit breaker, bulkhead, retry-with-jitter, timeout budgets, fallback, load shed — implemented today in mesh/middleware (Envoy, Resilience4j) rather than hand-rolled
- Refactoring vocabulary: strangler fig (again — it's the answer at code and system level), branch by abstraction (refactor safely on trunk without long-lived branches — pairs with §30.1), parallel change/expand-contract, seams (Feathers) for getting legacy code under test, characterization tests before touching untested code
35.5 How this shows up in interviews
- Code review rounds: naming the smell + the pattern-shaped fix + the simpler non-pattern fix, and recommending the simpler one when warranted
- Design rounds: "this boundary is a port; the Kafka consumer is just an adapter" — one sentence that reframes your whole diagram as testable
- Behavioral: a story about removing an abstraction is rarer and stronger than one about adding one
36. SLA vs SLO vs SLI — the Contractual Layer
§28 covered the engineering; this is the business-facing half a lead is expected to own.
- SLI — the measurement (e.g., fraction of requests under 300 ms). SLO — the internal target on that measurement (99.9% monthly). SLA — the external contract: an SLO subset promised to customers with remedies attached (service credits, termination rights). The one-liner: SLIs are what you measure, SLOs are what you aim for, SLAs are what you'll pay for.
- Always set the SLA looser than the SLO. Internal 99.95%, contractual 99.9% — the gap is your reaction buffer. Promising your SLO as your SLA means every internal miss is a customer credit.
- Read the fine print like an operator: measurement window (monthly vs quarterly changes everything), exclusions (scheduled maintenance, force majeure, customer-caused), who measures (their monitoring or yours), claim process (credits usually require the customer to file — which tells you how often they pay out)
- Your dependencies' SLAs bound your own. Compose them: if your product SLA is 99.9% but you synchronously depend on a vendor offering 99.5%, you've promised something you can't structurally deliver — you need redundancy, degradation, or a renegotiated promise. Walking this math is a genuinely impressive interview moment.
- OLA (operational level agreement) — the internal-team version of an SLA; useful vocabulary when discussing platform teams' commitments to stream teams (ties to §29.3's platform-as-product)
- Cloud SLA literacy: know that a typical 99.99% SLA credit caps at a fraction of the monthly bill — SLA credits are not insurance, they're an apology. Your availability architecture, not the vendor's SLA, is what protects revenue.
- Per-tenant/enterprise SLAs create engineering requirements: tenant-level SLI measurement, priority routing or isolation for premium tiers, and reporting. If sales sells a custom SLA, engineering inherits a custom observability requirement — say this; it's the lead-level insight.
- Error-budget-based SLA management: alert internally at SLO burn long before SLA breach; the SLA breach postmortem is a business event with comms, credits, and an exec readout, not just an engineering one.
On STAR vs SCOR: the doc teaches SCOR (§8.2) because it adds the Options slot. If an interviewer explicitly asks for STAR (Amazon often frames it that way), map cleanly: Situation → S+C, Task/Action → your chosen option and execution (compress the alternatives into one sentence — "we weighed X and Y and chose Z because…"), Result → R plus reflection. Same story, reshaped on demand. Practicing one story in both formats once is enough.
37. The Question Playbook, by Interviewer
§12 gave the director set. This is the full persona-by-persona version. The strategic frame first:
Your questions are scored. Every interviewer reports what you asked. Questions do three jobs at once: (1) gather real decision data, (2) demonstrate seniority by what you think to ask about, and (3) let the interviewer talk about themselves — which measurably improves how they remember the conversation. Ask questions only someone who has operated at the level could ask. Never ask anything the careers page answers.
Mechanics: 2–3 questions per session, matched to the persona; keep one universal spare for time-boxed endings; write their answers down — contradictions between interviewers are the highest-value diligence signal you can collect.
37.1 Recruiter screen
Goal: process intelligence and calibration. This person wants you to succeed; use that.
- "How is the loop structured, and what is each round actually evaluating?"
- "What level is this role calibrated at, and what does the committee look for at that level?" (asking about leveling early prevents the down-level surprise in §33)
- "What's the team's timeline, and how many candidates are in process?"
- "What has caused candidates to fall out of this loop?" (recruiters will often just tell you)
37.2 Hiring manager
Goal: understand the job behind the job description, and their theory of the role. This round is bidirectional evaluation at its most concentrated.
- "What's the problem in your org that made you open this req — what breaks or stays broken if it goes unfilled for six months?" (the single best HM question; the answer is the real job)
- "Walk me through the team: tenure, levels, who's strongest at what, and where the gaps are."
- "What did the last person in this seat (or the interim owner) struggle with?"
- "What would the first two deliverables be, and how will you personally judge whether they went well?"
- "How much of my time do you expect in code / in design / in people work at months 3 and 12?" (surfaces TL-vs-EM ambiguity before you accept the wrong job)
- "What's your operating rhythm with your leads — 1:1 cadence, what you want escalated, how you deliver hard feedback?"
- "Where do you and your manager currently disagree about this team's direction?" (bold; asks for candor and almost always gets a revealing answer)
37.3 Peer engineers / future reports
Goal: ground truth. ICs are the least media-trained people in the loop.
- "Walk me through your last production incident — how did it go, and did the postmortem action items actually happen?"
- "How long does a one-line change take to reach production, end to end?" (one number that reveals the whole delivery system)
- "What's the piece of the codebase everyone avoids, and why is it still like that?"
- "When you disagreed with a technical direction recently, what happened?"
- "What would you fix first if you had a month of unscheduled time?"
- For future reports specifically: "What do you want from your next lead that you're not getting today?" (also quietly demonstrates the kind of lead you'd be)
37.4 Director / skip-level
The §12 set applies; add the seasoned tier:
- "What are you accountable for this year that this team materially affects?"
- "When this team missed or slipped in the past year, what was the real cause — and what changed afterward?"
- "How do headcount and priority trade-offs get decided between your teams?"
- "What behavior gets people promoted here in practice — and what behavior gets tolerated that you wish didn't?" (the gap between those two answers is the actual culture)
- "If I'm sitting here in a year and this hire clearly worked, what happened? And what's the most likely way it fails?"
37.5 VP / CTO / executive round
Goal: strategy comprehension. Ask about the business, not the team — showing you think at their altitude is the entire point.
- "How does engineering show up in company strategy — cost center, product differentiator, or the product itself?"
- "What's the bet the company is making that you think is under-appreciated?"
- "How is the org thinking about AI structurally — product capability, productivity layer, or both — and who owns that call?"
- "What would make you say, two years from now, that engineering leadership hiring in this era was a success?"
- Then one that lands your positioning: "The way I've operated is X; where would that help most here, and where would it clash?"
37.6 Product / design / cross-functional partner
Goal: assess the partnership you'll live in — and signal you value it.
- "Walk me through how the last major feature went from idea to shipped. Where was the friction?"
- "When engineering pushes back on scope or dates, how does that usually go?"
- "Do engineers here engage in discovery, or receive requirements?"
- "What does engineering do that makes your job harder — honestly?"
37.7 Bar raiser / neutral third party (Amazon-style)
This person is deliberately outside the team; they evaluate long-term and culture. Ask questions that show you understand their function:
- "What does the bar look like at this level from where you sit — what separates a hire from a strong-but-no?"
- "You've seen many teams here — what distinguishes the ones that work?"
- Keep it shorter; bar raisers are time-boxed and score judgment density, not question count.
37.8 The universal closers (have all three loaded)
- "What's the question I should have asked about this role that I haven't?"
- "Based on this conversation, is there anything about my fit you're still unsure of? I'd rather address it now." (from §12 — it converts silent objections into answerable ones; use it in the final round or with the HM, once per loop)
- "What made you stay?" (works on anyone with tenure; the pause before the answer is data)
37.9 Reverse due diligence — the red-flag checklist
You're evaluating them. Patterns that predict a bad tenure, gathered across the loop:
- Interviewers describe the same team completely differently (no shared reality)
- Nobody can name what success looks like for the role (you'll inherit an unwinnable mandate)
- The role is open because two predecessors left inside 18 months and no one will say why
- Every answer about problems is "we just need to hire great people" (the problem is not headcount)
- The HM can't describe their own manager's expectations (air cover doesn't exist)
- Postmortem action items "usually get done" with no examples (reliability theater)
- All decision authority routes through one person, however impressive (you'd be a senior pair of hands, not a lead)
- Visible contempt between product and engineering in how each describes the other
- They can't explain why the level is what it is (leveling chaos follows you in) Weigh patterns, not single data points — any org has one bad answer. Three of these across a loop is a signal.
38. Signals of Seasonality — the Unasked-For Essentials
The things that make an interviewer write "operates like they've done this for years" — none of which appear in a topic list.
38.1 Executive communication mechanics
- Answer first (BLUF), then reasoning. "Yes — for three reasons" beats two minutes of context arriving at yes. This is the Pyramid Principle in one habit, and it's the fastest single upgrade available to most engineering candidates.
- Calibrate altitude to audience in real time: with a VP, lead with outcome and risk; with an IC, lead with mechanism. Mid-answer, watch for the glazed look and zoom out one level without being asked.
- Signposting: "There are three parts to this — cost, risk, and timeline. Cost first." Interviewers literally take notes in your structure.
- Quantify reflexively, and bound your uncertainty: "roughly 40%, could be 25–60" reads as more credible than a false-precision "43%."
38.2 Handling what you don't know
The moment interviewers most remember. The seasoned sequence: state the boundary plainly → reason from adjacent knowledge → name how you'd find out. "I haven't run Scylla in production. Here's what I'd expect to transfer from Cassandra, here's where I'd expect the shard-per-core model to change the tuning story, and here's what I'd benchmark first." Never bluff — at this level interviewers probe two layers past your claimed knowledge specifically to find the bluff, and one caught bluff outweighs ten strong answers. Conversely, "I don't know" with no reasoning attempt is a wasted at-bat. The middle path is the skill; drill it deliberately on questions at the edge of your knowledge.
38.3 Scar tissue — the stories only operators have
Prepare 4–5 short "I learned this the expensive way" fragments. They're deployable inside technical answers, not just behavioral rounds, and they're unfakeable:
- The migration that was "done" until the long tail of stragglers took longer than the migration
- The cache that hid a correctness bug for months
- The heroic engineer whose heroics were masking a process failure — and what happened when they took vacation
- The dashboard that was green through a sev1 because it measured the wrong thing
- The re-architecture you didn't do, and why restraint was right Delivered in 30 seconds, attached to a relevant technical point, these do more than any framework citation.
38.4 "It depends" — done correctly
Junior "it depends" stops there. Seasoned "it depends" immediately names the two or three variables it depends on, states which case you'd bet on given what you know of their context, and commits: "It depends on read/write ratio and consistency needs — for what you've described, which sounds read-heavy with tolerance for seconds of staleness, I'd pick X." Conditional, then decisive. Interviewers are explicitly listening for whether you land the plane.
38.5 Whiteboard & remote mechanics
- Narrate your zoom level (C4 language from §29.4), label arrows with protocols and data, write the numbers on the board — a diagram with QPS and p99 on it photographs like experience
- Manage the clock out loud: "We have 20 minutes left — deep-dive the ranking service or the ingestion path?" Handing the interviewer that choice is itself a leadership behavior being scored
- Remote logistics as professionalism signals: tested audio, wired connection where possible, a fallback ("if I drop, I'll rejoin from my phone"), Excalidraw/tldraw fluency so shared drawing costs you nothing, camera at eye level, notes okay but never read from
- Interview-day stamina is trainable and mostly ignored: full-loop simulation (§3 week 7) exists precisely because round 5 of 6 is where unrehearsed candidates fade; protein at lunch, water, stand between rounds
38.6 Operator literacies that surface in passing
You're rarely asked these directly; they leak out of good answers and mark seniority when they do:
- Budget fluency: loaded headcount cost (~2× salary), the build-vs-buy math including opportunity cost, cloud unit economics, why a $200k tool replacing half an engineer-year of toil is cheap
- Vendor management: running an eval with weighted criteria, negotiating with a genuine BATNA, exit-cost accounting before signing, managed-service SLAs as apologies not insurance (§36)
- Glue work (Tanya Reilly's framing): the non-promotable coordination work that makes teams function — the lead's job is to see it, value it in calibration, and distribute it deliberately rather than letting it accrete on whoever is most conscientious. Naming glue work unprompted is a strong people-leadership signal.
- Managing former peers: the transition conversation done explicitly and early, renegotiating friendships around new information asymmetry, the trap of keeping the fun technical work for yourself, and over-indexing on fairness in the first quarter because everyone is watching for favoritism
- Legacy modernization narrative: every senior loop eventually asks about old systems. The seasoned arc: understand before judging (the code is the way it is for reasons that were once good) → characterization tests → seams (§35.4) → strangler increments with value delivered at every step → celebrate deletion. Contempt for legacy code is a junior tell; respect for it plus a plan is the senior one.
- Remote/hybrid leadership specifics: async-first documentation as the default, explicit overlap-hours contracts, deliberate onboarding redesign (remote onboarding fails silently), watching for the proximity-bias promotion pattern in hybrid teams
38.7 Follow-up etiquette that compounds
- Same-day, short, specific: one line of thanks + one substantive continuation (the §11 artifact — a cleaned-up diagram, a source relevant to a discussion point). Before the debrief writes itself.
- If you flubbed something and know it: one-line correction in the follow-up ("I said X for the quorum math; on reflection it's Y") — this has rescued candidacies, because it demonstrates the exact self-correction behavior the loop tries to measure
- Post-rejection grace: ask for specifics, thank them, stay warm. Loops recycle interviewers and recruiters across companies for decades; several of your future offers are downstream of how you handled a past no.
38.8 Drill bank additions
- Explain repository pattern, then argue against using it in a given codebase.
- "Is CQRS a good idea for us?" — walk the adoption ladder and place them on it.
- Our vendor offers 99.5% and we sell 99.9%. What are our options?
- An interviewer asks a question you genuinely can't answer. Perform the §38.2 sequence on a real gap of yours.
- You have 90 seconds with the CTO at the end of the loop. What do you ask?
- Rewrite one of your SCOR stories as STAR on the spot.
- Name three red flags from §37.9 you'd probe for, and the exact questions you'd use to surface each without being adversarial.
- Tell a 30-second scar-tissue story that would fit inside a caching design answer.
Part IV added: design patterns and code architecture with the restraint framing, the SLA contractual layer and STAR mapping, the persona-by-persona question playbook with reverse due diligence, and the seasonality signals that don't appear on topic lists.
39. Deployment & Progressive Delivery (Consolidated Deep Dive)
Deployment appeared in fragments (§5.1, §19.3, §30.4); this section is the full treatment, because "how do you ship safely" is a guaranteed question at lead level and the answer is a system, not a technique.
39.1 The strategy matrix — mechanics, cost, and when
| Strategy | Mechanics | Rollback | Cost | Use when | Gotchas |
|---|---|---|---|---|---|
| Recreate | Stop old, start new | Redeploy old | None | Dev/test, singleton batch jobs | Downtime by design |
| Rolling | Replace instances in batches (maxSurge/maxUnavailable in K8s) | Roll back batch by batch — slow | Low | The default for stateless services | Two versions live simultaneously → N-1 compatibility required; a bad version can be 60% rolled out before detection |
| Blue/green | Full parallel environment; cut traffic over at the router | Instant — flip back | 2× capacity during deploy | Low-tolerance-for-bad-minutes services; big-bang framework upgrades | Stateful connections drop at cutover; DB is shared, so schema still needs expand–contract; 2× cost tempts teams to skip it exactly when it matters |
| Canary | Small % of real traffic to new version; widen on healthy metrics | Shift weight to 0 | Low | The default for anything with meaningful traffic | Needs enough traffic for statistical signal; sticky sessions can starve the canary; must compare canary vs contemporaneous baseline, not vs history |
| Shadow / mirror | Copy of live traffic to new version; responses discarded | N/A — no user exposure | ~2× compute for mirrored path | Rewrites, perf validation, ML model validation | Side effects: mirrored writes must hit a sandbox or be idempotent-suppressed; async downstream effects (emails, payments) are the classic disaster |
| Ring-based | Deploy in expanding rings: team → internal users → 1% → region → world (Microsoft-style) | Halt at current ring | Low | Large user bases, client software, OS/platform teams | Slow by design; ring 0 users are unrepresentative — don't tune on them |
| Rolling + surge regions | Region-by-region with bake time between | Halt sequence, evacuate region | Low | Multi-region services | Deploy order should follow traffic (lowest first), respect timezone peaks, and never deploy to all regions inside one bake window |
The composite answer that sounds like practice: rolling as the mechanism, canary as the policy, flags as the exposure control, rings for the blast-radius sequencing. They compose; they're not competitors.
39.2 Traffic-shifting mechanics (how the percentage actually happens)
- Load balancer weights (ALB weighted target groups, nginx upstream weights) — simplest, service-level granularity
- Service mesh (Istio VirtualService, Linkerd TrafficSplit) — per-request routing with header/cookie match: lets you canary internal services and send only employees or a consistent user cohort to the new version
- DNS weighting — coarse and slow (TTL caching); use for region-level only, never for fine-grained canary
- Consistent cohorting: hash on user ID, not random per request — a user flapping between versions mid-session experiences bugs neither version has. This detail is a strong practitioner signal.
- Sticky-session interaction: session affinity can pin your canary's traffic share below its configured weight; measure actual share, not intended share
- Client-side / mobile: you don't control the deploy — staged rollouts via app stores (1% → 100%), feature flags as the real control plane, server-driven config, and the discipline that API servers support N-2 client versions because mobile upgrades take weeks. Forced-upgrade mechanisms as the emergency brake.
39.3 Automated canary analysis (the depth interviewers probe)
- Compare canary against a contemporaneous baseline cohort of the same size — not against the whole fleet (different scale skews percentiles) and not against last week (traffic mix differs)
- Metric set: the SLIs (error rate, p50/p99 latency) + saturation (CPU, memory, GC) + a small set of business guardrails (checkout rate, search CTR). Kayenta-style scoring: per-metric pass/fail (Mann-Whitney U or similar), weighted aggregate, promote/hold/rollback thresholds
- Bake time matters as much as percentage: memory leaks, cache-warmup effects, and cron-triggered paths need hours, not minutes, at each step. A canary schedule is percentage × duration, e.g., 1%/30m → 5%/1h → 25%/2h → 100%
- Statistical honesty: at 1% of low traffic you cannot detect a 0.1% error-rate regression — know your minimum detectable effect, or your canary is a ritual
- Auto-rollback on breach with a human notification, not a human approval — at 3 a.m. the automation is the on-call
- What canaries can't catch: slow-burn data corruption, issues triggered by scale itself, coordination bugs that need both versions interacting. Name these limits; it's the difference between using a tool and understanding it
39.4 Canary vs A/B testing — the distinction interviewers fish for
Same mechanism (traffic splitting), different question and different math:
- Canary asks "is this version safe?" — operational metrics, minutes-to-hours horizon, asymmetric decision (any regression → rollback), run by the deploy system
- A/B asks "is this change better?" — product metrics, days-to-weeks horizon, requires pre-registered hypothesis, power analysis, fixed horizon or sequential correction (§30.5), run by the experimentation platform
- The seasoned line: every A/B variant rides through canary first — safety gates before measurement begins. Conflating them produces the classic failure: "the experiment shows +2% conversion" from a two-day peek with no power analysis, on a variant that was also 30 ms slower and nobody checked.
- Shared infra, separate concerns: one traffic-splitting layer can serve both, but ownership, metrics, and stopping rules must stay distinct
39.5 State, data, and the deploy
- Every strategy above assumes N-1 (ideally N-2) compatibility between code and schema: expand–contract (§30.4) is not optional garnish, it's the precondition. Column adds are safe; renames/drops ride the contract phase only after all code versions in the wild stopped reading them
- Event/message compatibility: consumers deploy before producers when the schema changes; schema registry compatibility modes (§17.1) enforced in CI are the mechanism
- Caches across versions: serialization changes in cached objects break the old version during rollback — version your cache keys with the schema, not the deploy
- Long-lived connections (WebSocket, gRPC streams): drain with connection deadlines and client reconnect logic; a "zero-downtime" deploy that severs 50k WebSockets is not zero-downtime to users
- Stateful services (databases, Kafka, stateful sets): rolling with quorum awareness — never take down more than the fault tolerance (one replica at a time for RF=3), verify catch-up/ISR before proceeding. PodDisruptionBudgets (§19.3) encode exactly this
- Rollback is a forward motion for data: you can roll code back, you cannot un-write data. Migrations that transform data need a reverse migration tested before deploy, or an explicit roll-forward-only declaration with sign-off (§30.4)
39.6 The delivery pipeline as a system (the lead-level answer)
When asked "walk me through how a change ships," narrate the whole system:
- Trunk merge behind a flag (§30.1–30.2) → CI: tests, contract checks, security scans, artifact build, immutable artifact signed once, promoted everywhere
- Auto-deploy to staging → smoke + synthetic checks
- Canary in the lowest-traffic production region, ACA gates each step (§39.3)
- Ring/region rollout with bake times; deploy freezes as data-driven policy (error-budget-based, §28.1), not calendar superstition — though respecting genuine peak events (Black Friday) is judgment, not superstition
- Flag ramp-up separately from deploy (deploy ≠ release, §30.4) with its own metrics
- Post-deploy: dashboards linked from the deploy notification, deploy markers on every graph (the single cheapest observability win — most incidents correlate with a deploy, and the marker turns an hour of diagnosis into a glance)
- Everything above applies to config and flags too — config changes cause as many outages as code and usually ship with less ceremony. Config canarying is a mature-org tell.
Metrics that prove the system works: deploy frequency and lead time (§23.9), rollback rate, mean-time-to-rollback (minutes, not "we'd redeploy"), % of deploys that are automated end to end, and change failure rate split by code vs config vs flag.
39.7 Drill bank additions
- Canary vs A/B — a PM says "the canary shows the feature is winning." Correct the confusion kindly.
- Design the canary schedule for a payments service doing 200 QPS. What can't you detect, and what do you do about that?
- Shadow-test a rewrite of a service that sends emails. Walk the side-effect containment.
- A rollback fails because the old version can't read new-format cache entries. What was the process failure, and what's the fix?
- Your mobile app's new version crashes for 2% of users; the store rollout is at 40%. Walk the response.
- Deploy a schema change that renames a column across a 3-version compatibility window. Sequence it.
§39 added: consolidated deployment and progressive delivery — strategy matrix, traffic-shifting mechanics, automated canary analysis, canary-vs-A/B, state and data coordination, and the pipeline-as-a-system narrative.
PART V — EXPANDED REFERENCE
Parts I–IV are the map. Part V is the territory: every term defined, every claim shown with code or a real system, every topic closed with follow-up questions and primary sources. Read Part V when you want to actually know the thing; read Parts I–IV to remember what to review.
Chapter roadmap (built in order of interview weight):
- §40 Distributed Systems & Performance Foundations ← this turn
- §41 LLM Serving, Inference & the Economics of Tokens ← this turn
- §42 Storage Engines & Databases, Expanded
- §43 Retrieval, Search & Ranking, Expanded
- §44 Agentic Systems & Context Engineering, Expanded
- §45 Kubernetes, Compute & the Kernel, Expanded
- §46 Streaming, Reactive & API Layers, Expanded
- §47 Security & Zero Trust, Expanded
- §48 Deployment, SRE & DR, Expanded
- §49 Leadership Scenarios: Full Worked Answers
- §50 Complete Drill Bank Answers (all 66+)
40. Distributed Systems & Performance Foundations
40.1 The Latency Numbers — and What to Actually Do With Them
The canonical table (Jeff Dean's "Numbers Everyone Should Know," updated for modern hardware). Memorize the orders of magnitude, not the digits.
| Operation | Latency | Mental anchor |
|---|---|---|
| L1 cache reference | 1 ns | 1 second |
| Branch mispredict | 3 ns | 3 seconds |
| L2 cache reference | 4 ns | 4 seconds |
| Mutex lock/unlock (uncontended) | 17 ns | 17 seconds |
| Main memory reference | 100 ns | 1.5 minutes |
| Compress 1 KB with Snappy | 2 µs | 33 minutes |
| Read 1 MB sequentially from memory | 3 µs | 50 minutes |
| SSD random read (NVMe) | 16 µs | 4.5 hours |
| Read 1 MB sequentially from NVMe SSD | 50 µs | 14 hours |
| Round trip within same datacenter | 500 µs | 5.8 days |
| Read 1 MB sequentially from spinning disk | 2 ms | 23 days |
| Disk seek | 3–10 ms | 35–115 days |
| Round trip CA → Netherlands → CA | 150 ms | 4.8 years |
Derived facts worth having on instant recall:
- Memory is ~100× faster than NVMe; NVMe is ~100× faster than a network round trip across regions. Every architecture decision is a placement decision on this ladder.
- Sequential is ~100–1000× faster than random on disk, ~4× on memory (prefetching). This single fact is why LSM trees exist, why Kafka is fast, and why columnar formats win at analytics.
- Speed of light in fiber ≈ 200,000 km/s → ~5 µs per km, ~1 ms per 100 km round trip. You cannot get from Toronto to Frankfurt in under ~60 ms. Ever. When someone asks for a 20 ms global p99, the answer is edge presence, not optimization.
- A 1 Gbps link moves ~125 MB/s → 1 GB takes 8 seconds. A 10 Gbps link, 0.8 s.
Worked back-of-envelope: sizing a search service
"Design search for 50M monthly actives, 20 searches/user/month, p99 < 200 ms, 500M documents."
Traffic
50M MAU × 20 searches = 1B searches/month
1B / (30 × 86400) ≈ 386 QPS average
Peak = 3-5× average → ~1,500 QPS peak
Design headroom 2× → provision for 3,000 QPS
Storage (inverted index)
500M docs × 2 KB avg text = 1 TB raw
Inverted index ≈ 30-50% of raw text → ~400 GB
Stored fields + doc values → ~1.5× → ~600 GB
Replication factor 2 → 1.2 TB total
Shard target 30 GB → 600/30 = 20 primary shards, 40 total
Vector index (if hybrid)
500M × 768 dims × 4 bytes (fp32) = 1.5 TB just for vectors
→ int8 quantization: 384 GB. Still too big for one node.
→ HNSW graph overhead ≈ M × 8 bytes × N; M=16 → 64 GB
Conclusion stated out loud: "vectors don't fit in RAM at fp32;
I'd quantize to int8 and shard across ~8 nodes with 64 GB each,
or use IVF-PQ if recall tolerance allows."
Latency budget (p99 = 200 ms total)
Network in/out 10 ms
Query parsing/rewrite 5 ms
Lexical retrieval (BM25) 30 ms ← fan-out to 20 shards, gather
Vector retrieval (ANN) 40 ms ← parallel with lexical
Fusion (RRF) 2 ms
Reranking (cross-encoder, top-100) 80 ms ← the fat one
Serialization/response 10 ms
─────────────────────────────────
Total ~177 ms, 23 ms slack
Immediately name the risk: "reranking is 45% of the budget and
it's a GPU call — that's my tail-latency exposure. I'd cap it at
top-50, set an 80 ms deadline, and fall back to fusion-only
ranking on timeout rather than blowing the SLO."
That last paragraph — naming where the tail lives and pre-committing a degradation — is what separates a staff answer from a senior one.
Little's Law: the one formula that governs capacity
L = λW — average number of items in a system = arrival rate × average time in system.
Applied to a service: concurrency = throughput × latency.
A service holds p99 latency of 50 ms and must serve 3,000 QPS.
concurrency = 3000 × 0.050 = 150 concurrent requests in flight.
If each request needs a thread and a DB connection:
→ 150 threads minimum (plus headroom → ~200)
→ the DB pool must sustain 150 concurrent queries, or the pool
becomes the bottleneck and latency rises, which raises
concurrency, which exhausts the pool — the classic death spiral.
Inverted: your connection pool is 50. What throughput can you serve
at 50 ms latency?
λ = L/W = 50/0.050 = 1,000 QPS. Hard ceiling. No amount of
application servers changes it.
Interview use: when someone asks "how many instances do you need," Little's Law is the answer, not intuition. When someone asks "why did latency explode at 80% load," the answer is queueing theory below.
Queueing theory: why you run at 70%
For an M/M/1 queue, average wait time scales as W = S / (1 − ρ) where S is service time and ρ is utilization.
| Utilization ρ | Latency multiplier 1/(1−ρ) |
|---|---|
| 50% | 2× |
| 70% | 3.3× |
| 80% | 5× |
| 90% | 10× |
| 95% | 20× |
| 99% | 100× |
This is the entire justification for headroom targets. At 90% CPU your latency is 10× its unloaded value and a 5% traffic bump doubles it again. The knee is around 70–80%; that's why SRE capacity targets live there — not conservatism, arithmetic.
Real variability (M/G/1) makes it worse: W = S·ρ/(1−ρ) · (1+C²)/2, where C is the coefficient of variation of service time. High variance in service time destroys tail latency even at moderate utilization — which is why one slow query type poisons a shared thread pool, and why bulkheads (separate pools per workload class) exist.
The Universal Scalability Law — why adding machines stops helping
Amdahl's Law says the serial fraction caps your speedup. Gunther's USL adds the term that actually bites in distributed systems:
C(N) = N / (1 + α(N−1) + βN(N−1))
α = contention (serialization, locks, the serial fraction)
β = coherence (crosstalk — nodes must agree with each other)
The β term is the killer: because it's quadratic in N, throughput doesn't just plateau, it declines past an optimum. This is why:
- A 5-node etcd cluster is faster than a 7-node one for writes (more nodes = more replication coherence, same quorum semantics)
- Adding app servers to a system bottlenecked on a shared lock makes things worse
- Cassandra scales near-linearly (β≈0, no cross-node coordination on the write path) while a distributed transaction system does not
Interview line: "I'd expect this to scale linearly until the coordination term dominates — for this design, that's the shared sequence generator, and I'd remove it with client-side ID generation (Snowflake-style) before it becomes the ceiling."
The Tail at Scale — Dean & Barroso's playbook
The core insight: if a request fans out to 100 servers and each has a 1% chance of a >1s response, then 63% of requests take >1s (1 − 0.99¹⁰⁰). Tail latency at the leaf becomes median latency at the root. Fan-out amplifies tails.
Google's mitigations, in the order you should name them:
- Hedged requests — send to one replica; if no response by p95, send a duplicate to another replica, take the first answer, cancel the other. Google reported this cutting p99 dramatically at ~2% extra load. Cheap and effective.
- Tied requests — send to two replicas simultaneously, each request carrying the identity of the other. Whichever server starts the work first sends a cancellation to its twin. Removes the p95 wait of hedging at the cost of a small window of duplicate work.
- Micro-partitioning — partition into far more shards than machines (e.g., 20 partitions per machine), so rebalancing is fine-grained and a hot partition can be moved without moving a machine's whole load. This is how Bigtable and Slicer manage skew.
- Selective replication — detect hot items and add replicas for those items only.
- Latency-induced probation — temporarily exclude a slow replica from the pool while continuing to send it shadow traffic to detect recovery.
- Request reissue against a different replica on timeout, with per-request deadlines propagated through the whole call tree (gRPC does this natively; §46).
Implementation sketch (hedging in Go):
// Hedged request: fire the backup at p95 of observed latency.
func hedgedGet(ctx context.Context, replicas []Client, key string,
hedgeAfter time.Duration) (Result, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel() // cancels the loser as soon as we return
results := make(chan Result, len(replicas))
errs := make(chan error, len(replicas))
launch := func(c Client) {
r, err := c.Get(ctx, key)
if err != nil { errs <- err; return }
results <- r
}
go launch(replicas[0])
timer := time.NewTimer(hedgeAfter)
defer timer.Stop()
for i := 1; ; {
select {
case r := <-results:
return r, nil // first winner wins
case <-timer.C:
if i < len(replicas) {
go launch(replicas[i]); i++
timer.Reset(hedgeAfter)
}
case <-ctx.Done():
return Result{}, ctx.Err()
}
}
}
The critical operational caveat to state: hedging under overload is an amplifier — if the system is slow because it's saturated, hedging adds load and accelerates collapse. Gate it: hedge only when the hedge rate is below a threshold (e.g., ≤5% of requests), and disable it when the circuit breaker is open. Say this; it's the difference between having read the paper and having run it.
Performance engineering discipline (the abseil/Google "Fast" hints, generalized)
The habits that make performance work real rather than folkloric:
- Measure, never guess. Intuition about hot spots is wrong most of the time. The first action in any performance investigation is a profile, not a code read.
- Benchmark the right thing. Microbenchmark pitfalls: dead-code elimination (the compiler deletes your unused result — use
benchmark::DoNotOptimize, JMH'sBlackhole, Go's assignment to a package-level sink), constant folding, unrealistic cache warmth (your benchmark's working set fits in L2; production's doesn't), and missing branch-predictor entropy. - Prefer end-to-end benchmarks and production profiles over microbenchmarks when they disagree — and they will. Google-Wide Profiling (always-on, sampled, fleet-wide profiling) exists because lab benchmarks systematically mislead.
- Optimize the memory hierarchy first. Most "CPU-bound" code is actually memory-latency-bound: cache misses, pointer chasing, and false sharing. Data layout beats instruction count. Struct-of-arrays over array-of-structs for scanning workloads; pad hot mutable fields to cache lines (64 bytes) to avoid false sharing between cores.
- Allocation is a first-class cost. In managed languages, allocation rate drives GC pressure, which drives tail latency. Object pooling, arena allocation, and avoiding per-request garbage are the standard fixes; in Go,
sync.Pooland escape analysis; in Java, watch allocation rate in JFR before touching GC flags. - Amdahl before micro-optimization: a 10× speedup of a component that is 5% of the total buys you 4.7%. Profile to find the 60% before touching the 5%.
- Beware the benchmark that measures the framework. If your load generator, serialization, or logging dominates, you're tuning noise.
Practical benchmarking, three languages:
// JMH — the only credible way to microbenchmark on the JVM.
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Fork(value = 2, jvmArgs = {"-Xms2G", "-Xmx2G"})
@Warmup(iterations = 5, time = 1) // JIT needs warmup; without
@Measurement(iterations = 10, time = 1) // this you measure the interpreter
public class HashBenchmark {
private byte[] data;
@Setup public void setup() {
data = new byte[1024];
ThreadLocalRandom.current().nextBytes(data);
}
@Benchmark public long xxhash(Blackhole bh) {
long h = XXHash.hash(data);
bh.consume(h); // prevents dead-code elimination
return h;
}
}
// Go — note b.ReportAllocs and the sink to defeat the optimizer.
var sink uint64
func BenchmarkHash(b *testing.B) {
data := make([]byte, 1024)
rand.Read(data)
b.ResetTimer()
b.ReportAllocs() // allocations are the usual culprit
for i := 0; i < b.N; i++ {
sink = xxhash.Sum64(data)
}
}
// go test -bench=. -benchmem -count=10 | benchstat -
// Always -count>=10 and pipe through benchstat: single runs are noise.
# Profiling a live Go service (works on any pprof-enabled binary)
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap
# Linux, any language: sample the whole process and make a flame graph
perf record -F 99 -p $(pgrep -f myservice) -g -- sleep 30
perf script | stackcollapse-perf.pl | flamegraph.pl > cpu.svg
# JVM: async-profiler avoids safepoint bias that afflicts jstack-based tools
./profiler.sh -d 30 -e cpu -f /tmp/flame.html $(pgrep -f java)
./profiler.sh -d 30 -e alloc -f /tmp/alloc.html $(pgrep -f java)
# Where is the time going at the syscall level?
strace -c -p $(pgrep -f myservice) # summary of syscall counts/time
bpftrace -e 'tracepoint:syscalls:sys_enter_futex { @[comm] = count(); }'
Follow-up questions you should expect here:
- "Your p50 is fine and p99 is terrible. Where do you look?" → GC pauses, lock contention, queueing at a saturated resource, a slow dependency's tail amplified by fan-out, noisy neighbors, or a cold cache path. Order the checks by cost-to-verify: GC logs and thread dumps first, then per-dependency latency histograms, then the fan-out arithmetic.
- "Why can't you average percentiles across instances?" → Percentiles aren't linear. Averaging ten instances' p99 gives a number that corresponds to nothing. You must merge the underlying histograms (HDR histogram, Prometheus native histograms, t-digest).
- "What's coordinated omission?" → When a load generator waits for a response before sending the next request, a stall suppresses the requests that would have measured it, so the tail vanishes from the data. Fix: constant-rate (open model) load generation, e.g.,
wrk2, or k6 with arrival-rate executors.
Further reading: Dean & Barroso, The Tail at Scale (CACM 2013) — read this one twice. Gunther, Guerrilla Capacity Planning (USL). Gregg, Systems Performance (2nd ed.) — the USE method and the tooling. abseil.io/fast/ hints for C++-flavored but universally applicable benchmarking discipline. Google-Wide Profiling (Ren et al., 2010).
40.2 Consensus: Paxos, Raft, and What Runs in Production
The problem, precisely
Consensus: a set of processes must agree on a single value, satisfying agreement (no two decide differently), validity (the decided value was proposed), and termination (every correct process eventually decides).
FLP impossibility (Fischer, Lynch, Paterson 1985): in an asynchronous system with even one crash failure, no deterministic algorithm guarantees consensus. This isn't academic pedantry — it's why every real system uses timeouts. Timeouts are an imperfect failure detector that buys you termination in practice by sacrificing the guarantee in theory. When an interviewer asks "what if the leader is just slow, not dead," FLP is the honest answer: you cannot distinguish them, so you design for the consequence (a spurious election, fenced by terms/epochs).
Paxos
Single-decree Paxos, two phases, three roles (proposer, acceptor, learner):
Phase 1a PREPARE(n) proposer picks proposal number n, sends to acceptors
Phase 1b PROMISE(n, ...) acceptor promises not to accept anything < n;
returns the highest-numbered proposal it already accepted
Phase 2a ACCEPT(n, v) proposer sends value v — which MUST be the value from the
highest-numbered accepted proposal it saw, if any exists;
only if none exists may it propose its own value
Phase 2b ACCEPTED(n, v) acceptor accepts unless it promised to a higher n
That constraint in Phase 2a is the entire safety argument, and it's the part people get wrong: a proposer may not propose its own value if any acceptor has already accepted something. It must adopt the existing value. This is what makes the algorithm safe under arbitrary message reordering.
Why Paxos has a reputation: the paper describes a protocol for deciding one value. Real systems need a log of values. Multi-Paxos adds a stable leader that skips Phase 1 for subsequent slots — and everything hard (leader election, log compaction, membership change, gap filling) lives in the part the paper doesn't specify. Google's Paxos Made Live documents exactly this gap: turning the algorithm into Chubby required solving disk corruption, master leases, membership changes, and testing infrastructure — none of it in the original paper.
Where Paxos actually runs: Google Chubby (the lock service Bigtable and GFS depend on), Google Spanner (a Paxos group per tablet/split), Microsoft Azure Storage, Neo4j causal clustering (Raft actually), Apache Cassandra's lightweight transactions (a Paxos round per LWT — which is why IF NOT EXISTS costs ~4× a normal write).
Raft — understandable by design
Raft decomposes consensus into three sub-problems, which is why it won adoption: leader election, log replication, safety.
State every node holds:
type RaftState struct {
// Persistent — must survive a crash; fsync BEFORE responding to RPCs.
currentTerm int // latest term seen
votedFor int // candidate voted for in currentTerm (or none)
log []Entry // each Entry: {Term int, Index int, Command []byte}
// Volatile — all servers
commitIndex int // highest index known committed
lastApplied int // highest index applied to the state machine
// Volatile — leaders only, reinitialized after election
nextIndex []int // per follower: next log index to send
matchIndex []int // per follower: highest index known replicated
}
type AppendEntriesArgs struct {
Term int // leader's term
LeaderId int
PrevLogIndex int // index of entry immediately preceding new ones
PrevLogTerm int // term of that entry ← the consistency check
Entries []Entry // empty for heartbeat
LeaderCommit int
}
type RequestVoteArgs struct {
Term int
CandidateId int
LastLogIndex int // used for the up-to-date check
LastLogTerm int
}
The five properties that make it safe (be able to name at least three):
- Election Safety — at most one leader per term
- Leader Append-Only — a leader never overwrites or deletes its own entries
- Log Matching — if two logs contain an entry with the same index and term, the logs are identical in all preceding entries. Enforced by the
PrevLogIndex/PrevLogTermcheck on every AppendEntries. - Leader Completeness — a leader for term T contains all entries committed in terms < T. Enforced by the up-to-date vote restriction: a voter refuses a candidate whose last log entry is older (lower term, or same term but shorter).
- State Machine Safety — if a server applies an entry at index i, no other server applies a different entry at i.
The subtle rule people miss: a leader may only mark an entry committed once it has replicated an entry from its own current term to a majority. Counting replicas on an inherited entry from a previous term is unsafe (Figure 8 in the paper). The standard implementation trick is a no-op entry appended immediately on election, which both commits the tail of the previous term and confirms leadership.
Election mechanics:
- Randomized election timeouts (typically 150–300 ms, must be ≫ broadcast time) to avoid split votes
- Terms act as a logical clock; any message with a higher term forces a step-down to follower
- Pre-vote extension: before incrementing its term, a candidate asks whether it would win. Prevents a partitioned node from rejoining with an inflated term and disrupting a healthy leader. Any production Raft has this; mentioning it signals real familiarity.
- CheckQuorum / leader leases: a leader that can't reach a quorum steps down, and leases let it serve linearizable reads locally without a round trip.
Membership changes: joint consensus (both old and new configurations must agree during transition) in the original paper; most implementations use the simpler single-server-at-a-time change, which avoids overlapping-majority problems.
Log compaction: snapshots of the state machine plus InstallSnapshot RPC for followers that have fallen too far behind. Getting this wrong is the most common source of production Raft bugs.
Performance realities:
- Every commit costs one round trip to a majority plus an
fsync. Your write latency floor is fsync latency + intra-cluster RTT — roughly 1–2 ms with NVMe in one AZ, 5–15 ms across AZs. If someone asks why etcd is "slow," this is why. - Batching and pipelining are essential: batch multiple client commands into one AppendEntries; pipeline by not waiting for a response before sending the next batch.
- The leader is a bottleneck (all writes flow through it). The fix at scale is Multi-Raft: partition the keyspace into many ranges, each with its own Raft group and its own leader, spreading leadership across the cluster. This is precisely what CockroachDB and TiKV do — a range per ~512 MB of data, thousands of Raft groups per cluster, with leadership balanced by an allocator.
- Cluster sizing: 3 nodes tolerates 1 failure, 5 tolerates 2. Beyond 5 you pay replication cost for diminishing availability. Never use an even number — 4 nodes tolerates the same 1 failure as 3 while being slower and more likely to lose quorum.
Where Raft runs: etcd (and therefore all of Kubernetes' control plane state), Consul, CockroachDB (per-range), TiKV/TiDB, MongoDB (Raft-like replica set protocol), Kafka KRaft (which replaced ZooKeeper — the metadata log is itself a Raft log), RethinkDB, Neo4j, Redpanda, InfluxDB.
ZAB (ZooKeeper Atomic Broadcast) predates Raft, solves the same problem with a different decomposition, and guarantees primary order — which ZooKeeper needs and generic consensus doesn't provide. Viewstamped Replication (Oki & Liskov, 1988) is arguably first and remarkably close to Raft.
Modern variants worth naming: EPaxos (leaderless, commits in one round trip when commands don't conflict), Flexible Paxos (quorum intersection only needs to hold between phases, enabling smaller Phase-2 quorums), Raft with witness/learner replicas (cheap tie-breakers holding no data), and Delos at Meta (a virtual consensus layer that lets you swap the underlying consensus protocol without downtime — a genuinely clever piece of engineering to reference).
Follow-up questions:
- "Do you need consensus here?" — the best answer is often no. Consensus is expensive; use it for metadata, leadership, and configuration, not for the data path. Cassandra, Dynamo, and S3 achieve massive scale by avoiding consensus for normal writes (quorum replication is not consensus — it doesn't guarantee agreement on order).
- "How do you do linearizable reads without a round trip?" — leader leases (safe only with bounded clock drift), or ReadIndex (the leader confirms leadership with a heartbeat round, then serves from local state), or just accept follower reads with a staleness bound.
- "What happens in a network partition?" — the majority side elects/keeps a leader and continues; the minority side cannot commit and must reject writes. If the minority side keeps serving reads, you've chosen availability over linearizability, and you should say so explicitly.
- "Why did Kafka move off ZooKeeper?" — operational simplicity (one system, not two), metadata scalability (the controller's ZooKeeper read amplification limited partition counts), and faster failover. KRaft stores metadata as an event log replicated by Raft.
Further reading: Ongaro & Ousterhout, In Search of an Understandable Consensus Algorithm (Raft, USENIX ATC 2014) and Ongaro's PhD thesis (the complete treatment of membership changes and compaction). Lamport, Paxos Made Simple. Chandra, Griesemer & Redstone, Paxos Made Live (Google, PODC 2007) — the single most useful paper on the gap between algorithm and system. raft.github.io for the visualization; read the etcd raft package source, which is a clean, production-grade implementation with the state machine isolated from I/O.
40.3 Replication, Consistency & Time
The consistency model ladder
Precise definitions, strongest to weakest — mixing these up is a common and costly interview error:
- Linearizability (single-object, real-time): every operation appears to take effect atomically at some point between its invocation and response, consistent with real time. This is "the system behaves like a single copy."
- Serializability (multi-object, transactional): the outcome of concurrent transactions equals some serial order. Note: no real-time requirement — a serializable system may serve you stale data forever and remain correct.
- Strict serializability = serializability + linearizability. What Spanner and CockroachDB provide.
- Sequential consistency: all processes see the same order of operations, but that order need not respect real time.
- Causal consistency: operations causally related are seen in order by everyone; concurrent operations may be seen in any order. The strongest model achievable while remaining available under partition (per the CALM theorem's neighborhood).
- Read-your-writes / monotonic reads / monotonic writes / writes-follow-reads: the four session guarantees. Most "eventual consistency is fine" systems actually need these, and providing them is cheap (sticky routing, or a client-held version token).
- Eventual consistency: replicas converge if writes stop. Says nothing about when.
The gotcha to have ready: "Is serializability stronger than linearizability?" Neither — they're orthogonal. Linearizability is about recency on single objects; serializability is about transaction isolation across objects. Being able to say this crisply is a strong signal.
Isolation anomalies (SQL isolation levels are defined by which they permit):
| Anomaly | Description | Prevented by |
|---|---|---|
| Dirty read | Read uncommitted data | Read Committed |
| Non-repeatable read | Same row read twice differs | Repeatable Read |
| Phantom read | Same query returns new rows | Serializable (or predicate locks) |
| Lost update | Two read-modify-writes, one lost | RR in most engines / SELECT FOR UPDATE |
| Write skew | Two txns read overlapping data, write disjoint data, together violating an invariant | Only Serializable |
Write skew is the one worth knowing deeply because PostgreSQL's REPEATABLE READ is snapshot isolation, which permits it:
-- Invariant: at least one doctor must remain on call.
-- Two doctors, both on call, both try to go off call simultaneously.
-- Txn A -- Txn B
BEGIN ISOLATION LEVEL REPEATABLE READ; BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT count(*) FROM doctors SELECT count(*) FROM doctors
WHERE on_call = true; -- returns 2 WHERE on_call = true; -- returns 2
UPDATE doctors SET on_call = false UPDATE doctors SET on_call = false
WHERE id = 1; WHERE id = 2;
COMMIT; -- succeeds COMMIT; -- succeeds
-- Result: zero doctors on call. Both transactions were "correct."
-- Fixes, in order of preference:
-- 1. SET TRANSACTION ISOLATION LEVEL SERIALIZABLE (Postgres SSI detects
-- the dangerous structure and aborts one — retry logic required)
-- 2. Materialize the conflict: SELECT ... FOR UPDATE on the rows read
-- 3. Move the invariant into a constraint the database can enforce
Postgres's SSI (Serializable Snapshot Isolation) detects "dangerous structures" (a rw-antidependency cycle) and aborts a transaction with 40001. Every serializable system requires retry logic in the application — if a candidate proposes SERIALIZABLE without mentioning retries, they haven't run it.
Time in distributed systems
- Physical clocks lie. NTP drift, leap seconds, VM pauses, and clock skew of tens to hundreds of milliseconds between machines are normal. Never order events by wall clock across machines.
- Lamport clocks: a counter per process, incremented on each event, max'd on receive. Gives you happens-before (
a → b ⟹ L(a) < L(b)) but not the converse. - Vector clocks: a vector of counters, one per node. Gives you the converse too — you can detect concurrency, hence conflicts. Used by Dynamo and Riak. Cost: size grows with participants; requires pruning.
- Google TrueTime (Spanner): GPS + atomic clocks in every datacenter expose an interval
[earliest, latest]with a bounded uncertainty ε (single-digit ms). Spanner's commit wait deliberately sleeps out the uncertainty window before releasing locks, which is how it achieves external consistency (strict serializability) globally. It trades latency for correctness and requires special hardware — that tradeoff is the whole point of the design. - Hybrid Logical Clocks (HLC): combine physical time with a logical counter, giving timestamps that are close to wall clock but never violate causality — without atomic clocks. CockroachDB and YugabyteDB use HLC, which is why they need a configured
max-offsetand will shut down a node that exceeds it rather than risk a consistency violation. This is the practical answer for anyone who doesn't own a datacenter with GPS receivers.
CRDTs — conflict-free replicated data types
Data types whose merge function is commutative, associative, and idempotent, so replicas converge without coordination.
- State-based (CvRDT): ship full state, merge with a join (least upper bound). Simple, bandwidth-heavy; delta-CRDTs fix the bandwidth.
- Operation-based (CmRDT): ship operations; requires exactly-once, causally-ordered delivery.
- The catalog: G-Counter (grow-only), PN-Counter (two G-Counters), G-Set, 2P-Set, LWW-Register, OR-Set (observed-remove — the one that handles add/remove correctly), RGA/Logoot/YATA for sequences (collaborative text).
Real production use: Redis Enterprise active-active geo-replication is CRDT-based. Riak shipped CRDTs as a first-class data type. Figma's multiplayer uses a CRDT-inspired scheme (they've written publicly that they use a simplified LWW approach rather than a full CRDT because their server is authoritative — a great example of choosing the pragmatic 80%). Automerge and Yjs are the reference libraries for collaborative editing; Yjs's YATA algorithm is what most modern collaborative editors build on. Apple Notes and Teletype (Atom) used CRDTs.
The honest limitation to name: CRDTs guarantee convergence, not correctness. Two users concurrently decrementing an inventory counter converge to a value that may be negative. If your invariant is "stock ≥ 0," a CRDT will not save you — you need coordination. Saying this shows you understand the boundary rather than treating CRDTs as magic.
# OR-Set: the CRDT that actually gets add/remove semantics right.
# Naive 2P-Set can never re-add a removed element; OR-Set can,
# because each add carries a unique tag and removes only kill
# the tags they observed.
class ORSet:
def __init__(self):
self.adds = {} # element -> set of unique tags
self.removes = {} # element -> set of tombstoned tags
def add(self, element):
tag = uuid.uuid4()
self.adds.setdefault(element, set()).add(tag)
def remove(self, element):
# Only removes tags this replica has OBSERVED. A concurrent
# add on another replica has a tag we haven't seen, so it survives.
observed = self.adds.get(element, set())
self.removes.setdefault(element, set()).update(observed)
def contains(self, element):
return bool(self.adds.get(element, set()) - self.removes.get(element, set()))
def merge(self, other): # commutative, associative, idempotent
for e, tags in other.adds.items():
self.adds.setdefault(e, set()).update(tags)
for e, tags in other.removes.items():
self.removes.setdefault(e, set()).update(tags)
Replication topologies in practice
- Single-leader (Postgres streaming, MySQL, MongoDB replica sets): simple, no write conflicts, but failover is the hard part. Read-after-write requires sticky-to-primary routing for a window, or an LSN/token the client presents.
- Multi-leader (multi-region writes, CouchDB, DynamoDB global tables): write conflicts are inevitable; resolution is LWW (data loss, but simple — DynamoDB global tables do this and it is not configurable), application-defined merge, or CRDTs.
- Leaderless / quorum (Dynamo, Cassandra, Riak):
R + W > RFfor overlap. Read repair, hinted handoff, and anti-entropy (Merkle-tree repair) restore convergence. Quorum is not consensus — concurrent writes to the same key withW=quorumcan produce conflicting versions with no agreed order. - Chain replication (used in Meta's Delos, Azure Storage, and CORFU-style systems): writes go head→tail, reads served at the tail. Gives linearizability with high throughput and simple recovery; cost is write latency proportional to chain length.
Jepsen — Kyle Kingsbury's testing work is the field's reality check. His findings repeatedly showed that databases' consistency marketing outran their implementations (early MongoDB, Elasticsearch, Redis Sentinel, and many others lost acknowledged writes under partition). Two interview-usable points: (1) "I'd want to see a Jepsen report before trusting a consistency claim" is a credible, senior thing to say; (2) the general lesson — the failure modes that matter are partial: partitions that heal asymmetrically, processes that pause for 30 seconds and resume, clocks that jump. Not clean crashes.
Further reading: Kleppmann, DDIA ch. 5, 7, 9 (the best treatment in print). Bailis et al., Highly Available Transactions. Corbett et al., Spanner (OSDI 2012). DeCandia et al., Dynamo (SOSP 2007). Shapiro et al., A Comprehensive Study of CRDTs. jepsen.io analyses — read the one for whatever database you're about to be interviewed on.
40.4 Partitioning, Hashing & Rebalancing
Consistent hashing — the actual mechanics
Naive hash(key) mod N remaps ~all keys when N changes. Consistent hashing remaps ~K/N.
import bisect, hashlib
class ConsistentHashRing:
"""Virtual nodes are not optional: with 1 token per physical node,
load variance is ~±30%. With 100-200 vnodes, variance drops to a
few percent (standard deviation ~1/sqrt(vnodes))."""
def __init__(self, nodes=None, vnodes=150):
self.vnodes = vnodes
self.ring = {} # hash -> node
self.sorted_keys = []
for n in (nodes or []):
self.add_node(n)
def _hash(self, key):
return int(hashlib.md5(key.encode()).hexdigest()[:8], 16)
def add_node(self, node):
for i in range(self.vnodes):
h = self._hash(f"{node}#{i}")
self.ring[h] = node
bisect.insort(self.sorted_keys, h)
def remove_node(self, node):
for i in range(self.vnodes):
h = self._hash(f"{node}#{i}")
del self.ring[h]
self.sorted_keys.remove(h)
def get_node(self, key):
if not self.ring: return None
h = self._hash(key)
idx = bisect.bisect_right(self.sorted_keys, h) % len(self.sorted_keys)
return self.ring[self.sorted_keys[idx]]
def get_replicas(self, key, n):
"""Walk clockwise, skipping vnodes of already-chosen physical nodes —
otherwise all N replicas can land on one machine."""
if not self.ring: return []
h = self._hash(key)
idx = bisect.bisect_right(self.sorted_keys, h) % len(self.sorted_keys)
seen, out = set(), []
for i in range(len(self.sorted_keys)):
node = self.ring[self.sorted_keys[(idx + i) % len(self.sorted_keys)]]
if node not in seen:
seen.add(node); out.append(node)
if len(out) == n: break
return out
Alternatives worth naming (this is where you separate from the pack):
- Rendezvous / HRW hashing — for each key, compute
hash(key, node)for all nodes and take the max. O(N) per lookup but needs no ring state, gives perfect minimal disruption, and handles weights cleanly. Better than consistent hashing for small N. - Jump consistent hash (Lamping & Veach, Google) — ~5 lines, no memory, perfectly balanced, O(ln N). The catch: it only maps to buckets
0..N-1and cannot handle arbitrary node removal (only shrinking from the end). Perfect for sharding into a fixed number of shards.
// Jump consistent hash — Google. No storage, no ring, perfectly uniform.
int32_t JumpConsistentHash(uint64_t key, int32_t num_buckets) {
int64_t b = -1, j = 0;
while (j < num_buckets) {
b = j;
key = key * 2862933555777941757ULL + 1;
j = (b + 1) * ((double)(1LL << 31) / (double)((key >> 33) + 1));
}
return (int32_t)b;
}
- Maglev hashing (Google's software load balancer) — builds a lookup table giving near-perfect balance and minimal disruption, optimized for L4 load balancing where connection affinity matters and backends churn. Used in Google's frontend and adopted in Cilium and Katran (Meta's L4 LB).
Range partitioning
Ordered key ranges instead of hashes. Enables efficient range scans; risks hot spots on sequential keys (timestamps, auto-increment IDs).
Used by: Bigtable/HBase (tablets, split on size), CockroachDB (ranges, default ~512 MB, auto-split and auto-merge), TiKV (regions), Spanner (splits), FoundationDB, DynamoDB internally (partitions with automatic splitting on both size and throughput).
Hot-spot mitigation, the concrete toolkit:
- Salt the key — prefix with
hash(key) % Nto spread; you now need N parallel scans for a range query. DynamoDB's documented write-sharding pattern. - Reverse the timestamp bits so sequential writes distribute (classic HBase advice).
- Split by throughput, not just size — DynamoDB and CockroachDB both split hot ranges even when small. This is the elegant fix.
- Micro-partitioning + adaptive placement (§40.1) — many more partitions than machines lets the balancer move heat.
- Cache the hot key at the client — for read hotspots specifically, the cheapest fix (DynamoDB DAX, or an in-process cache).
Snowflake IDs — how to get roughly-sortable unique IDs without a coordinator, and the standard answer to "how do you generate IDs at scale":
64 bits: [1 unused][41 bits timestamp ms][10 bits machine id][12 bits sequence]
41 bits of ms ≈ 69 years from a custom epoch
10 bits → 1,024 nodes
12 bits → 4,096 IDs per node per millisecond → ~4M IDs/sec/node
Trade-off to name: time-ordered IDs are great for index locality and
terrible for partition balance — they concentrate all writes on the
newest shard. Twitter accepted this (they wanted sortability);
if you don't need sortability, use ULID/UUIDv7 for readability or
random UUIDv4 for perfect spread and awful B-tree locality.
UUIDv7 is the modern compromise: time-ordered prefix + random suffix.
Rebalancing in production
- Never rebalance automatically without rate limits. The failure mode: a node is slow, the balancer decides it's down, moves its data, which saturates the network, which makes more nodes look slow. Cascading rebalance is a real outage class.
- Vitess (YouTube's MySQL sharding layer, now used by Slack, Shopify, GitHub, Square) implements resharding as: create target shards →
VReplicationcopies and then tails the binlog → verify with diff → switch reads → switch writes → clean up. This is the reference implementation of online resharding; if asked how to reshard without downtime, describe this sequence. - Shopify's "pods" — they shard the entire application stack (DB, cache, workers) into isolated pods, so a shard is a full vertical slice. Failure and load are contained per pod. A great architecture to cite for multi-tenant isolation.
- Slicer (Google's general-purpose sharding service) and Shard Manager (Meta's) exist because every team was rebuilding the same rebalancing logic. The lesson for a lead: sharding policy is a platform capability, not a per-service concern.
Follow-up questions:
- "How many vnodes?" — 100–256 per node is the common range; more reduces variance but increases metadata and rebalance granularity. Cassandra's default moved from 256 to 16 (with the newer allocation algorithm) precisely because high vnode counts hurt repair and availability: with 256 vnodes, any two node failures are likely to share a token range, which is bad for availability. This is a great "I've operated this" detail.
- "Your shard key is wrong and you're in production. Now what?" — you cannot change it in place. The path is: dual-write to a new correctly-keyed store → backfill historically → shadow-read and diff → cut reads over → cut writes → decommission. That is weeks-to-months of work, which is exactly why shard key selection gets so much interview attention.
Further reading: Karger et al., Consistent Hashing and Random Trees (1997). Lamping & Veach, A Fast, Minimal Memory, Consistent Hash Algorithm (Google, 2014). Eisenbud et al., Maglev (NSDI 2016). DeCandia et al., Dynamo (SOSP 2007) — read for vnodes, sloppy quorum, and hinted handoff. Vitess docs on VReplication and resharding.
41. LLM Serving, Inference & the Economics of Tokens
This is the chapter that most directly differentiates you, because most interviewers can go one layer deep here and stop. It is also the area where "design an inference batching system" appears near-verbatim in real loops.
41.1 The single most important distinction: prefill vs decode
An LLM request has two phases with completely different hardware characteristics. Everything about serving architecture follows from this.
| Prefill (prompt processing) | Decode (token generation) | |
|---|---|---|
| What happens | Process all N input tokens at once | Generate 1 token at a time, autoregressively |
| Parallelism | All N tokens in parallel — one big matmul | Inherently sequential — token t+1 needs token t |
| Bottleneck | Compute-bound (FLOPs) | Memory-bandwidth-bound (reading weights + KV cache) |
| Arithmetic intensity | High (matrix-matrix) | Very low (matrix-vector) — GPU sits mostly idle |
| Scales with | Prompt length (quadratic in attention, linear in FFN) | Output length × batch size |
| User-visible metric | TTFT (time to first token) | TPOT/ITL (time per output token) |
The consequence that drives every design decision: during decode, a GPU with 3 TB/s of memory bandwidth serving one request at batch size 1 might use under 5% of its FLOPs. It reads the entire model's weights from HBM to produce a single token. The fix is batching — reading those weights once and amortizing across many concurrent sequences. Batching is nearly free throughput during decode and nearly zero benefit during prefill.
Concretely: for a 70B model in fp16, weights are 140 GB. At 3 TB/s HBM bandwidth, you can read them ~21 times per second. That's your ceiling: ~21 tokens/sec at batch size 1 — but ~21 × batch_size tokens/sec aggregate. At batch 64, that's ~1,300 tokens/sec from the same hardware. This arithmetic is the entire business case for batching, and being able to do it out loud is a strong signal.
Roofline framing: the crossover point where decode stops being memory-bound and becomes compute-bound is roughly at batch sizes of 100–500 depending on the GPU's FLOPs:bandwidth ratio. Below it, add batch. Above it, add GPUs.
41.2 The KV cache — what actually limits your service
During decode, attention needs the keys and values of every prior token. Recomputing them each step would be quadratic; caching them makes decode linear. The cache is the memory hog.
KV cache bytes = 2 (K and V)
× num_layers
× num_kv_heads × head_dim (= hidden_size for MHA)
× sequence_length
× batch_size
× bytes_per_element
Worked example — Llama-3-70B-class model, fp16, MHA:
layers=80, hidden=8192, seq=8192, batch=1, 2 bytes
= 2 × 80 × 8192 × 8192 × 1 × 2
= 21.5 GB ← for ONE request at 8k context
An 80 GB H100 holding 140 GB of fp16 weights already doesn't fit
(needs 2 GPUs). With 2×80=160 GB, ~20 GB is left for KV cache
→ ONE concurrent 8k request. That is a catastrophic serving story.
This is why the following exist, and you should name them in this order:
- GQA (Grouped-Query Attention) — share K/V heads across groups of Q heads. Llama-2-70B uses 8 KV heads instead of 64: an 8× KV cache reduction with negligible quality loss. MQA (Multi-Query) is the extreme: 1 KV head. This is the single biggest architectural lever, and it's why modern models all use GQA.
Same model with GQA (8 KV heads of 128 dim = 1024 instead of 8192): 2 × 80 × 1024 × 8192 × 1 × 2 = 2.7 GB per 8k request → ~7 concurrent requests in the same 20 GB. 8× better. - KV cache quantization — store K/V in fp8 or int8. Another 2× on top of GQA, with measurable but usually acceptable quality cost.
- PagedAttention (vLLM) — the systems fix. Classic implementations allocate a contiguous buffer for
max_seq_lenper request, wasting 60–80% to internal fragmentation and reservation for tokens never generated. PagedAttention borrows OS virtual memory: the cache is split into fixed-size blocks (typically 16 tokens), a per-sequence block table maps logical to physical blocks, and blocks are allocated on demand. Waste drops to under one block per sequence. vLLM's paper reported 2–4× throughput improvements from this alone. - Prefix caching / RadixAttention — sequences sharing a prefix (a system prompt, a few-shot preamble, a long document in a multi-turn chat) share the same physical KV blocks via copy-on-write. In production this is often the largest single win, because system prompts are identical across every request. SGLang's RadixAttention organizes cached prefixes in a radix tree with LRU eviction, generalizing this to arbitrary shared prefixes.
- MLA (Multi-head Latent Attention) — DeepSeek's approach: compress KV into a low-rank latent vector, decompress on use. Dramatically smaller cache; a good "I follow the frontier" mention.
- Offloading — spill KV to CPU RAM or NVMe for long-context or low-QPS workloads. Trades PCIe bandwidth for capacity.
Interview line worth memorizing: "The weights determine whether the model fits; the KV cache determines how many users fit. Capacity planning for LLM serving is KV cache planning."
41.3 Continuous batching — the throughput lever
Static batching (the naive approach): collect N requests, run them together until all finish. Fatal flaw — the batch runs until the longest generation completes, and every finished sequence's slot sits idle. With generation lengths varying 10× (common), utilization is terrible.
Continuous / in-flight batching (from the Orca paper, implemented in vLLM, TGI, TensorRT-LLM): schedule at the iteration level, not the request level. After every single forward pass, evict finished sequences and admit waiting ones.
Static batching (batch of 4, generation lengths 100/20/300/50):
step: |████████████████████████████| 300 steps, 3 slots idle most of it
GPU utilization ≈ 39%
Continuous batching:
Seq B finishes at step 20 → slot immediately filled by Seq E
Seq D finishes at step 50 → slot filled by Seq F
Seq A finishes at step 100 → slot filled by Seq G
GPU utilization ≈ 90%+, 2-4x throughput at the same latency
Chunked prefill — the refinement that fixes the remaining problem. A long prefill (say 8k tokens) monopolizes the GPU for hundreds of milliseconds, stalling every decode in flight and spiking inter-token latency for existing users. Chunked prefill splits the prompt into pieces and interleaves them with decode steps, trading a slightly worse TTFT for a dramatically smoother TPOT. Naming this tradeoff — TTFT vs ITL smoothness — is the depth signal in a batching design round.
Disaggregated prefill/decode — the frontier architecture (used in production at large-scale inference providers): run prefill and decode on separate GPU pools, transferring the KV cache between them. Because the two phases have opposite bottlenecks, co-locating them means neither runs optimally. Separating lets you scale each independently and use different hardware. The cost is a KV cache transfer over the interconnect. This is the answer to "how would you improve on vLLM's architecture."
Sketch: an inference scheduler (the near-verbatim interview question)
"Design a batching system for a single GPU serving up to 100 synchronous requests, maximizing utilization under a latency SLA."
class InferenceScheduler:
"""Iteration-level scheduler with a KV-block budget.
The core constraint is NOT request count — it's KV cache blocks."""
def __init__(self, total_kv_blocks, block_size=16,
max_batch=256, max_prefill_tokens=2048):
self.free_blocks = total_kv_blocks
self.block_size = block_size
self.max_batch = max_batch
self.max_prefill_tokens = max_prefill_tokens # chunked prefill budget
self.running = [] # sequences currently decoding
self.waiting = deque() # admitted, not yet started
self.swapped = [] # preempted, KV offloaded to CPU
def blocks_needed(self, seq):
return math.ceil(seq.total_len / self.block_size)
def schedule_step(self):
"""Called before EVERY forward pass. This is what makes it continuous."""
batch, prefill_budget = [], self.max_prefill_tokens
# 1. Existing decodes get priority — they have users waiting on
# inter-token latency. Each needs at most 1 new block.
for seq in self.running:
if seq.needs_new_block():
if self.free_blocks == 0:
# Out of memory. Preempt — LIFO (newest first) minimizes
# wasted work, and preserves fairness for old requests.
self._preempt_newest()
self.free_blocks -= 1
batch.append(seq)
# 2. Admit waiting requests, chunk-prefilling within budget.
while self.waiting and len(batch) < self.max_batch and prefill_budget > 0:
seq = self.waiting[0]
need = self.blocks_needed(seq)
if need > self.free_blocks:
break # can't fit; wait rather than thrash
chunk = min(seq.remaining_prompt_tokens(), prefill_budget)
seq.prefill_chunk = chunk
prefill_budget -= chunk
self.free_blocks -= need
batch.append(self.waiting.popleft())
self.running.append(seq)
return batch
def _preempt_newest(self):
"""Two options: SWAP (copy KV to CPU, restore later — costs PCIe
bandwidth) or RECOMPUTE (drop KV, re-prefill on resume — costs
FLOPs). Recompute usually wins for short prompts; swap wins for
long ones. vLLM implements both."""
victim = self.running.pop()
self.free_blocks += self.blocks_needed(victim)
self.swapped.append(victim)
def on_step_complete(self, outputs):
for seq, token in outputs:
seq.append(token)
if seq.is_finished(): # EOS or max_tokens or stop string
self.free_blocks += self.blocks_needed(seq)
self.running.remove(seq)
seq.future.set_result(seq.output)
Follow-ups an interviewer will push on, with the answers:
- "How do you keep the latency SLA under load?" — admission control. Reject or queue at the gateway when the estimated queueing delay exceeds the SLA budget; it is better to fail fast with a 429 than to accept work you'll deliver late. Model the queue with Little's Law (§40.1).
- "How do you prioritize?" — multiple queues by tier with weighted fair scheduling; short generations can be favored (SJF-like) for mean latency, but you must add aging to prevent starvation of long requests.
- "What's your eviction policy under memory pressure?" — preempt newest-first (LIFO), because the newest request has the least sunk cost and the fewest user-visible tokens already delivered. Recompute for short prompts, swap for long ones.
- "Streaming?" — yes, SSE (§18.2). Which means a preempted sequence has already sent tokens to the user — you cannot abandon it. This constrains preemption policy, and noticing it unprompted is a real signal.
41.4 Attention and the kernels underneath
- FlashAttention (Dao et al.) — attention is memory-bound, not compute-bound, because the N×N attention matrix round-trips to HBM. FlashAttention tiles the computation, keeps blocks in SRAM, and never materializes the full matrix, using the online-softmax trick to combine tiles. Result: 2–4× faster, and memory linear in sequence length instead of quadratic. FlashAttention-2 and -3 improve work partitioning and exploit newer hardware (FP8, async copies on Hopper). The takeaway to state: the win came from managing the memory hierarchy, not from reducing FLOPs — same lesson as §40.1.
- Speculative decoding — a small "draft" model proposes k tokens; the large model verifies all k in a single forward pass (verification is parallel, like prefill). Accepted tokens are kept, the first rejection resets. Because decode is memory-bound, verifying 5 tokens costs almost the same as generating 1. Typical 2–3× speedups with provably identical output distribution (rejection sampling guarantees this — an important detail, since it means no quality tradeoff). Variants: Medusa (extra decoding heads instead of a draft model), EAGLE (feature-level drafting), n-gram/prompt lookup (draft by copying from the prompt — free and remarkably effective for summarization and code editing where output echoes input).
- Quantization — the practical ladder:
| Format | Memory vs fp16 | Quality | Notes |
|---|---|---|---|
| BF16/FP16 | 1× | baseline | Default |
| FP8 (E4M3) | 0.5× | ~lossless | Native on H100+; the current sweet spot |
| INT8 (SmoothQuant, LLM.int8) | 0.5× | near-lossless with outlier handling | Activation outliers are the whole problem |
| INT4 (GPTQ, AWQ) | 0.25× | small but real degradation | AWQ preserves salient weights by activation magnitude; usually beats GPTQ |
| GGUF k-quants | varies | good at 4–5 bit | The llama.cpp/CPU ecosystem |
Weight-only quantization helps decode most (decode is bandwidth-bound on weights). It helps prefill less (compute-bound). Say this — it shows you understand why rather than that.
- Parallelism — tensor parallelism splits each layer's matrices across GPUs (needs very high bandwidth: NVLink within a node; two all-reduces per layer). Pipeline parallelism splits layers across nodes (tolerates slower interconnect, introduces bubbles). Expert parallelism for MoE. Rule of thumb: TP within a node, PP across nodes, and never use PP if you can avoid it for latency-sensitive serving.
41.5 Serving stacks and what to pick
| Stack | Strengths | Choose when |
|---|---|---|
| vLLM | PagedAttention, continuous batching, huge model coverage, OpenAI-compatible API, active community | The default for self-hosting |
| SGLang | RadixAttention prefix caching, structured-output speed, strong for agents with repeated prefixes | Heavy prefix reuse, constrained decoding |
| TensorRT-LLM | Highest raw throughput on NVIDIA, in-flight batching, FP8 | You've committed to NVIDIA and need max perf |
| TGI (HuggingFace) | Solid production defaults, good observability | HF ecosystem shops |
| llama.cpp / Ollama | CPU/Metal, GGUF quantization | Edge, laptops, prototypes |
| Managed (Bedrock, Vertex, Azure OpenAI, Anthropic/OpenAI APIs) | Zero ops, frontier models, per-token pricing | Almost always the right first answer |
The lead-level position to state: self-hosting only wins when you have (a) sustained high volume — the crossover is typically in the hundreds of millions of tokens per month, (b) a data-residency or latency requirement an API can't meet, or (c) a fine-tuned model you own. Otherwise you're paying an SRE team to lose to an API's price curve. Saying this — rather than defaulting to "we'd host it" — reads as commercial judgment.
41.6 Cost engineering, with the actual math
Unit economics of a RAG endpoint:
System prompt 800 tokens (identical every request!)
Retrieved context 3,000 tokens
User question 50 tokens
Output 400 tokens
At $3/M input, $15/M output:
input = 3,850 × $3/1M = $0.01155
output = 400 × $15/1M = $0.006
total ≈ $0.0176 per request
At 2M requests/month → $35,100/month.
Now apply the levers, in order of impact:
1. PROMPT CACHING on the 800-token system prompt + stable context.
Cached reads typically ~10% of input price.
If 3,000 of the 3,850 input tokens are cacheable at 80% hit rate:
effective input ≈ 3,850 - (3,000 × 0.8 × 0.9) = 1,690 tokens
→ input cost drops ~56% → saves ~$13k/month.
THIS IS ALMOST ALWAYS THE BIGGEST AND CHEAPEST WIN.
2. MODEL ROUTING. Classify intent; send the ~60% of simple queries to a
model 10x cheaper. Escalate on low confidence or explicit complexity.
0.6 × cost/10 + 0.4 × cost = 0.46 × cost → another ~50%.
3. CONTEXT TRIMMING. Rerank to top-5 chunks instead of top-15.
3,000 → 1,200 context tokens. Frequently IMPROVES quality
(less "lost in the middle") while cutting cost. Free lunch.
4. SEMANTIC CACHING. Embed the query, serve cached answers above a
similarity threshold. Works for FAQ-shaped traffic (support, docs);
dangerous for personalized or time-sensitive answers — a wrong cache
hit is a correctness bug, not a performance one. Gate by intent class.
5. OUTPUT LENGTH DISCIPLINE. Output tokens cost 5x input. "Be concise"
in the prompt plus a hard max_tokens is real money.
6. BATCH API for anything not user-facing (evals, backfills, enrichment)
— typically ~50% off.
7. DISTILLATION. Fine-tune a small model on your production traffic's
large-model outputs. Highest effort, highest ceiling; only worth it
at sustained volume with a stable task.
Realistic stacked outcome: $35k → $8-12k/month with no quality loss.
Track cost as a first-class metric: cost per request, per feature, per tenant, per team. Emit tokens and dollars on every trace span. Alert on cost-per-request drift the way you alert on latency drift — a prompt change that adds 500 tokens is a 15% cost regression that no test will catch.
41.7 Observability for LLM systems
Standard APM misses the failure mode that matters: a confidently wrong answer returns HTTP 200 in 800 ms. Your dashboards are green while the product is broken.
What a span must carry:
# OpenTelemetry semantic conventions for GenAI (gen_ai.* namespace)
with tracer.start_as_current_span("llm.chat") as span:
span.set_attribute("gen_ai.system", "anthropic")
span.set_attribute("gen_ai.request.model", model) # PIN THE VERSION
span.set_attribute("gen_ai.request.temperature", temp)
span.set_attribute("gen_ai.request.max_tokens", max_tokens)
span.set_attribute("app.prompt.version", prompt_version) # git sha of the prompt
span.set_attribute("app.retrieval.doc_ids", ",".join(doc_ids))
span.set_attribute("app.retrieval.scores", str(scores[:5]))
resp = client.messages.create(...)
span.set_attribute("gen_ai.usage.input_tokens", resp.usage.input_tokens)
span.set_attribute("gen_ai.usage.output_tokens", resp.usage.output_tokens)
span.set_attribute("gen_ai.usage.cache_read_tokens", resp.usage.cache_read_input_tokens)
span.set_attribute("app.cost_usd", compute_cost(resp.usage, model))
span.set_attribute("gen_ai.response.finish_reason", resp.stop_reason)
span.set_attribute("app.ttft_ms", ttft_ms)
Metrics to alert on that a generic stack won't give you: cache hit rate (a drop means a prompt change broke prefix stability — silent 40% cost increase), finish_reason == "max_tokens" rate (truncation), retrieval score distribution shift (your index went stale), refusal rate, eval score on a continuously-sampled production slice, and cost per request p50/p99.
Further reading: Kwon et al., Efficient Memory Management for LLM Serving with PagedAttention (vLLM, SOSP 2023) — the single most useful systems paper in this space. Yu et al., Orca (OSDI 2022) for continuous batching. Dao et al., FlashAttention and FlashAttention-2. Leviathan et al., Fast Inference from Transformers via Speculative Decoding (2023). Pope et al., Efficiently Scaling Transformer Inference (Google, 2022) — the definitive treatment of parallelism layouts and the roofline analysis. Zheng et al., SGLang / RadixAttention. Ainslie et al., GQA (2023).
42. Architecture Patterns, Fully Worked
Every pattern here follows the same structure: precise definition → what it is commonly confused with → a code walkthrough of the before and after → the operational problems it creates → who runs it in production and why → when it's the wrong choice → follow-up questions with answers.
42.1 CQRS — Command Query Responsibility Segregation
The precise definition
CQRS is the separation of the model used to write data from the model used to read it. That is the entire idea. It descends from Bertrand Meyer's Command-Query Separation (CQS) — a method should either change state (command, returns void) or return data (query, no side effects), never both — and Greg Young extended it from methods to models.
What CQRS is NOT (get these wrong and you'll be corrected mid-interview):
- Not event sourcing. They're frequently deployed together and are completely independent. You can do CQRS with two SQL tables and no events. You can event-source without CQRS.
- Not inherently eventually consistent. Level 1–2 below are fully synchronous and transactional.
- Not microservices. It's a pattern inside a service boundary as often as across one.
- Not "read replicas." Read replicas give you the same model on more hardware. CQRS gives you a different model shaped for the query.
The adoption ladder — the framing that makes you sound experienced
Almost every CQRS conversation goes wrong because people jump to Level 4 and then blame CQRS. Present it as a ladder and place the team on it:
| Level | What's separated | Consistency | Cost | Typical fit |
|---|---|---|---|---|
| 0 | Nothing — one model, one ORM | Strong | None | Most CRUD apps. Stay here. |
| 1 | Commands and queries as separate code paths (handlers, DTOs) | Strong | Trivial | Any app with non-trivial business rules |
| 2 | Separate read and write models over the same database (write to normalized tables, read from denormalized views) | Strong | Low | Complex domains, reporting pressure |
| 3 | Separate read store, updated synchronously or via transactional outbox | Strong-ish (outbox lag) | Medium | Read/write ratios >100:1, different access shapes |
| 4 | Separate read store fed asynchronously by events, often with event sourcing | Eventual | High | Extreme read scale, audit requirements, multiple divergent read shapes |
Say this in an interview: "CQRS is a ladder, not a switch. Most teams need Level 1 or 2 and adopt Level 4, then blame the pattern for the eventual-consistency bugs they signed up for."
Level 1 — the version everyone should do
Before: the classic anemic service where one model serves both purposes.
// BEFORE — one model, one repository, one entity leaking everywhere.
@Service
public class OrderService {
private final OrderRepository repo;
// Returns the entity — which is also the persistence model AND the
// API model. Change the DB schema, break the API.
public Order placeOrder(OrderDto dto) {
Order o = new Order();
o.setCustomerId(dto.customerId());
o.setItems(dto.items());
o.setStatus("PENDING");
return repo.save(o); // command that also returns state
}
public List<Order> findOrders(Long customerId) {
// N+1 waiting to happen; loads full aggregates to render a list
return repo.findByCustomerId(customerId);
}
}
After: commands and queries separated. Commands go through the domain model and enforce invariants; queries bypass it entirely and project straight to a DTO.
// ---------- WRITE SIDE: goes through the domain, enforces invariants ----------
public record PlaceOrderCommand(UUID customerId, List<OrderLine> lines, UUID idempotencyKey) {}
@Component
public class PlaceOrderHandler {
private final OrderRepository repo; // aggregate repository
private final InventoryPort inventory; // port, not a concrete client
private final IdempotencyStore idem;
@Transactional
public UUID handle(PlaceOrderCommand cmd) {
// Idempotency FIRST — commands arrive twice; this is not optional
// in any distributed system. Return the prior result on replay.
var prior = idem.find(cmd.idempotencyKey());
if (prior != null) return prior.orderId();
// The domain model enforces the rules. This is the ONLY place
// an Order can become invalid, which is the point of the write model.
Order order = Order.place(cmd.customerId(), cmd.lines(), inventory::reserve);
repo.save(order);
idem.record(cmd.idempotencyKey(), order.id());
return order.id(); // return the ID only — not the entity
}
}
// ---------- READ SIDE: no domain model, no ORM, straight to a DTO ----------
public record OrderSummary(UUID id, String status, BigDecimal total,
int itemCount, Instant placedAt, String customerName) {}
@Component
public class OrderQueryService {
private final JdbcTemplate jdbc; // deliberately NOT the ORM
public List<OrderSummary> forCustomer(UUID customerId, int limit, int offset) {
// One query, exactly the columns the screen needs, joined and
// aggregated in the database. No lazy loading, no N+1, no
// hydrating aggregates you'll throw away.
return jdbc.query("""
SELECT o.id, o.status, o.total_amount, o.item_count,
o.placed_at, c.display_name
FROM order_summary o
JOIN customer c ON c.id = o.customer_id
WHERE o.customer_id = ?
ORDER BY o.placed_at DESC
LIMIT ? OFFSET ?
""", ORDER_SUMMARY_MAPPER, customerId, limit, offset);
}
}
What you actually gained at Level 1, and should be able to articulate:
- The read path stops paying for the write path's abstractions (aggregate loading, dirty checking, lazy proxies). This is often a 10× latency improvement on list endpoints with zero infrastructure change.
- The write model is free to be rich (invariants, value objects, encapsulated collections) without making every read slow.
- The API contract decouples from the persistence schema.
- You can now optimize the two sides independently — which is what makes Levels 2–4 possible later.
Level 3–4 — separate read store, with the projection
// The write side emits a domain event inside the same transaction
// as the state change (via the outbox — §42.4, this is mandatory).
public record OrderPlaced(UUID orderId, UUID customerId,
List<OrderLine> lines, BigDecimal total,
Instant occurredAt, long version) {}
// The projector builds a read model shaped for exactly one screen.
@Component
public class OrderSummaryProjector {
private final ElasticsearchClient es;
private final CustomerLookup customers;
@KafkaListener(topics = "order-events", groupId = "order-summary-projector")
public void on(OrderPlaced e) {
// DENORMALIZE. The read model duplicates customer name so the
// query needs no join. This is the trade: storage and staleness
// in exchange for read latency.
var doc = Map.of(
"orderId", e.orderId().toString(),
"customerId", e.customerId().toString(),
"customerName", customers.displayName(e.customerId()),
"status", "PENDING",
"total", e.total(),
"itemCount", e.lines().size(),
"placedAt", e.occurredAt().toString(),
"version", e.version()
);
// IDEMPOTENT WRITE. Projectors WILL see duplicates (at-least-once
// delivery, consumer rebalance, replay). Two defenses:
// 1. Use the aggregate ID as the document ID (upsert, not insert)
// 2. Guard with the version — reject out-of-order/stale events
es.update(u -> u
.index("order-summary")
.id(e.orderId().toString())
.doc(doc)
.docAsUpsert(true)
.ifSeqNo(...) // or a scripted version check
);
}
}
Projection rules that separate people who've run this from people who've read about it:
- Projectors must be idempotent. Always. Use upserts keyed by aggregate ID, and carry a monotonic version to discard stale events.
- Projections must be rebuildable from scratch. If you can't drop the read store and replay, you don't have a projection — you have a second source of truth that will silently diverge. Rebuild capability is the whole safety net.
- Track projection lag as a first-class SLI.
max(event_timestamp_in_source) − max(event_timestamp_projected). Alert on it. This number is your consistency window, and product and support need to know it. - One projector per read model, independently versioned. Don't build a god-projector. When a screen changes, you rebuild one projection, not all of them.
- Version the projection, not just the code. Blue/green projections: build
order-summary-v2alongside v1, backfill, verify with a diff, switch reads, drop v1. Same expand-contract discipline as a schema migration (§39.5).
The eventual consistency problem — and the five real fixes
This is the question interviewers use to find out whether you've actually shipped CQRS: "The user places an order, gets redirected to their order list, and the order isn't there. What do you do?"
Weak answer: "add a spinner" or "it's eventually consistent, that's the tradeoff." Strong answer names the options and picks:
- Return the result from the command. The write side already knows the order ID and status; render the confirmation from the command's response rather than re-querying. Solves ~70% of real cases and costs nothing. Try this first.
- Read-your-writes via a version token. The command returns a version/LSN; the client sends it with subsequent reads; the query layer either waits for the projection to reach that version or falls back to the write store. This is exactly how you'd handle replica lag in a database, and it generalizes.
- Synchronous projection for the originating user only. Update the read model in the same transaction (or immediately after) for this one aggregate, asynchronously for everyone else. Hybrid, pragmatic, common in production.
- Client-side optimistic insert. The UI inserts the expected row locally, reconciles when the real projection arrives. Standard in modern frontends (TanStack Query optimistic updates, §22.2) and invisible to users.
- Design the UX around it. "Your order is being processed" is honest and often better product design than a fake-synchronous illusion — payments and shipping are genuinely asynchronous.
The senior framing: "Eventual consistency isn't a bug I hide, it's a property I expose deliberately where it's true and hide where it isn't. The originating user gets read-your-writes; other users get the async path."
Where CQRS-shaped architecture runs in production
- Meta / TAO — the canonical read/write split at planet scale. Writes go to MySQL (the durable, normalized source of truth); reads are served by TAO, a distributed write-through cache with a graph-shaped API. Reads outnumber writes by orders of magnitude and are served by an entirely different system with a different data model. That is CQRS at Level 4 whether or not they call it that.
- LinkedIn / Venice — a derived-data serving platform, purpose-built for exactly this pattern: batch and streaming jobs compute derived datasets (recommendations, people-you-may-know, feature data), which are pushed into a read-optimized store serving online traffic. The write path (Samza/Spark jobs) and the read path (Venice) are entirely separate systems. Espresso is the source-of-truth OLTP store; Venice serves the derived reads.
- Netflix — the homepage is precomputed. Personalized row assembly happens offline/near-line and lands in read-optimized stores (EVCache, Cassandra) so the request path is a lookup, not a computation. This is the "materialize the read model" instinct applied to recommendations.
- Uber — Schemaless as the write-side store with derived indexes and read models built from the changelog; Cadence/Temporal orchestrating the write-side workflows.
- Financial ledgers generally — the write model is an append-only double-entry journal; balances are a projection. Nobody computes an account balance by summing all history on every read, and nobody stores a balance as the source of truth. That split is CQRS, and it's the most defensible use case there is.
- Elasticsearch alongside a relational database — the single most common real-world CQRS deployment, and most teams doing it don't call it that. Postgres is the write model; an OpenSearch index is a projection. If you've done this (and you have), you have shipped CQRS — say so in the interview using this vocabulary. That reframing turns routine experience into pattern fluency.
When CQRS is the wrong answer
- Simple CRUD with symmetric read/write shapes — you've added a second store and a consistency window to solve nothing.
- Teams without the operational maturity to run a projection pipeline (monitoring lag, rebuilding, versioning). The pattern's cost is operational, not conceptual.
- When the real problem is a missing index or an N+1. Fix that first; a startling share of "we need CQRS" is "we need
EXPLAIN ANALYZE." - Strong consistency requirements on the read path with no tolerance for staleness — unless you stay at Level 2.
Follow-up questions and answers
- "How do you handle a projection bug that corrupted the read model?" → Fix the projector, delete the read store, replay from the event log into a new versioned index, diff against the old, switch reads. This is why rebuildability is non-negotiable — it turns a data-corruption incident into a rebuild job.
- "What if replaying the whole log takes 8 hours?" → Snapshot the projection periodically and replay from the snapshot; parallelize by partition key; and keep the projection's throughput at least 10× the live event rate so catch-up is feasible. Measure your rebuild time and treat it like an RTO (§32) — it is one.
- "How do you handle events arriving out of order?" → Order is guaranteed per Kafka partition, so partition by aggregate ID. Across aggregates, don't assume order. Carry a per-aggregate version and drop stale updates.
- "Two read models disagree. Which is right?" → Neither — the event log is. Read models are caches with extra steps. That mental model prevents a whole class of bugs.
- "Does CQRS require a message broker?" → No. Level 2 needs nothing. Level 3 can use a transactional outbox polled by the same service. Brokers become necessary when multiple independent consumers need the stream.
Further reading: Greg Young's CQRS Documents and his talks (the origin, and notably he has publicly warned against the pattern's overuse). Fowler's bliki entries on CQRS and CQS. Vernon, Implementing Domain-Driven Design, ch. 4 and 8. Meta's TAO paper (TAO: Facebook's Distributed Data Store for the Social Graph, USENIX ATC 2013). LinkedIn's engineering blog on Venice.
42.2 Event Sourcing — the full treatment
Definition
Store the sequence of state-changing events as the source of truth, rather than the current state. Current state is derived by replaying events. Account balance = fold(events), not a column.
The trade in one line: you gain a perfect audit log, time travel, and the ability to build any read model retroactively; you pay with schema-evolution pain, GDPR complexity, and a much higher operational bar.
The write model
# ---------- Events: immutable facts, past tense, carrying everything
# needed to understand them WITHOUT external lookups ----------
@dataclass(frozen=True)
class MoneyDeposited:
account_id: str
amount_cents: int # integers for money; never floats
currency: str
occurred_at: datetime
correlation_id: str # ties this to the request that caused it
@dataclass(frozen=True)
class MoneyWithdrawn:
account_id: str
amount_cents: int
currency: str
occurred_at: datetime
correlation_id: str
# ---------- The aggregate: rebuilt from events, enforces invariants ----------
class Account:
def __init__(self, account_id):
self.id = account_id
self.balance_cents = 0
self.version = 0 # for optimistic concurrency
self._pending = [] # events not yet persisted
@classmethod
def rehydrate(cls, account_id, events):
acct = cls(account_id)
for e in events:
acct._apply(e) # apply mutates state, no validation
acct.version += 1
return acct
def _apply(self, event):
"""Pure state transition. MUST NOT validate — these events already
happened. Validation lives in the command methods below."""
match event:
case MoneyDeposited(amount_cents=a): self.balance_cents += a
case MoneyWithdrawn(amount_cents=a): self.balance_cents -= a
# ---- Commands: validate, then emit ----
def withdraw(self, amount_cents, correlation_id):
if amount_cents <= 0:
raise ValueError("amount must be positive")
if amount_cents > self.balance_cents:
# THE invariant. It can only be checked here, on the write model,
# against a consistent view of this aggregate. This is why
# aggregates are consistency boundaries (§29.1).
raise InsufficientFunds(self.balance_cents, amount_cents)
e = MoneyWithdrawn(self.id, amount_cents, "CAD",
datetime.now(timezone.utc), correlation_id)
self._apply(e)
self._pending.append(e)
return e
The event store, with optimistic concurrency
-- The append-only log. Note the unique constraint: it is the entire
-- concurrency control mechanism.
CREATE TABLE events (
global_position BIGSERIAL PRIMARY KEY, -- total order for projections
stream_id TEXT NOT NULL, -- e.g. 'account-1234'
version INT NOT NULL, -- position WITHIN the stream
event_type TEXT NOT NULL,
event_data JSONB NOT NULL,
metadata JSONB NOT NULL, -- correlation/causation ids, actor
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (stream_id, version) -- ← optimistic concurrency
);
CREATE INDEX ON events (stream_id, version);
CREATE INDEX ON events (global_position);
def append(conn, stream_id, expected_version, events):
"""Optimistic concurrency: if another writer appended since we read,
the UNIQUE(stream_id, version) constraint fires and we retry the
whole command against fresh state. No locks, no distributed
transaction — just a constraint."""
try:
with conn.transaction():
for i, e in enumerate(events, start=1):
conn.execute(
"INSERT INTO events (stream_id, version, event_type,"
" event_data, metadata) VALUES (%s,%s,%s,%s,%s)",
(stream_id, expected_version + i, type(e).__name__,
json.dumps(asdict(e)), json.dumps(current_metadata()))
)
except UniqueViolation:
raise ConcurrencyConflict(stream_id, expected_version)
# Caller re-reads the stream, re-runs the command, retries.
Snapshots — when replay gets expensive
def load(store, account_id, snapshot_every=100):
snap = store.latest_snapshot(account_id)
if snap:
acct = Account.from_snapshot(snap) # state at version N
events = store.read(account_id, from_version=snap.version + 1)
else:
acct, events = Account(account_id), store.read(account_id, 0)
for e in events:
acct._apply(e); acct.version += 1
return acct
The rule: snapshots are a cache, never a source of truth. You must be able to delete every snapshot and rebuild. If a snapshot format change requires a migration, you've made them load-bearing — that's a design smell.
The four hard problems (name these unprompted; they're what separates experience from enthusiasm)
1. Schema evolution / upcasting. Events are immutable and live forever. A 2019 event must still be readable by 2026 code.
# Upcasters transform old event versions into the current shape at read time.
# Chain them: v1 -> v2 -> v3. Never rewrite history in the store.
def upcast_money_withdrawn(raw, version):
if version == 1:
raw["currency"] = "CAD" # v1 had no currency field
raw["amount_cents"] = int(raw.pop("amount") * 100) # v1 used floats
version = 2
if version == 2:
raw["correlation_id"] = raw.get("request_id", "unknown")
version = 3
return raw
Rules: only ever add optional fields; never change a field's meaning; never delete a field other code might read; version the event type explicitly (MoneyWithdrawn.v3) if the change is structural.
2. GDPR / right to erasure vs an immutable log. You cannot delete an event from an append-only log without destroying the model. The accepted solution is crypto-shredding: encrypt personal data in events with a per-subject key, store keys separately, and delete the key on an erasure request. The events remain, the PII becomes unrecoverable ciphertext. This is a genuinely impressive answer to give.
3. Kafka is not an event store (a real interview trap). Kafka is an excellent event log for transport, but as an event store it lacks: efficient per-aggregate reads (you can't cheaply load "all events for account-1234" out of a partition with millions of interleaved streams), a way to enforce optimistic concurrency on a stream, and unbounded retention semantics per key without compaction losing history. Teams that try get an external index bolted on and reinvent an event store badly. Use a real event store (EventStoreDB, Marten on Postgres, DynamoDB with stream_id as the partition key and version as the sort key) and publish to Kafka for consumers.
4. It's very hard to undo. Event sourcing is close to irreversible architecturally — the entire system's history is in a format only your code understands. Adopt it for a bounded context with a strong reason, not organization-wide.
Where event sourcing genuinely runs
- Financial ledgers and payments — the natural fit and the strongest case. Double-entry bookkeeping is event sourcing, invented in the 15th century. Balances are projections.
- Nubank — built on Datomic, an immutable database where the fact log is the source of truth; the entire architecture is event-sourcing-shaped and they've spoken publicly about immutability as a core bet.
- Insurance and healthcare claims — regulatory requirements make "why does this record look like this" a first-class query.
- Order/fulfillment systems — an order's lifecycle is inherently a sequence of events (placed, paid, picked, shipped, delivered, returned) and modeling it as state transitions loses information the business wants.
- Git is an event-sourced content-addressed store; your working directory is a projection. Useful analogy in interviews.
- Where it's rejected: most CRUD systems, and most "we'll add event sourcing for the audit log" cases — an audit table is 1% of the cost and solves the actual requirement.
Follow-ups
- "How do you query 'all accounts with balance > $1000'?" → You don't, from the event store. That's a projection. This question is really testing whether you understand that event sourcing without CQRS is unusable.
- "How big can a stream get?" → Bounded aggregates are a design requirement. A stream with 10M events is a modeling failure — the aggregate is too coarse. Split it, or close and open new streams periodically (e.g., per accounting period).
- "Concurrency across aggregates?" → Sagas (§42.3). You cannot have a transaction across aggregates; that's the definition of the boundary.
Further reading: Greg Young's Event Sourcing talks. Vernon, Implementing DDD. Vaughn Vernon and Kleppmann both on the log-as-source-of-truth. Kleppmann's Turning the Database Inside-Out talk — the clearest articulation of why logs and materialized views generalize database internals to the application layer.
42.3 Sagas — transactions across aggregates
The problem
You cannot hold an ACID transaction across services or aggregates. A saga is a sequence of local transactions where each has a compensating action, executed in reverse on failure. The guarantee you get is not atomicity — it's eventual consistency with explicit compensation, sometimes called "semantic atomicity."
Choreography vs orchestration
CHOREOGRAPHY — services react to each other's events. No central brain.
OrderService --OrderPlaced--> [bus]
├──> PaymentService --PaymentTaken-->
├──> InventoryService --StockReserved-->
└──> ShippingService
+ No single point of failure, low coupling to a coordinator
+ Easy to add a new participant (just subscribe)
- The workflow exists NOWHERE as a readable artifact. To understand
the business process you must trace event subscriptions across
six repositories. This is the killer at 5+ steps.
- Cyclic dependencies emerge silently
- Debugging requires distributed tracing as a hard prerequisite
ORCHESTRATION — a coordinator owns the workflow explicitly.
OrderSaga
├─1─> PaymentService.charge() ↩ refund()
├─2─> InventoryService.reserve() ↩ release()
├─3─> ShippingService.schedule() ↩ cancel()
└─4─> NotificationService.confirm() ↩ (none needed)
+ The process is one readable file. New engineers can understand it.
+ Compensation logic is centralized and testable
+ State is queryable ("where is order 123?")
- The orchestrator is a component to run and scale
- Risk of a smart-orchestrator/dumb-services anemic design
The position to take: choreography for 2–3 steps with genuinely independent services; orchestration past that, because a business process that nobody can read is an operational liability. Most production sagas of consequence are orchestrated.
Orchestrated saga with compensation
class OrderSaga:
"""Each step records its compensation BEFORE executing, so a crash
between execution and recording doesn't orphan the compensation."""
STEPS = [
("charge_payment", "refund_payment"),
("reserve_stock", "release_stock"),
("schedule_shipping","cancel_shipping"),
]
async def run(self, saga_id, order):
completed = []
try:
for action, compensation in self.STEPS:
# Persist intent before acting — crash recovery reads this
await self.state.record_step_started(saga_id, action, compensation)
result = await getattr(self, action)(saga_id, order)
await self.state.record_step_completed(saga_id, action, result)
completed.append((compensation, result))
await self.state.mark_complete(saga_id)
except Exception as e:
await self.state.mark_compensating(saga_id, str(e))
# Compensate in REVERSE order. Each compensation must be
# idempotent and must itself be retried until it succeeds —
# a failed compensation is an operational incident, not a
# code path you can swallow.
for compensation, result in reversed(completed):
await self._compensate_with_retry(saga_id, compensation, result)
await self.state.mark_failed(saga_id)
raise
async def charge_payment(self, saga_id, order):
# Idempotency key derived from saga + step: a retry after a
# timeout must not double-charge. This is THE most important
# detail in the whole pattern.
return await self.payments.charge(
amount=order.total,
idempotency_key=f"{saga_id}:charge_payment"
)
The four rules of sagas (recite these):
- Every step is idempotent, keyed on
saga_id:step. Retries are guaranteed, not hypothetical. - Every compensation is idempotent and retried until success. A compensation that fails permanently is a page, not an exception.
- Some things cannot be compensated — an email sent, a physical shipment dispatched. Order the saga so irreversible steps come last, after everything reversible has succeeded. This is called the "pivot transaction," and naming it is a strong signal.
- Compensation is not rollback. A refund is a new transaction that appears in the customer's statement. The business must accept the semantics.
Production systems
-
Uber / Cadence → Temporal. Uber built Cadence for exactly this problem; Temporal is its successor and is now the industry-standard durable execution engine. The model: write the workflow as ordinary sequential code, and the engine persists every step's result so that on crash it replays deterministically and resumes at the exact point of failure. It removes almost all of the boilerplate above.
# Temporal: the saga is just... code. Durability is the runtime's job. @workflow.defn class OrderWorkflow: @workflow.run async def run(self, order: Order) -> str: compensations = [] try: await workflow.execute_activity( charge_payment, order, start_to_close_timeout=timedelta(seconds=30), retry_policy=RetryPolicy(maximum_attempts=5)) compensations.append(refund_payment) await workflow.execute_activity(reserve_stock, order, ...) compensations.append(release_stock) # Durable timer — survives process restarts, no cron needed await workflow.sleep(timedelta(hours=1)) await workflow.execute_activity(schedule_shipping, order, ...) return "completed" except Exception: for c in reversed(compensations): await workflow.execute_activity(c, order, ...) raiseMentioning Temporal and the durable-execution model is a strong modern signal — it reframes "how do you build a saga" as "why would you hand-roll one."
-
AWS Step Functions — the managed state-machine equivalent; standard vs express workflows, built-in retry/catch, and visual execution history. The right answer inside AWS-native shops.
-
Netflix Conductor — Netflix's orchestration engine for their media pipeline, open-sourced.
-
Camunda / Zeebe — BPMN-based, common in enterprise and finance.
-
Airbnb — has written about their orchestration for booking flows; the general industry direction is toward durable execution engines rather than hand-rolled state machines.
Follow-ups
- "How do you test a saga?" → Unit test each step and compensation; integration test the happy path; then explicitly test failure injection at every step boundary, including "failure after the remote side succeeded but before we recorded it." That last case is the one that produces double-charges in production.
- "What about a saga that gets stuck?" → Timeouts per step, a dead-letter state, and a queryable saga store so support can see stuck instances. Every long-running saga needs an operational UI or a query interface; that's a requirement, not a nicety.
- "Isn't two-phase commit simpler?" → 2PC gives real atomicity but requires all participants to support it, holds locks across the network (killing throughput), and blocks indefinitely if the coordinator dies mid-commit. It's viable within a single database or with XA in a controlled environment; it does not survive the public internet or heterogeneous services.
42.4 The Transactional Outbox — the pattern that makes all of the above work
The dual-write problem: you must update the database and publish an event. Two systems, no shared transaction. Whatever order you choose, a crash between them leaves you inconsistent — a database row with no event (silently lost downstream work), or an event with no row (downstream acts on a fiction).
The fix: write the event to an outbox table in the same local transaction as the state change, then relay it asynchronously.
CREATE TABLE outbox (
id BIGSERIAL PRIMARY KEY,
aggregate_type TEXT NOT NULL,
aggregate_id TEXT NOT NULL, -- becomes the Kafka partition key
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
headers JSONB NOT NULL, -- trace context, correlation id
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ -- NULL = unpublished (polling variant)
);
CREATE INDEX ON outbox (published_at) WHERE published_at IS NULL;
@Transactional // ONE transaction. This is the entire point.
public UUID placeOrder(PlaceOrderCommand cmd) {
Order order = Order.place(cmd);
orderRepo.save(order); // business state
outboxRepo.save(new OutboxRecord( // event, same txn
"Order", order.id().toString(), "OrderPlaced",
json(new OrderPlaced(order)), traceHeaders()));
return order.id();
} // Either both commit or neither does. Atomicity restored.
Two relay strategies:
| Polling publisher | CDC / log tailing (Debezium) | |
|---|---|---|
| How | A worker selects unpublished rows, publishes, marks published | Read the database WAL/binlog directly |
| Latency | Poll interval (100 ms–1 s) | ~milliseconds |
| DB load | Extra queries + updates | Near zero on the primary |
| Ops cost | Trivial — it's just code | Kafka Connect cluster, connector config, schema handling |
| Table bloat | Needs a cleanup job | Debezium's outbox router can transform and you delete immediately |
| Choose when | Small/medium scale, no Kafka Connect | High volume, already running Connect |
// Debezium outbox event router — extracts the outbox row into a proper
// event on a topic per aggregate type, keyed by aggregate id.
{
"name": "orders-outbox-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"plugin.name": "pgoutput",
"table.include.list": "public.outbox",
"transforms": "outbox",
"transforms.outbox.type":
"io.debezium.transforms.outbox.EventRouter",
"transforms.outbox.route.by.field": "aggregate_type",
"transforms.outbox.route.topic.replacement": "${routedByValue}.events",
"transforms.outbox.table.field.event.key": "aggregate_id",
"transforms.outbox.table.field.event.payload": "payload",
"tombstones.on.delete": "false"
}
}
Guarantees and the honest caveat: the outbox gives at-least-once delivery, never exactly-once. The relay can publish and crash before marking the row. Therefore every consumer must be idempotent — which is why §42.1's projector rules and §42.3's idempotency keys exist. Saying "the outbox gives at-least-once, so consumers must be idempotent" in one breath is exactly the level of precision interviewers are listening for.
The inbox pattern is the mirror image on the consumer side: record processed message IDs in a table, in the same transaction as the side effect, and skip duplicates. Together, outbox + inbox give you effectively-once processing without distributed transactions.
Production use: the outbox is near-universal in event-driven systems that survived contact with reality — it's the default in Debezium's documentation, in Chris Richardson's microservices patterns, and in essentially every serious CDC pipeline. Shopify, Stripe, and most payments infrastructure use some variant, because losing an event in a payments flow is a business incident.
42.5 Repository & Hexagonal, concretely
// ---------- DOMAIN LAYER: no framework imports, no annotations, no SQL ----------
// The port. Defined BY the domain, FOR the domain, in domain language.
public interface OrderRepository {
Optional<Order> findById(OrderId id);
void save(Order order);
List<Order> findUnfulfilledOlderThan(Duration age); // domain language
}
public class FulfillmentService { // pure policy, trivially testable
private final OrderRepository orders;
private final ShippingPort shipping; // another port
public void fulfillStaleOrders() {
for (Order o : orders.findUnfulfilledOlderThan(Duration.ofHours(24))) {
o.markEscalated(); // invariants live in the aggregate
orders.save(o);
shipping.expedite(o.id());
}
}
}
// ---------- ADAPTER LAYER: all the infrastructure ugliness lives here ----------
@Component
class JpaOrderRepository implements OrderRepository {
private final SpringDataOrderJpa jpa;
private final OrderMapper mapper; // entity <-> aggregate translation
public Optional<Order> findById(OrderId id) {
return jpa.findById(id.value()).map(mapper::toDomain);
}
public void save(Order order) { jpa.save(mapper.toEntity(order)); }
public List<Order> findUnfulfilledOlderThan(Duration age) {
return jpa.findByStatusAndCreatedBefore("UNFULFILLED",
Instant.now().minus(age)).stream().map(mapper::toDomain).toList();
}
}
Why this is worth the mapping cost — and when it isn't. You gain: the domain is testable with an in-memory OrderRepository and zero infrastructure; the persistence model can change (JPA → jOOQ → a document store) without touching business rules; and the domain model is free to differ from the table structure. You pay: a mapper per aggregate, and real boilerplate.
The honest verdict to give: for a CRUD service, this is ceremony — use Spring Data repositories directly and move on. For a domain with genuine invariants, or one you expect to outlive its current database, the boundary pays for itself within a year. A generic Repository<T> interface that just proxies your ORM is the anti-pattern — it adds a layer and buys nothing, because you haven't actually decoupled from the ORM's semantics (lazy loading, identity map, transaction scoping all leak through).
Follow-up: "Where do transactions belong?" → Not in the repository (too fine-grained) and not in the domain (that's infrastructure). They belong at the application service / command handler boundary — one command, one transaction. That's the Unit of Work boundary, and stating it crisply resolves a question a lot of candidates fumble.
43. Worked System Design Answers
Every design question posed anywhere in this document gets a full answer here: the requirements dialogue, the capacity math, the design, the deep dive, the failure modes, the alternatives I rejected and why, and the follow-ups an interviewer will actually push on. Read these as transcripts of a strong answer, not as summaries.
43.1 Design a Distributed Rate Limiter
(Referenced in §5.2 #1. This is the most common warm-up design question in existence, and most candidates give a shallow answer.)
Step 1 — Clarify (2 minutes, out loud)
"Before I design, five questions. What are we limiting on — user, API key, IP, or tenant? What's the scale — requests/sec and number of distinct keys? Is this a hard limit or a soft one — do we need exact enforcement, or is 5% overshoot acceptable? Where does it sit — edge, API gateway, or in-process? And what's the failure policy — if the limiter is down, do we fail open or closed?"
Assume the answers: per-API-key, 1M keys, 100k RPS aggregate, soft limit (small overshoot fine), at the API gateway, fail open (availability over enforcement — a rate limiter that takes down the API is worse than the abuse it prevents).
That last question is the one candidates skip, and it's the one that reveals operational judgment. Say the fail-open decision explicitly and justify it.
Step 2 — The algorithms, with actual tradeoffs
| Algorithm | Memory/key | Burst behavior | Boundary accuracy | Verdict |
|---|---|---|---|---|
| Fixed window counter | 1 int | Allows 2× burst at window edges | Poor | Simple but the edge burst is real: 100/min limit permits 200 requests in the 2 seconds spanning a boundary |
| Sliding window log | O(N) timestamps | Exact | Perfect | Correct but memory-prohibitive at scale — 1M keys × 100 timestamps = unusable |
| Sliding window counter | 2 ints | Smooth | ~Excellent | The pragmatic winner. Weighted average of current and previous window |
| Token bucket | 2 values (tokens, last_refill) | Allows controlled bursts | Perfect | Best when bursts are desirable — the standard for API quotas |
| Leaky bucket (queue) | queue | Smooths, no bursts | Perfect | Use when downstream needs constant rate (e.g., a legacy system) |
| GCRA | 1 timestamp | Precise, burst-tolerant | Perfect | Elegant — one value per key, token-bucket semantics. Redis's redis-cell implements it |
Sliding window counter math (the one to show on the board):
limit = 100/min. Now = 12:01:30 (30% into the current window).
previous window count = 84
current window count = 36
estimate = current + previous × (1 − elapsed_fraction)
= 36 + 84 × 0.5
= 78 → under 100, allow.
Error bound is provably small and it costs two integers per key.
Cloudflare published an analysis of this approach across billions of
requests showing well under 1% error versus an exact sliding log.
Step 3 — The implementation (Redis + Lua, atomic)
-- token_bucket.lua — atomic check-and-consume. Lua in Redis is the
-- correct primitive here: single-threaded execution means no race
-- between read and write, and one round trip instead of three.
-- KEYS[1] = bucket key
-- ARGV[1] = capacity, ARGV[2] = refill_rate/sec, ARGV[3] = now_ms, ARGV[4] = cost
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(bucket[1])
local last = tonumber(bucket[2])
if tokens == nil then -- first request for this key
tokens = capacity
last = now
end
-- Lazy refill: no background job, no cron. Compute what WOULD have
-- accrued since the last request. This is the key trick — it makes
-- the whole thing O(1) memory with no sweeper process.
local elapsed = math.max(0, now - last) / 1000.0
tokens = math.min(capacity, tokens + elapsed * refill_rate)
local allowed = 0
if tokens >= cost then
tokens = tokens - cost
allowed = 1
end
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'ts', now)
-- TTL so idle keys evict themselves: time to refill a full bucket + slack.
redis.call('EXPIRE', KEYS[1], math.ceil(capacity / refill_rate) + 60)
-- Return retry-after so the client can back off intelligently
local retry_after = 0
if allowed == 0 then
retry_after = math.ceil((cost - tokens) / refill_rate)
end
return {allowed, math.floor(tokens), retry_after}
class RateLimiter:
def __init__(self, redis, capacity=100, refill_rate=10):
self.script = redis.register_script(TOKEN_BUCKET_LUA)
self.capacity, self.refill_rate = capacity, refill_rate
self.breaker = CircuitBreaker(failure_threshold=5, reset_timeout=30)
def allow(self, key, cost=1):
try:
with self.breaker:
allowed, remaining, retry_after = self.script(
keys=[f"rl:{key}"],
args=[self.capacity, self.refill_rate,
int(time.time() * 1000), cost])
return Decision(bool(allowed), remaining, retry_after)
except (RedisError, CircuitOpen):
# FAIL OPEN. Decided in step 1, implemented here, and I say
# so out loud: "I'd rather serve abuse than serve an outage."
metrics.increment("ratelimiter.fail_open")
return Decision(True, -1, 0)
Always return the standard headers — this is a detail that marks API maturity:
RateLimit-Limit: 100
RateLimit-Remaining: 22
RateLimit-Reset: 17
Retry-After: 17 # on 429 responses
Step 4 — Scaling to 100k RPS
At 100k RPS, one Redis round trip per request is ~100k ops/sec — a single Redis node handles this, but it's now a single point of failure and a latency tax (0.5 ms in-DC RTT on every request).
The production architecture is a two-tier hybrid:
┌──────────────────────────────┐
request ────────►│ Gateway instance (local tier)│
│ • in-memory token bucket │ ← 0 network hops
│ • holds a LEASE of N tokens │ for most requests
└──────────┬───────────────────┘
│ async, batched: "I used 50, give me 50 more"
┌──────────▼───────────────────┐
│ Redis cluster (global tier) │
│ • authoritative counters │
│ • sharded by key hash │
└──────────────────────────────┘
How the lease works: each gateway instance requests a batch of tokens (say 10% of the limit) from Redis, serves requests from that local allowance with zero network calls, and refreshes asynchronously when it's ~70% consumed. Redis load drops by the batch factor — 100k RPS becomes ~1k RPS of Redis traffic.
The tradeoff to name: with M gateway instances each holding a lease, worst-case overshoot is bounded by the outstanding leases. That's why step 1's "is a soft limit acceptable" question mattered — this entire architecture depends on that answer. If the answer had been "hard limit, financial consequences," you must do a synchronous check per request and accept the latency and the availability coupling.
Sharding: shard Redis by hash(api_key) so all operations for one key land on one node — cross-slot Lua doesn't work in Redis Cluster (§18.5). Hot keys (one abusive tenant) get handled by the local tier absorbing them.
Step 5 — Failure modes and operations
- Redis down → fail open with a circuit breaker; local buckets keep enforcing approximate limits from their last lease. Alert loudly.
- Clock skew between gateways → token bucket uses elapsed time from the server's clock in the Lua script, so skew between gateways doesn't corrupt shared state. Pass
nowfrom Redis'sTIMEcommand if you want to eliminate it entirely. - Thundering herd on reset → fixed windows cause synchronized retries at the boundary. Token bucket avoids it naturally; also add jitter to
Retry-After. - Hot key → one key at 50k RPS saturates a shard. Mitigate with the local tier plus, for extreme cases, key splitting (
key:0..9, each with 1/10 the limit). - Multi-region → do not try to share global counters across regions; the cross-region RTT (§40.1) destroys the latency budget. Give each region a proportional share of the limit and accept the imprecision, or accept per-region limits as the product semantics.
Alternatives I'd mention and reject
- API Gateway built-ins (AWS API Gateway usage plans, Kong, Envoy's global rate limit service) — "For most teams this is the right answer and I'd start here rather than build." Envoy's
ratelimitservice is exactly this design, open source and battle-tested. Say this. Reaching for build when buy exists is a junior instinct; the interview reward is for knowing when to build (custom quota semantics, multi-dimensional limits, tight cost control). - Nginx
limit_req— per-instance only, no shared state. Fine for crude protection, not for per-tenant quotas. - Sliding window log in Redis sorted sets — exact, but O(N) memory per key and
ZREMRANGEBYSCOREon every request. Viable at low key counts, not at 1M keys.
Real-world references
Stripe published their rate limiter design — they run multiple limiter types simultaneously (a request-rate limiter, a concurrency limiter, and per-endpoint fleet limits), which is the key insight that a single limiter is rarely enough: you need to protect against both "too many requests" and "too many slow requests holding workers." Cloudflare published the sliding-window-counter analysis showing sub-1% error at planetary scale. Envoy/Lyft's open-source ratelimit service is the reference implementation of the gateway-plus-Redis pattern. GitHub, Twitter, and Shopify all publish X-RateLimit-* headers with token-bucket semantics — Shopify's leaky-bucket API limit is a well-documented public example.
Follow-ups
- "Add a per-endpoint cost — a search costs 10, a GET costs 1." → The
costparameter is already in the Lua script. Weight by measured backend cost, publish the weights. - "How would you rate limit by IP when clients are behind a NAT?" → You'll punish shared egress. Layer signals: IP + user agent + auth token, and prefer authenticated identity where available. For unauthenticated traffic, use a lower limit and a challenge (CAPTCHA/proof-of-work) rather than a hard block.
- "Distinguish rate limiting from load shedding." → Rate limiting is a fairness/contract mechanism, applied per-tenant, independent of system health. Load shedding is a survival mechanism, applied based on your own saturation, dropping the lowest-value work regardless of who sent it. You need both. Netflix's concurrency-limits library (adaptive limits based on observed latency, using TCP-congestion-control-like algorithms) is the reference for the second.
43.2 Design a RAG System with Document-Level Access Control
(Referenced in §15.10 #1. This is the design most relevant to your background — enterprise search over permissioned corpora — and access control is the part that separates a demo from a product.)
Step 1 — Clarify
"Key questions: How many documents and how much churn — are we indexing 100k docs updated weekly, or 50M with real-time updates? What's the permission model — flat ACLs, group-based, hierarchical inheritance from folders, or row-level rules? How fresh must permissions be — if someone's access is revoked, is a 5-minute window acceptable or must it be immediate? What's the latency budget? And what's the consequence of a leak — is this internal docs, or regulated client material?"
Assume: 10M documents, ~100k updates/day, group-based ACLs with folder inheritance, revocation must be effective immediately (this is the hard requirement), p95 < 2 s end to end, and leakage is a serious compliance event. That combination is realistic for legal or financial document search.
Step 2 — The architecture
INGESTION (async, throughput-optimized)
Source systems ──CDC/webhook──► Document queue
│
├─► Extract (Tika/Unstructured: PDF, DOCX, HTML → text + structure)
├─► Chunk (structure-aware, §43.2 step 3)
├─► Enrich (contextual summary per chunk, entity extraction)
├─► Embed (batch, GPU, ~2k chunks/sec)
└─► Index → OpenSearch (BM25 + dense vector in ONE index)
with acl_groups as an indexed keyword field
PERMISSION SYNC (separate pipeline, lower latency)
Identity provider ──SCIM/webhook──► Permission service
└─► group membership cache (Redis, ~50 ms lookup)
└─► document ACL updates → partial index update (ACL field only)
QUERY (synchronous, latency-optimized)
Request + JWT
│
├─1─ AuthZ: resolve user → effective group set (~10 ms, cached)
├─2─ Query understanding: rewrite/decompose (~100 ms, small LLM, optional)
├─3─ HYBRID RETRIEVAL with ACL FILTER APPLIED IN THE QUERY (~80 ms)
│ BM25 top-100 ∥ kNN top-100 → RRF fusion → top-50
├─4─ Rerank: cross-encoder on top-50 → top-8 (~120 ms)
├─5─ Assemble context, generate with citations (~1.2 s, streamed via SSE)
└─6─ Post-check citations resolve to permitted docs (defense in depth)
Step 3 — The access control design (the part that matters)
The cardinal rule: filter at retrieval time, inside the query, never after. Post-filtering is wrong for three independent reasons, and naming all three is the strong answer:
- Recall collapse. If you retrieve top-100 and then filter, a user with access to 1% of the corpus may get zero results despite thousands of relevant permitted documents existing.
- Information leakage. Result counts, latency differences, and pagination behavior leak the existence of documents the user can't see. This is a real compliance finding, not a theoretical one.
- It's unbounded. You cannot know how deep to retrieve to guarantee k permitted results.
// OpenSearch: ACL filter INSIDE the kNN query, not applied after.
// The `filter` clause in knn is evaluated during graph traversal
// (efficient filtering), not as a post-processing step.
{
"size": 100,
"query": {
"bool": {
"filter": [
{ "terms": { "acl_groups": ["grp:legal-team", "grp:all-staff",
"user:s.albatati"] } }
],
"should": [
{ "match": { "content": { "query": "termination clause precedent",
"boost": 1.0 } } }
]
}
},
"knn": {
"field": "embedding",
"query_vector": [...],
"k": 100,
"num_candidates": 500,
"filter": {
"terms": { "acl_groups": ["grp:legal-team", "grp:all-staff",
"user:s.albatati"] }
}
}
}
The filtered-ANN recall problem — the deep technical point here. HNSW traverses a proximity graph; if you filter during traversal and the filter is highly selective (a user can see 0.1% of documents), the graph walk hits mostly-excluded neighborhoods and recall degrades badly. Three mitigations, chosen by selectivity:
| Filter selectivity | Strategy |
|---|---|
| >10% of corpus visible | Filtered HNSW traversal (efSearch raised ~2–3×). Works well. |
| 1–10% visible | Raise num_candidates substantially, or use a partitioned index |
| <1% visible | Pre-filter to a candidate set, then exact/flat search over it. Below a few hundred thousand vectors, brute-force cosine on a filtered subset is faster and exact. Lucene's engine will actually choose this automatically when the filter is selective enough. |
| Tenant isolation | Separate index per tenant — the cleanest answer for hard multi-tenancy. Costs index overhead per tenant; use for big tenants and a shared filtered index for the long tail. |
Denormalizing ACLs into the index is the design decision, and it has a cost: permission changes require reindexing the ACL field. The answer:
# Partial update of ONLY the ACL field — no re-embedding, no re-chunking.
# A folder permission change fans out to every chunk of every doc under it.
def revoke_group_from_folder(folder_id, group):
doc_ids = folder_service.documents_under(folder_id) # may be 100k docs
for batch in chunks(doc_ids, 1000):
opensearch.bulk([
{"update": {"_index": "docs", "_id": chunk_id}}
| {"script": {
"source": "ctx._source.acl_groups.removeAll([params.g])",
"params": {"g": group}}}
for chunk_id in expand_to_chunks(batch)
])
But the requirement was immediate revocation, and a 100k-document bulk update takes minutes. This is the crux of the design, and the honest answer is a two-layer model:
- Denormalized ACLs in the index for efficient retrieval — eventually consistent, seconds to minutes.
- A synchronous authorization check at result assembly against the authoritative permission service, for the ~8 documents that actually reach the context window. Eight point-checks at 5 ms each is 40 ms — affordable — and it makes revocation effective immediately, because the authoritative source is consulted before any content is shown.
"The index filter is an optimization for recall and cost; the post-check on the final result set is the security boundary. I never rely on a denormalized copy of an ACL as my only enforcement point."
That sentence is the whole answer to the security question, and it's the kind of layered thinking that reads as seasoned.
Also required: the same check must gate citations (a citation reveals a title and a snippet) and any cached answers. Semantic caching (§41.6) must include the effective permission set in the cache key, or you will serve one user's answer to another. This is the single most likely way a RAG system leaks data, and mentioning it unprompted is a strong signal.
Step 4 — Chunking and retrieval quality
def chunk_document(doc):
"""Structure-aware chunking. Fixed-size splitting is the default and
it's wrong for legal/technical documents: it splits mid-clause and
destroys the context that makes a chunk interpretable."""
sections = parse_structure(doc) # headings, clauses, tables, lists
chunks = []
for section in sections:
if section.token_count <= 512:
chunks.append(section) # keep intact
else:
chunks.extend(recursive_split(section, 512, overlap=64))
for c in chunks:
# CONTEXTUAL RETRIEVAL: prepend a short LLM-generated summary
# situating the chunk in the document. Anthropic published that
# this materially reduces retrieval failures, because a chunk
# that says "the party may terminate with 30 days notice" is
# meaningless without knowing WHICH contract and WHICH party.
c.context_header = summarize_placement(doc, c) # ~50 tokens, cached
c.embed_text = f"{c.context_header}\n\n{c.text}"
c.bm25_text = f"{doc.title} {c.section_path} {c.text}"
return chunks
Hybrid retrieval with RRF (§15.3) — and the reason it beats score normalization: BM25 scores and cosine similarities live on incompatible scales that vary per query, so any normalization is a heuristic. RRF only uses rank, which is scale-free:
def reciprocal_rank_fusion(rank_lists, k=60):
"""k=60 is the value from the original TREC work; it damps the
influence of top ranks just enough that a doc ranked #1 in one
list and #50 in another beats a doc ranked #3 in both only when
it should. Tune it on your own golden set — but 60 is a good prior."""
scores = defaultdict(float)
for ranks in rank_lists:
for rank, doc_id in enumerate(ranks, start=1):
scores[doc_id] += 1.0 / (k + rank)
return sorted(scores.items(), key=lambda kv: -kv[1])
Step 5 — Evaluation (the part that gets skipped and shouldn't)
Separate retrieval eval from generation eval. Conflating them means you can't tell whether a regression came from the index or the prompt.
EVAL_SUITE = {
# RETRIEVAL — needs labeled query→relevant-doc pairs
"recall@10": lambda: mean(retrieved_relevant / total_relevant),
"mrr": lambda: mean(1 / rank_of_first_relevant),
"ndcg@10": lambda: ...,
# Permission correctness — a HARD gate, not a metric to optimize
"acl_violations": lambda: count(retrieved_docs_user_cannot_see), # MUST be 0
# GENERATION — needs answer labels or a judge
"groundedness": lambda: judge("is every claim supported by context?"),
"answer_relevance": lambda: judge("does it answer the question asked?"),
"citation_accuracy":lambda: fraction_of_citations_that_support_their_claim,
"refusal_rate": lambda: fraction_where_model_correctly_said_unknown,
}
# Run on every prompt change, model version change, chunking change,
# and embedding model change. Block the merge on regression.
# acl_violations > 0 fails the build unconditionally.
Build a golden set of 200–500 queries before building the system, covering the head, the tail, adversarial permission probes ("show me the CEO's compensation memo"), and known-unanswerable questions.
Step 6 — Failure modes and degradation
| Failure | Response |
|---|---|
| Embedding service down | Fall back to BM25-only retrieval; quality drops, service survives |
| LLM provider down | Fail over to secondary provider; if all down, return ranked search results with snippets — a search engine is a valid degraded RAG |
| Reranker times out | Skip reranking, use RRF order, log the degradation |
| Permission service down | Fail closed. This is the one place you do not fail open. Return an error rather than risk serving unpermitted content. |
| Index stale | Surface a freshness indicator; alert on ingestion lag as an SLI |
Alternatives considered and rejected
- Fine-tuning instead of RAG — rejected: permissions can't be baked into weights, updates require retraining, and there's no citation path. Fine-tuning complements RAG (for tone, format, domain vocabulary), it doesn't replace it here.
- Long-context stuffing (put all documents in the prompt) — rejected at 10M docs on cost and latency grounds, and it doesn't solve permissions. Viable only for small, fixed corpora.
- Pure vector search, no BM25 — rejected: exact-match queries (case numbers, statute citations, defined terms) are exactly what legal users search for, and dense retrieval is weak on them. Hybrid is non-negotiable in this domain.
- GraphRAG — considered: valuable if the dominant queries are relational ("which contracts reference this entity across the portfolio"). Expensive to build and maintain. I'd recommend it as a phase 2 for a specific query class, not as the base architecture.
- A separate ACL-filtered index per user — rejected: 1M users × index overhead is absurd. Per-tenant indexes for large tenants is the viable version of this instinct.
Follow-ups
- "Users say results are worse than the old keyword search." → Almost always a recall problem masked as a ranking problem. Instrument: log queries with zero relevant results in the top-10, check whether the document was retrieved at all (retrieval failure) or retrieved and ranked low (ranking failure), and check whether the ACL filter is over-restricting. Two different fixes.
- "How do you handle a 400-page document?" → Parent-document retrieval: embed and search small chunks, but return the enclosing section for context. Also hierarchical summarization for "what does this document say overall" queries, which chunk retrieval answers badly.
- "How do you know when the model doesn't know?" → Retrieval score thresholds (if top result's score is below a calibrated floor, refuse), explicit refusal instructions in the prompt, and measuring refusal rate as a first-class metric. A system that never refuses is not confident, it's broken.
- "Cost at 1M queries/month?" → Run the §41.6 math live: context is the dominant term, so top-8 reranked chunks at ~400 tokens each plus a system prompt is ~4k input; caching the system prompt and using a small model for query rewriting takes it to roughly $12–18k/month at frontier-model prices, and materially less with routing.
44. Worked Design Answers — AI & LLM Systems
44.1 Design an LLM Gateway
(§15.10 #2. This is the design most AI-platform teams actually need, and it's the one that shows you think about a fleet rather than a feature.)
Clarify
"How many internal consumers and what's the aggregate volume? Are we multi-provider or single? Is the requirement cost control, reliability, governance, or all three? Do teams need to bring their own keys, or is this a central cost pool? And is there a data-residency or PII constraint on which provider sees what?"
Assume: 40 internal teams, ~50M requests/month, multi-provider (Anthropic + OpenAI + a self-hosted Llama for cheap/private work), central cost pool with chargeback, and a rule that PII-bearing traffic must stay on the self-hosted model.
Architecture
┌─────────────── LLM Gateway ───────────────┐
Client SDK ──────────►│ │
(OpenAI-compatible) │ 1. AuthN/Z → team identity, quota check │
│ 2. Policy → PII scan, model allowlist │
│ 3. Cache → exact + semantic │
│ 4. Route → model selection │
│ 5. Execute → with retry/failover/hedge │
│ 6. Meter → tokens, cost, attribution │
│ 7. Trace → OTel span with gen_ai.* │
└───────┬──────────┬──────────┬──────────────┘
│ │ │
Anthropic OpenAI vLLM (self-hosted)
Design decision #1: expose an OpenAI-compatible API. Every SDK, framework, and tool speaks it. Making your gateway a drop-in base-URL change is what drives adoption; a bespoke API means teams route around you, and a gateway nobody uses controls nothing. Say this — it's a product-thinking signal on an infrastructure question.
The routing layer
class Router:
def select(self, request, team_policy) -> ModelChoice:
# 1. HARD CONSTRAINTS FIRST — these are not optimizations.
if self.pii_detector.scan(request.messages).found:
return ModelChoice("self-hosted-llama-70b", reason="pii_policy")
if request.model_override and request.model_override in team_policy.allowed:
return ModelChoice(request.model_override, reason="explicit")
# 2. CAPABILITY FLOOR — some requests can't use a small model.
if request.tools or request.response_format == "json_schema":
candidates = MODELS_WITH_TOOL_USE
elif estimate_tokens(request) > 100_000:
candidates = LONG_CONTEXT_MODELS
else:
candidates = ALL_MODELS
# 3. COST-AWARE ROUTING — the actual lever. Classify difficulty
# with a cheap classifier (not another frontier LLM call —
# that defeats the purpose). A fine-tuned small model or even
# a logistic regression on features works.
difficulty = self.classifier.score(request) # ~5ms, local
if difficulty < 0.3:
return ModelChoice(cheapest(candidates), reason="easy",
escalate_on_low_confidence=True)
return ModelChoice(best(candidates), reason="hard")
The escalation pattern that makes cheap-first safe: run the small model, evaluate a confidence signal (logprobs, a self-rated confidence field in a structured response, or a cheap verifier), and retry on the large model when it's low. You pay for two calls on the escalated fraction; if 60% of traffic resolves on a model 10× cheaper, you still net ~45% savings. Measure the escalation rate — if it exceeds ~25%, routing is costing you money and you should raise the difficulty threshold.
Caching — two tiers, different risk profiles
# TIER 1: EXACT MATCH. Hash of (model, messages, temperature, tools, seed).
# Zero risk. Only valid at temperature=0 or with a fixed seed — at
# temperature>0 the user asked for variety and you'd be lying to them.
exact_key = sha256(canonical_json(request))
# TIER 2: SEMANTIC. Embed the query, find near-neighbors above threshold.
# HIGH RISK, must be gated:
# - Cache key MUST include the effective permission set (§43.2) or you
# leak across tenants. This is the #1 cause of data leaks in these systems.
# - Only for intent classes where a paraphrase deserves the same answer
# (FAQ, docs lookup). NEVER for personalized, computational, or
# time-sensitive queries.
# - Threshold must be tuned on a labeled set; 0.95 cosine is a starting
# point, and you should measure the false-hit rate explicitly.
semantic_key = (embedding(request.user_message), tenant_id, permission_hash)
Provider-side prompt caching is separate and strictly better where available — it's exact, provider-enforced, and cuts input cost ~90% on the cached prefix. The gateway's job is to maximize its hit rate: keep system prompts byte-stable, put variable content (retrieved chunks, user message) after static content, and warn teams when a prompt change invalidates a cache prefix. A gateway that reports "your cache hit rate dropped from 82% to 11% after deploy X" is delivering real value.
Reliability
async def execute(self, choice, request):
for attempt, provider in enumerate(self.failover_chain(choice)):
try:
# Deadline propagation: the remaining budget shrinks each attempt.
budget = request.deadline - now()
if budget <= 0: raise DeadlineExceeded()
return await provider.call(request, timeout=budget)
except RateLimited as e:
# Respect Retry-After; if it exceeds our budget, fail over NOW
# rather than sleeping through the deadline.
if e.retry_after > budget: continue
await asyncio.sleep(e.retry_after + jitter())
except (Overloaded, ServerError, Timeout):
self.breaker[provider].record_failure()
continue # fail over to next provider
except InvalidRequest:
raise # 4xx: retrying won't help. Fail fast.
- Circuit breakers per provider, so a degraded provider stops receiving traffic instead of consuming every request's budget before failing over.
- Hedging (§40.1) for latency-critical paths — but gate it hard: LLM calls are expensive, so hedge only when the hedge rate is under a few percent, and never hedge streaming requests that have already emitted tokens.
- Streaming and failover conflict: once you've streamed tokens to the client, you cannot silently retry on another provider. Either buffer the first N tokens before emitting (adds TTFT) or accept that mid-stream failures surface to the user. State the tradeoff.
Metering and chargeback
# Emitted on EVERY request, to both the trace and a metering topic.
UsageRecord(
team_id=ctx.team, service=ctx.service, feature=ctx.feature,
model=choice.model, provider=choice.provider,
input_tokens=r.usage.input_tokens,
cache_read_tokens=r.usage.cache_read_input_tokens,
output_tokens=r.usage.output_tokens,
cost_usd=price(choice.model, r.usage),
cached=cache_hit, cache_tier=tier,
routed_reason=choice.reason, escalated=escalated,
latency_ms=elapsed, ttft_ms=ttft, finish_reason=r.stop_reason,
)
Chargeback needs three things to be credible: attribution granularity finer than the team (feature-level, so a team can act on it), a daily dashboard rather than a monthly surprise, and budget guardrails — soft alert at 80% of a team's monthly budget, hard throttle at 100% with an override path. Without the override path, you'll take down someone's launch and lose the platform's political capital.
Follow-ups
- "Why not let teams call providers directly?" → You lose central cost visibility, per-team rate limiting (one team's runaway loop exhausts the org's provider quota and takes down everyone), PII policy enforcement, provider failover, and the ability to switch models centrally. The gateway is the only place those controls can exist.
- "What's the added latency?" → Target under 10 ms p99 of gateway overhead — auth from a cache, policy scan on a local model, routing from a local classifier. Anything more and teams will route around you. Measure and publish it.
- "How do you roll out a new model version?" → Exactly like any other deploy (§39): shadow the new model on a traffic sample, run the eval suite on the shadow output, canary at 1%/5%/25% with eval-score and cost as the gating metrics, auto-rollback on regression. Model versions are deploys, and pinned versions are mandatory — a provider silently updating a model under you is a production change you didn't make.
44.2 Design a Multilingual Semantic Search System
(§15.10 #6, and directly your domain — this is the design you should be able to give better than the interviewer.)
Clarify
"How many languages, and are queries and documents in the same language or cross-lingual? What's the corpus size and update rate? Is there a hard requirement that a query in Arabic finds a document in English? What's the relevance bar — is this navigational lookup or exploratory research? And do we have relevance judgments, or are we starting cold?"
Assume: 30 languages, cross-lingual required (an Arabic query must find English documents), 500M documents, 100k updates/day, exploratory research use case, p99 < 300 ms, and existing click logs but no human judgments.
The three architectural choices, with the decision
Choice 1: index topology
| Option | Pros | Cons | Verdict |
|---|---|---|---|
| One index, all languages | Simple ops, cross-lingual by default with multilingual embeddings | Analyzer conflicts — you can't apply Arabic stemming and German compound splitting to the same field | Use for the vector field |
| Per-language index, routed by detected language | Correct analyzers, tunable per language | Cross-lingual requires fan-out; language detection errors misroute | Use for the lexical field |
| Hybrid: one vector index + per-language lexical fields in a shared index | Correct analysis and cross-lingual retrieval | More mapping complexity | This is the answer |
// Per-language analyzed subfields; one shared dense vector field.
{
"mappings": {
"properties": {
"content_en": { "type": "text", "analyzer": "english" },
"content_ar": { "type": "text", "analyzer": "arabic" },
"content_de": { "type": "text", "analyzer": "german" },
"content_zh": { "type": "text", "analyzer": "smartcn" },
"content_generic": { "type": "text", "analyzer": "icu_analyzer" },
"lang": { "type": "keyword" },
"embedding": {
"type": "knn_vector", "dimension": 768,
"method": { "name": "hnsw", "engine": "lucene",
"parameters": { "m": 16, "ef_construction": 256 } }
}
}
}
}
Choice 2: the embedding model. Cross-lingual retrieval requires embeddings where "contract termination" in English and its Arabic equivalent land in the same region of vector space. That means a genuinely multilingual model trained with cross-lingual alignment (the LaBSE / multilingual-E5 / BGE-M3 family), not a strong English model with translated inputs. Name the tradeoff: multilingual models are typically a few points weaker than a same-size English-only model on English-only benchmarks. You pay that tax deliberately for cross-lingual capability.
Choice 3: translate or not. Three viable strategies:
- Multilingual embeddings only — simplest, no translation cost, works well for semantic similarity. Default choice.
- Translate queries into every document language, run lexical search per language — expensive at query time, but gives exact-match capability cross-lingually. Use selectively for high-value queries.
- Translate documents into a pivot language at index time — high one-time cost, loses nuance, but makes everything a monolingual problem. Reasonable if your corpus is small and stable; not at 500M docs.
Language-specific problems most candidates miss
Naming these is where you separate yourself, because they're only known from doing the work:
- Tokenization has no whitespace in CJK and Thai — you need dictionary-based segmentation (kuromoji for Japanese, smartcn/jieba for Chinese, ICU for Thai). Whitespace tokenization silently produces garbage.
- German and Dutch compounds — "Lebensversicherungsgesellschaft" must decompound to match "Versicherung". Requires a decompounder with a dictionary.
- Arabic and Hebrew — rich morphology (clitics, prefixes), optional diacritics that must be normalized away, and the Arabic letter forms (alef variants ا/أ/إ/آ, taa marbuta ة vs ه) that need normalization or exact match fails.
- Turkish — agglutinative; a single word carries what English needs six for. Also the famous dotted/dotless i lowercasing bug that breaks naive
toLowerCase(). - Transliteration and code-switching — users type Arabic in Latin script ("kayf halak"), or mix languages in one query. Language detection on a 3-word query is unreliable; prefer running multiple analyzers over betting on detection.
- Token cost asymmetry — non-Latin scripts consume 2–4× the tokens of English in most BPE tokenizers, which directly hits embedding cost and LLM context budget (§41.6). Say this; it connects the linguistic problem to the cost model.
- Normalization pipeline — Unicode NFKC, script detection, diacritic folding (language-aware: folding is right for Arabic, wrong for Vietnamese where diacritics are phonemic).
Query pipeline
Query "شروط إنهاء العقد"
│
├─ Normalize (NFKC, Arabic letter normalization, diacritic strip)
├─ Detect language (with confidence; low confidence → run multi-analyzer)
├─ Optional: LLM query understanding (rewrite, expand, extract filters)
│ — gate on latency budget; only for long/ambiguous queries
├─ Embed with multilingual model ──┐
├─ Lexical: query content_ar + ├─► parallel, ~40ms each
│ content_generic ───────┘
├─ RRF fusion of both rank lists (k=60)
├─ Rerank top-50 with a MULTILINGUAL cross-encoder (~120ms)
│ — critical: a monolingual reranker destroys cross-lingual results
└─ Diversity/business rules → top-10
Scaling the vector index — the math you must do out loud
500M docs × 768 dims × 4 bytes (fp32) = 1.54 TB of raw vectors
HNSW graph overhead ≈ m × 8 bytes × N = 16 × 8 × 500M = 64 GB
Total fp32: ~1.6 TB, must be RAM-resident for low latency.
That's 20+ large nodes. Options, in order of what I'd try:
1. SCALAR QUANTIZATION to int8: 768 × 1 byte = 384 GB. 4x reduction,
typically 1-2% recall loss with rescoring of the top candidates
against full-precision vectors. ← DO THIS FIRST, nearly free.
2. DIMENSION REDUCTION via Matryoshka embeddings: models trained so
truncating to 256 dims retains most quality. 384 GB → 128 GB.
Combined with (1), a 12x total reduction.
3. PRODUCT QUANTIZATION (IVF-PQ): 10-50x reduction, larger recall cost,
worse for filtered search. Use when memory is the binding constraint.
4. DISK-BASED ANN (DiskANN-style): SSD-resident graph, RAM for a
compressed index. Trades ~2-5ms latency for a 10x cost reduction.
Worth it at this scale.
Sharding: by document ID hash, NOT by language. Language sharding
seems natural and is wrong — cross-lingual queries would need to hit
every shard anyway, and language distribution is heavily skewed, so
you'd get badly unbalanced shards.
Evaluation without human judgments (the realistic constraint)
Cold start plan, in order:
1. CLICK MODELS from existing logs. Raw clicks are position-biased;
correct with a position-based model or use counterfactual estimation
(inverse propensity scoring). Gives you a large, cheap, noisy label set.
2. INTERLEAVING for online comparison — far more sensitive than A/B for
ranking changes because each user sees both rankings, removing
between-user variance. Team-draft interleaving is the standard method.
THIS IS THE HIGHEST-VALUE TECHNIQUE HERE and it comes from your domain.
3. A SMALL GOLDEN SET (300-500 queries) with human judgments, weighted
toward the head and toward cross-lingual pairs specifically. Expensive
but necessary as ground truth to validate the click model.
4. LLM-as-judge for relevance grading to scale (3) — validate its
agreement with human judges on the golden set BEFORE trusting it,
and re-validate whenever the judge model version changes.
Metrics: NDCG@10 overall AND per-language. The trap: a global average
hides that Arabic recall is 40% while English is 85%. ALWAYS SLICE BY
LANGUAGE. Report the worst language, not the mean.
Follow-ups
- "Cross-lingual results are worse than monolingual. Why?" → Usually the reranker. Cross-encoders are frequently English-heavy in training even when the bi-encoder is multilingual. Check whether reranking hurts cross-lingual pairs by evaluating with reranking disabled — this is a specific, testable hypothesis and offering it is a strong signal.
- "How do you handle a new language?" → If the embedding model supports it, retrieval works immediately with the generic analyzer; add a language-specific analyzer and evaluate the lift. If the model doesn't support it, you need a new model, which means reindexing 500M documents — a multi-week job. Model choice is a long-term commitment; say so.
- "How do you reindex 500M docs without downtime?" → Alias-based blue/green (§20.4): build
docs-v2alongsidedocs-v1, backfill withrefresh_interval: -1and replicas 0 for speed, dual-write new updates to both, verify with a sampled relevance diff, atomically swap the alias, keep v1 for a rollback window, then drop. At 500M docs with GPU embedding at ~2k/sec, that's ~70 hours of embedding — parallelize across GPUs and plan for days, not hours.
44.3 Design an Agent Platform with Sandboxed Tool Execution
(§15.10 #4. The design that most signals you understand where agentic systems actually break.)
Clarify
"What class of tools — read-only APIs, or things that write to production systems? Who are the agents acting as — the user's identity, or a service identity? What's the tolerance for a wrong action, and does anything need human approval? How long can a task run — seconds, or hours? And do we need to explain, after the fact, exactly why the agent did something?"
Assume: mixed read/write tools including some irreversible ones, agents act on behalf of a user with that user's permissions, wrong actions on write tools are serious, tasks run up to an hour, and full auditability is required.
Architecture
┌──────────────────────────────────────────────────────────┐
│ Control plane │
│ • Agent registry (versioned definitions, prompts, tools)│
│ • Tool registry (schemas, permissions, rate limits) │
│ • Policy engine (who may run what, against what) │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ Execution plane │
│ Orchestrator (durable workflow — Temporal-style) │
│ ├── LLM step → gateway (§44.1) │
│ ├── Tool step → sandbox (gVisor/Firecracker/container)│
│ ├── Approval step → human-in-the-loop, durable wait │
│ └── Checkpoint after EVERY step │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ Observability plane │
│ Full trace: every prompt, tool call, result, token, cost│
│ Replay: re-run a trace deterministically for debugging │
└──────────────────────────────────────────────────────────┘
Design decision: the orchestrator is a durable workflow engine, not a while loop. An agent loop that lives in a process is lost when the process restarts — after 40 minutes and $12 of tokens. Durable execution (§42.3) checkpoints every step, so a crash resumes exactly where it stopped. This single choice removes an entire class of production pain, and choosing it unprompted is the architectural signal in this question.
The security model — the heart of the answer
class ToolInvocation:
"""Three independent gates, all required. Defense in depth, because
the LLM's decision to call a tool is NOT a trusted input."""
async def invoke(self, agent_ctx, tool_name, raw_args):
# GATE 1: SCHEMA VALIDATION. The model hallucinates tool calls
# and arguments. Validate against the registered JSON Schema
# BEFORE anything else. Reject and return an error the model can
# correct from — don't crash the run.
tool = self.registry.get(tool_name)
if tool is None:
return ToolError(f"unknown tool: {tool_name}", recoverable=True)
try:
args = tool.schema.validate(raw_args)
except ValidationError as e:
return ToolError(f"invalid arguments: {e}", recoverable=True)
# GATE 2: AUTHORIZATION AGAINST THE USER, NOT THE AGENT.
# The agent runs with a token derived from the user's identity
# (RFC 8693 token exchange, §21.1), scoped DOWN to the minimum
# this agent needs. The agent can never exceed the user's rights,
# and usually holds far fewer.
decision = await self.policy.check(
principal=agent_ctx.on_behalf_of,
agent=agent_ctx.agent_id,
action=tool.action, resource=args.get("resource_id"))
if not decision.allowed:
return ToolError("not permitted", recoverable=False)
# GATE 3: HUMAN APPROVAL for irreversible / high-blast-radius
# actions. This is a DURABLE WAIT — the workflow suspends, could
# be hours, resumes on approval. Classify tools by reversibility
# at registration time, not per-call.
if tool.requires_approval or args_exceed_threshold(tool, args):
approval = await workflow.wait_for_signal(
"approval", timeout=timedelta(hours=4))
if not approval.granted:
return ToolError("approval denied", recoverable=False)
# EXECUTE in a sandbox with a hard timeout and no ambient credentials.
result = await self.sandbox.run(
tool, args,
timeout=tool.timeout,
credentials=decision.scoped_token, # short-lived, narrow
network_policy=tool.egress_allowlist, # explicit allowlist only
idempotency_key=f"{agent_ctx.run_id}:{agent_ctx.step}")
# GATE 4 (output side): tool output is UNTRUSTED INPUT.
return self.sanitize(result)
Prompt injection via tool output is the central threat, and the answer has layers:
- Structural separation — tool results go into the context clearly delimited and labeled as data, never concatenated into the instruction region.
- Least privilege — an agent that can only read cannot be talked into writing. The best defense against injection is that the injected instruction has no capability to abuse. Say this; it reframes the problem correctly.
- The lethal trifecta — an agent with (a) access to private data, (b) exposure to untrusted content, and (c) an egress channel can be made to exfiltrate. Break at least one leg: no untrusted content, or no private data, or no egress. Egress allowlists and stripping URLs/images from model output are the practical controls.
- Output validation — never render agent output as HTML without sanitization; never pass it to a shell, an eval, or an SQL string.
Loop termination is a correctness requirement, not a nicety:
LIMITS = dict(max_steps=40, max_tokens=500_000, max_cost_usd=5.00,
max_wall_clock=timedelta(hours=1), max_consecutive_errors=3)
# Plus cycle detection: hash (tool_name, args) per step; if the same
# call repeats 3 times, the agent is stuck — break and escalate rather
# than burning budget. This is the single most common runaway pattern.
Memory and context
- Short-term: the run's message history, compacted when it approaches the window — summarize completed sub-tasks into a structured state object rather than truncating (§16.3).
- Long-term: a store the agent reads/writes via tools, with the same permission model. Not a magic side channel.
- Sub-agent isolation: give a sub-agent only the slice of context it needs and return only its conclusion. This bounds context growth and limits injection blast radius — one mechanism, two benefits.
Evaluation
Agent eval is task-level success rate, not step accuracy. Build a suite of realistic tasks with programmatic success checks (did the ticket get created with the right fields? did the file end up in the right state?), and track: success rate, steps-to-completion, cost per successful task, human-intervention rate, and unsafe-action rate (attempted actions blocked by policy — this should be near zero and any spike is an incident signal, possibly an injection campaign).
Follow-ups
- "Multi-agent or single agent?" → Single agent with good tools, until proven otherwise. Multi-agent adds coordination failure modes, context duplication, and cost multiplication for benefits that usually come from better tool design. Use sub-agents for context isolation on genuinely separable subtasks — that's the case where it earns its keep.
- "How do you debug a run that did something bizarre?" → Full trace + deterministic replay. Store every prompt, response, tool call, and result with model version pinned. Replay with the same inputs to reproduce. Without replay, agent debugging is archaeology.
- "What if the tool is down?" → Return a structured error to the model — modern models handle "the tool failed, try another approach" well. Distinguish recoverable (return to model) from non-recoverable (abort the run). Don't retry non-idempotent tools blindly.
44.4 Design an Evaluation Pipeline That Gates CI
(§15.10 #5. Rarely asked directly, always impressive when volunteered.)
On every PR touching prompts / retrieval config / model version / tools:
1. FAST GATE (< 2 min, runs on every commit)
• Unit tests on parsing, schema validation, tool contracts
• 30-example smoke set, assertions not judges
• Cost/token regression check: did the prompt grow >10%?
2. FULL EVAL (< 20 min, runs on PR)
• 400-example golden set, stratified: head / tail / adversarial /
unanswerable / permission-probe
• RETRIEVAL metrics: recall@k, NDCG@10, MRR ← separate from below
• GENERATION metrics: groundedness, answer relevance, citation accuracy
• SAFETY: injection suite, PII leakage, refusal calibration
• Judge: rubric-based, pairwise vs the baseline output, RANDOMIZED
ORDER (position bias is real and large)
3. GATING POLICY
• Any acl_violation or PII leak → hard fail, no override
• Primary metric regression > 2% → fail, requires explicit override
with a written justification recorded on the PR
• Cost per request regression > 15% → fail
• Variance check: run the judge 3x on a subsample; if judge variance
exceeds the measured delta, the result is noise — say so rather
than shipping on a phantom improvement
4. POST-MERGE
• Shadow the new config on 1% of production traffic
• Compare eval scores on real queries (not just the golden set)
• Canary per §39.3 with eval score as a gating metric
The three things that make this credible rather than theatrical:
- Judge validation. Measure your LLM judge's agreement with human labels (Cohen's kappa) on a subset before trusting it. Re-measure when the judge model changes. An unvalidated judge is a random number generator with good grammar.
- Statistical honesty. A 400-example set detects large regressions, not 1% ones. Compute and state your minimum detectable effect. Report confidence intervals, not point estimates.
- The golden set is a living asset. Every production failure becomes a new eval case. That feedback loop is what makes the suite converge on reality instead of on what you imagined at design time.
44.5 Design Cost Attribution for LLM Spend Across 40 Teams
The gateway (§44.1) emits the raw records; this is the system on top.
Metering topic (Kafka) ──► Stream aggregator (Flink)
│ tumbling 1-min windows
│ keyed by (team, service, feature, model)
├──► Real-time budget checks → throttle signals
└──► Iceberg table (daily partitions)
│
├──► Dashboard (team self-serve)
├──► Anomaly detection (cost/request drift)
└──► Monthly chargeback report
The hard parts, which are organizational rather than technical:
- Attribution requires context propagation. Team/service/feature must ride the request from the caller through the SDK to the gateway. Make the SDK refuse to send requests without attribution headers — a hard requirement from day one, because retrofitting attribution is nearly impossible.
- Shared costs. A platform team's shared retrieval service consumes tokens on behalf of others. Decide the model early: direct attribution (pass through the originating team) beats proportional allocation, which beats a central pool nobody feels.
- Cached tokens must be priced correctly or teams optimizing for cache hits see no benefit and stop caring.
- Budgets need an override path with an audit trail. A hard throttle with no escape hatch will eventually take down a launch, and the platform will lose the mandate.
The metric that actually drives behavior: cost per unit of business value — cost per resolved support ticket, per document processed, per successful task. Raw spend rises with usage and tells a team nothing. Unit cost tells them whether they're getting better or worse, and it's the number to put on the dashboard.
45. Worked Design Answers — The Classics
Compressed but complete: for each, the clarifying question that matters most, the key insight interviewers are testing for, the design, and the follow-up that separates candidates.
45.1 News Feed (Push vs Pull vs Hybrid)
The question being tested: do you understand fan-out economics?
FAN-OUT ON WRITE (push): when Alice posts, write the post ID into every
follower's precomputed timeline (Redis list per user).
Read = O(1) — just read your list. Sub-millisecond.
Write = O(followers) — Alice has 50M followers → 50M writes per post.
→ Great for read-heavy, bad for celebrities.
FAN-OUT ON READ (pull): store posts by author; at read time, fetch the
authors you follow, merge, sort.
Write = O(1). Read = O(following × posts) with a merge.
→ Great for write-heavy, terrible read latency.
HYBRID (what everyone actually ships):
• Push for normal users (< ~10k followers) — 99.9% of accounts
• Pull for celebrities — their posts are NOT fanned out
• At read time: merge your precomputed timeline with a live pull of
the handful of celebrities you follow
This is the answer. Twitter's original architecture and its evolution
is the canonical public case study.
Details that show depth: timelines are capped (store ~800 entries, not all history — older reads fall back to pull); fan-out is asynchronous via a queue, so posting returns immediately and the timeline fills within seconds; inactive users are skipped in fan-out entirely and materialized lazily on login (a huge saving — most accounts are dormant); and ranking is a separate stage on top of retrieval, so the timeline store holds candidates, not the final order.
Follow-up: "How do you handle a post deleted after fan-out?" → You don't chase 50M list entries. Filter at read time against a tombstone set (a Bloom filter of deleted IDs in front of an exact check). Cheap deletes, slightly more expensive reads — the correct trade given the ratio.
45.2 Notification / Fan-out System
Key insight tested: deduplication and preference management, not delivery.
Event → Notification service
├─ 1. Preference check (per user, per channel, per category) — the
│ most common product bug is notifying someone who opted out
├─ 2. DEDUPLICATION + AGGREGATION — "5 people liked your post" not
│ 5 notifications. Window-based (5 min) with a coalescing key.
├─ 3. Rate limiting per user (§43.1) — a runaway loop must not send
│ 10k pushes to one person
├─ 4. Channel routing: push (APNs/FCM) / email (SES) / SMS / in-app
├─ 5. Template rendering + localization
└─ 6. Delivery with per-channel retry, DLQ, and bounce handling
Idempotency at the event level (event_id in a dedupe store with TTL) because upstream will double-deliver. Track delivery, open, and action rates per channel — and unsubscribe rate as a guardrail metric, because the failure mode of a notification system is not outage, it's users turning it off forever.
45.3 Distributed Job Scheduler with Exactly-Once Semantics
Key insight tested: "exactly-once execution" is impossible; "at-least-once execution + idempotent jobs" or "effectively-once" is achievable. Say this in the first 30 seconds.
-- The lease pattern: the entire design in one table.
CREATE TABLE jobs (
id UUID PRIMARY KEY,
run_at TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL, -- pending|leased|done|failed
lease_owner TEXT,
lease_expires TIMESTAMPTZ,
attempts INT NOT NULL DEFAULT 0,
payload JSONB NOT NULL
);
CREATE INDEX ON jobs (run_at) WHERE status = 'pending';
-- Atomic claim. SKIP LOCKED is the key: workers never block each other.
UPDATE jobs SET status='leased', lease_owner=$1,
lease_expires=now()+interval '5 minutes', attempts=attempts+1
WHERE id IN (
SELECT id FROM jobs
WHERE status='pending' AND run_at <= now()
ORDER BY run_at LIMIT 10
FOR UPDATE SKIP LOCKED)
RETURNING *;
Why leases and not locks: a worker that dies holding a lock blocks forever; a lease expires and the job is reclaimed. The consequence you must name: a worker that hangs past its lease means the job runs twice — hence idempotent jobs are mandatory, and long jobs must heartbeat to extend their lease.
Scale beyond one database with partitioned queues, or use a purpose-built engine (Temporal for durable workflows, Quartz clustered for simple cron, Airflow for DAGs, or a Kafka-based scheduler with time-bucketed topics). Recommend the buy over the build; the interesting answer is knowing why the naive SELECT ... WHERE run_at < now() polling loop fails (thundering herd, no fairness, no crash recovery).
45.4 Metrics / Observability Pipeline
Agents (OTel SDK) → OTel Collector (batch, tail-sample, redact, enforce
cardinality limits)
├─ metrics → Prometheus/Mimir (TSDB: delta-of-delta timestamp
│ encoding + XOR float compression, Gorilla-style)
├─ traces → tail sampler → object storage + index (Tempo/Jaeger)
└─ logs → hot (7d, searchable) / warm (30d) / cold (1y, archived)
The central problem is cardinality, and it's what the question is really about. A metric with an unbounded label (user ID, request ID, full URL path) creates a new time series per value; a million series will take down Prometheus. Enforce at the Collector: allowlist labels, drop or bucket high-cardinality dimensions, and route the high-cardinality questions to traces and logs where they belong. Meta's Scuba and the "wide events" model exist precisely because pre-aggregated metrics can't answer arbitrary high-cardinality questions — cite this when arguing for event-based observability.
Downsampling and retention: raw at 15 s for 7 days, 5-minute rollups for 90 days, hourly for 2 years. Storage cost drops ~100× and nobody queries 15-second resolution from six months ago.
45.5 Ad Click Aggregation (Stream Processing)
Key insight tested: event time vs processing time, and late data.
Click events → Kafka (partitioned by ad_id) → Flink
• Event-time windows (tumbling 1 min) keyed by (ad_id, campaign)
• WATERMARK = max_event_time − allowed_lateness(5 min)
"I will not see events older than this" — the promise that lets
a window close
• Allowed lateness 1 hour → late events update already-emitted results
• Side output for events later than that → a repair job, not silent loss
• Deduplication on click_id in a keyed state store with TTL
• Two-phase-commit sink → effectively-once into the serving store
The reconciliation answer that impresses: streaming gives you fast, approximate counts; a nightly batch job over the raw event log recomputes authoritative numbers, and you reconcile. Money is billed on the batch number, dashboards use the stream. That's the Lambda architecture's surviving lesson, and it's how ad systems actually work — nobody bills advertisers off a streaming aggregate that might have dropped a window.
45.6 Payments Ledger with Idempotency
Key insight tested: double-entry, immutability, and idempotency as an API contract.
-- Entries are IMMUTABLE. No UPDATE, no DELETE, ever. Corrections are
-- new reversing entries. This is 500-year-old accounting practice and
-- it's non-negotiable.
CREATE TABLE ledger_entries (
id BIGSERIAL PRIMARY KEY,
transaction_id UUID NOT NULL,
account_id UUID NOT NULL,
direction TEXT NOT NULL CHECK (direction IN ('debit','credit')),
amount_minor BIGINT NOT NULL CHECK (amount_minor > 0), -- integers only
currency CHAR(3) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- The invariant: for any transaction_id, SUM(debits) = SUM(credits).
-- Enforce in the write path AND verify continuously with a reconciliation
-- job that alerts on any imbalance. A ledger that can drift silently
-- is not a ledger.
CREATE TABLE idempotency_keys (
key TEXT PRIMARY KEY,
request_hash TEXT NOT NULL, -- detect key reuse with DIFFERENT body
response JSONB,
status TEXT NOT NULL, -- in_progress | complete
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Idempotency semantics that show you've done this (Stripe's model is the reference): the key is provided by the client; same key + same body returns the cached response; same key + different body is an error (422), not a silent overwrite — that catches client bugs; a key seen while in_progress returns 409 so concurrent retries don't double-execute; keys expire after 24 hours.
Balances are a projection (§42.2), maintained incrementally with periodic full recomputation for verification. Never store balance as the source of truth.
45.7 Ride-Hailing Dispatch
Key insight tested: geospatial indexing and the matching loop.
- Geo index: geohash, S2 cells (Google), or H3 hexagons (Uber's, and the better choice — hexagons have uniform neighbor distance, unlike squares where diagonal neighbors are farther). Driver locations stream into a per-cell index in Redis with short TTLs.
- Matching: don't greedily match each rider to the nearest driver — that's locally optimal and globally poor. Batch requests over a short window (a few seconds) and solve a bipartite assignment (Hungarian algorithm or a min-cost flow approximation) over the batch. Uber has published on batched matching outperforming greedy; naming this is a strong differentiator.
- Supply/demand signals feed surge pricing, which is a separate service consuming the same location stream.
- Scale: shard by geography — a city is an independent unit, which is also the natural blast-radius and deployment boundary (§39.1 rings).
Follow-up: "driver location updates at 1M writes/sec?" → Don't durably persist every ping. Keep current location in memory/Redis, sample to durable storage at a lower rate for the trip record, and use the stream for real-time matching. Not all writes deserve durability — deciding which do is the design.
45.8 Object Storage / File Sync (Dropbox-style)
Key insight tested: content-addressed chunking and delta sync.
- Chunk files with content-defined chunking (rolling hash / Rabin fingerprint) so an insertion at the start of a file doesn't shift every subsequent boundary — fixed-size chunking gets this wrong and re-uploads the whole file.
- Content-address each chunk by hash → automatic deduplication across all users, and sync becomes "which chunk hashes do you not have."
- Metadata service (file tree, versions, sharing) separate from the block service (chunk storage on S3). Different scaling properties, different consistency needs.
- Sync protocol: client computes local chunk list, server diffs, transfers only missing chunks. Notification of remote changes over a long-lived connection.
- Conflict resolution: last-writer-wins with conflict copies ("file (conflicted copy from Sam's laptop)") is what real products ship — honest about the fact that automatic merge of arbitrary binary files is impossible.
45.9 Ticketing / Inventory Under Contention
Key insight tested: how you handle 100k people wanting 1k seats.
The naive design (SELECT available; UPDATE) fails three ways:
1. Lock contention on the same rows melts the database
2. Oversell under race conditions
3. Queue collapse — everyone retries simultaneously
The real design:
1. VIRTUAL WAITING ROOM at the edge. Admit users at a rate the
backend can serve. This is the single most important decision:
you shape demand instead of absorbing it.
2. INVENTORY IN REDIS as an atomic counter (DECR), not in the RDBMS.
Redis is single-threaded — DECR is atomic and fast. The database
is the durable record, updated asynchronously.
3. RESERVATION with a TTL (10 min) — a hold, not a sale. Expired
holds return inventory automatically.
4. IDEMPOTENT checkout keyed on reservation ID.
5. Overselling protection: reserve a small buffer; reconcile
continuously against the durable record.
Follow-up: "bots?" → Rate limiting by identity not IP, proof-of-work or CAPTCHA at the waiting room, device fingerprinting, and purchase limits per verified identity. Accept that this is an arms race and instrument it as one.
45.10 Multi-Region Active-Active KV Store
Key insight tested: you must state the conflict resolution strategy unprompted.
- Partition by key hash; replicate each partition across regions.
- Write path options: (a) single-writer-per-key (route writes for key K to its home region — no conflicts, cross-region write latency for non-local keys); (b) local writes with async replication and conflict resolution (low latency, conflicts guaranteed); (c) global consensus per write (Spanner-style — correct, expensive, 100 ms+ cross-region).
- Conflict resolution: LWT with HLC timestamps (simple, silently loses data), vector clocks with application-level merge (Dynamo's model, pushes complexity to the client), or CRDTs (converges automatically, constrains your data types — §40.3).
- State the choice and its cost. "DynamoDB global tables use last-writer-wins and it is not configurable — if the business can't tolerate silent loss on concurrent writes to the same key, global tables are the wrong primitive."
- Failover: health-checked GeoDNS or anycast; and the hard part is not failover but failback with divergent data (§32).
46. Storage Engines & Databases, Expanded
46.1 LSM Trees vs B-Trees — the fundamental fork
Every storage decision traces back to this. Be able to draw both.
B+TREE (Postgres, MySQL/InnoDB, most RDBMS)
• Fixed-size pages (8 KB Postgres, 16 KB InnoDB) in a balanced tree
• WRITE: find the leaf, modify IN PLACE, write the whole page
→ random I/O, write amplification = page_size / row_size
(a 100-byte update rewrites 8 KB = 80x amplification)
→ must write to the WAL first (and Postgres writes the ENTIRE page
to WAL on first touch after a checkpoint — "full page writes" —
to survive torn pages)
• READ: O(log n) page reads, typically 3-4 levels, top levels cached
→ excellent, predictable read latency
• Space amplification ≈ 1.3x (fill factor, fragmentation)
LSM TREE (RocksDB, Cassandra, ScyllaDB, LevelDB, HBase, and the engine
under DynamoDB, CockroachDB, TiKV, Kafka Streams state stores)
• WRITE: append to WAL + insert into an in-memory memtable (skiplist)
→ SEQUENTIAL I/O only. Write amplification at ingest = ~1x
→ memtable fills → flushed as an immutable sorted SSTable to L0
• COMPACTION merges SSTables in the background
→ THIS is where LSM's write amplification actually lives:
leveled compaction ≈ 10-30x total; tiered ≈ 4-10x
• READ: check memtable → check each level's SSTables
→ potentially many disk reads. Mitigated by:
- BLOOM FILTERS per SSTable (~10 bits/key = 1% false positive;
turns "is this key here?" into a memory lookup)
- Block cache for hot data
- Index and summary blocks
→ read amplification is the LSM's cost
• Space amplification: leveled ~1.1x, tiered up to 2x+ (obsolete data
lingering until compaction)
THE TRADE (RUM conjecture — you optimize two of Read, Update, Memory):
B-tree → read-optimized, low space amp, high write amp
LSM → write-optimized, low space amp (leveled), high read amp
Compaction strategies — and picking one is a real interview question:
| Strategy | Write amp | Read amp | Space amp | Use when |
|---|---|---|---|---|
| Leveled (LCS) | High (~10-30×) | Low (≤1 SSTable per level) | Low (~1.1×) | Read-heavy, update-heavy, space matters |
| Size-tiered (STCS) | Low (~4×) | High (many overlapping tables) | High (up to 2×+) | Write-heavy, mostly-immutable data |
| Time-window (TWCS) | Lowest | Low for time-range queries | Low | Time series with TTL — whole SSTables expire and are dropped without merging. Correct answer for metrics/events. |
| Universal/hybrid | Middle | Middle | Middle | RocksDB default-ish compromise |
The tombstone problem (Cassandra's signature failure mode). A delete in an LSM writes a marker, not a removal — the data still exists in older SSTables. The tombstone can only be purged after gc_grace_seconds (default 10 days, sized to allow repair to propagate the delete everywhere). Consequences:
- A range scan over a partition with a million tombstones reads all of them →
TombstoneOverwhelmingException, query timeouts, and node instability. - Queue-like workloads on Cassandra are an anti-pattern — insert, read, delete, repeat over the same partition accumulates tombstones catastrophically. If you're asked "why is our Cassandra queue slow," this is the answer.
- Skipping repair past
gc_grace_secondscauses data resurrection: a node that missed the delete re-propagates the old value after the tombstone is purged elsewhere.
RocksDB tuning parameters worth knowing by name: write_buffer_size (memtable size), max_write_buffer_number, level0_slowdown_writes_trigger / level0_stop_writes_trigger (backpressure when compaction falls behind — "write stall" is the symptom you'll be asked to diagnose), target_file_size_base, bloom_filter_bits_per_key, and separating the WAL onto a different device. RocksDB is embedded in an enormous amount of infrastructure (Meta built it; it's under Kafka Streams, Flink state backends, CockroachDB historically, TiKV, MyRocks, and dozens more) — knowing it is broadly transferable.
Follow-up: "Write throughput dropped 10× suddenly and disk isn't full." → Compaction is falling behind and the LSM is applying backpressure (write stall). Check pending compaction bytes, L0 file count against the slowdown/stop triggers, and whether compaction threads are starved or I/O-bound. Fixes: more compaction threads, a different compaction strategy, faster storage, or reduced ingest rate. This is a very common real incident.
46.2 PostgreSQL internals that come up
MVCC and the vacuum problem. Postgres implements MVCC by writing a new row version for every UPDATE and marking the old one dead (xmin/xmax transaction IDs). Consequences:
- An UPDATE is effectively a DELETE + INSERT → indexes must be updated too (mitigated by HOT updates when the changed column isn't indexed and the new version fits on the same page — a real reason to avoid indexing frequently-updated columns).
- Dead tuples accumulate → table bloat.
autovacuumreclaims them. If autovacuum can't keep up (long-running transactions hold the horizon open, or it's tuned too conservatively for the workload), tables and indexes bloat, sequential scans get slower, and the fix isVACUUM FULL(takes anACCESS EXCLUSIVElock — an outage) orpg_repack(online). - Transaction ID wraparound: XIDs are 32-bit. If a table isn't vacuumed within ~2 billion transactions, Postgres shuts down writes to prevent data loss. This has taken down major services (Sentry wrote a well-known postmortem). Monitor
age(datfrozenxid). Naming wraparound unprompted is a strong operator signal.
Index types and when each wins:
-- B-tree: default; equality and range on scalar types
CREATE INDEX ON orders (customer_id, created_at DESC); -- composite, order matters
-- Covering index: include non-key columns so the query never touches the heap
CREATE INDEX ON orders (customer_id) INCLUDE (status, total); -- index-only scan
-- Partial: index only the rows you query. Dramatically smaller.
CREATE INDEX ON orders (created_at) WHERE status = 'pending';
-- GIN: multi-value columns — JSONB, arrays, full-text
CREATE INDEX ON docs USING gin (metadata jsonb_path_ops);
CREATE INDEX ON docs USING gin (to_tsvector('english', body));
-- BRIN: huge append-only tables with natural correlation to physical order.
-- A BRIN index on a 1 TB time-series table is MEGABYTES, not gigabytes.
CREATE INDEX ON events USING brin (created_at) WITH (pages_per_range = 128);
-- GiST: geometric, range types, nearest-neighbour
CREATE INDEX ON shapes USING gist (geom);
Reading a query plan — the skill, not the syntax:
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT ...;
-- What to look for, in order:
-- 1. Rows Removed by Filter → an index is missing or unusable
-- 2. estimated rows vs actual rows off by >10x → stale stats (ANALYZE)
-- or a correlated-columns problem (CREATE STATISTICS)
-- 3. Seq Scan on a large table in a selective query → missing index,
-- or the planner chose it because the index is unselective
-- 4. Nested Loop with a large outer → often the cause of a 100x slowdown
-- when row estimates are wrong
-- 5. shared read vs shared hit → cache miss ratio
-- 6. Sort Method: external merge Disk → work_mem too small
Connection pooling: Postgres uses a process per connection (~10 MB each); a few hundred connections is the practical ceiling. PgBouncer in transaction pooling mode multiplexes thousands of clients onto tens of server connections — but it breaks session-scoped features: prepared statements (pre-PG14 protocol-level), SET/session variables, advisory locks, LISTEN/NOTIFY, and temp tables. Know what breaks; that's the interview content.
46.3 DynamoDB — modeling by access pattern
Single-table design, concretely. The idea: overload one table's PK/SK so that related entities share a partition and one query retrieves them together.
PK SK Attributes
------------------ -------------------- -------------------------
USER#42 PROFILE name, email, created_at
USER#42 ORDER#2026-08-01#a1 total, status
USER#42 ORDER#2026-07-15#b2 total, status
ORDER#a1 METADATA shipping_address
ORDER#a1 ITEM#1 sku, qty, price
ORDER#a1 ITEM#2 sku, qty, price
Access patterns satisfied:
"user profile + recent orders" → Query PK=USER#42, SK begins_with ""
ONE request, ONE round trip
"all items in order a1" → Query PK=ORDER#a1, SK begins_with ITEM#
"orders in a date range" → SK is sortable by design (ISO dates)
"all pending orders" (GSI) → GSI1PK=STATUS#pending, GSI1SK=created_at
— a SPARSE index: only write GSI1PK
when status is pending, and the index
contains only pending orders. Elegant
and cheap.
The limits that shape design: 400 KB item, 1 MB per query page, 10 GB per partition key for LSIs (a hard cap that has caused real migrations), 3000 RCU / 1000 WCU per partition. Transactions are limited to 100 items and cost 2× — they exist for correctness, not for bulk work.
Hot partition mitigation, in order of preference: (1) pick a better partition key; (2) write sharding — append #{random(0..N)} to the PK and scatter-gather on read; (3) DAX or an application cache for read hotspots; (4) on-demand capacity to absorb spikes. Modern DynamoDB splits hot partitions automatically by both size and throughput, which handles much of this — but a single hot key cannot be split, and that's the case you must design around.
The honest verdict: DynamoDB is superb when access patterns are known and stable, and painful when they change. The migration cost of a wrong partition key is the reason "access patterns first" is the mantra. If the product is still discovering its queries, Postgres is the lower-regret choice.
46.4 Choosing — the decision walkthrough
When asked "which database," don't name one. Walk the questions:
- What are the access patterns? Point lookups, range scans, ad hoc analytics, full-text, similarity, graph traversal? This alone eliminates most options.
- What's the consistency requirement per operation? Not per system — per operation. Most systems have a few operations needing strong consistency and many that don't.
- What's the read:write ratio and absolute scale? 1000:1 at 100 QPS is a laptop. 1:1 at 1M QPS is an architecture.
- What's the data size and growth? 100 GB fits in RAM on one machine and changes everything.
- What does the team already run? A boring database the team can operate at 3 a.m. beats an optimal one they can't. This is a legitimate, senior answer and interviewers reward it.
The default recommendation to state and then justify departures from: "Postgres until it hurts." It does OLTP, JSON, full-text, geospatial, time-series (with Timescale), and now vectors (pgvector). One system, one operational model, mature tooling. Add specialized stores when a specific access pattern genuinely outgrows it — and be able to name the specific metric that triggered the move.
47. Retrieval, Search & Ranking, Expanded
47.1 The inverted index, mechanically
Documents:
d1: "distributed systems design"
d2: "systems design interview"
Inverted index (term → posting list):
distributed → [(d1, tf=1, pos=[0])]
systems → [(d1, tf=1, pos=[1]), (d2, tf=1, pos=[0])]
design → [(d1, tf=1, pos=[2]), (d2, tf=1, pos=[1])]
interview → [(d2, tf=1, pos=[2])]
Phrase query "systems design" uses POSITIONS: find docs where
pos(design) = pos(systems) + 1. This is why positions are stored
and why they cost index space.
Lucene segment mechanics — the operational model behind Elasticsearch/OpenSearch:
- A segment is an immutable mini-index. Writes go to an in-memory buffer.
- Refresh (default 1 s) makes the buffer searchable as a new segment — this is why ES is "near-real-time," not real-time. Set
refresh_interval: -1during bulk loads for a large throughput win. - Flush persists to disk and truncates the translog (durability, not visibility — a distinction candidates confuse).
- Merge combines small segments into larger ones in the background. Deletes are tombstones until a merge purges them (same pattern as LSM — because Lucene is an LSM-ish structure).
- Consequences: too many segments → slow queries (every segment is searched); merge storms → I/O spikes; a
force_mergeon a live index is a foot-gun except on read-only indices.
BM25, and why it beat TF-IDF:
score(q,d) = Σ IDF(t) × [ tf(t,d) × (k1+1) ] /
[ tf(t,d) + k1 × (1 − b + b × |d|/avgdl) ]
k1 (≈1.2) controls TERM FREQUENCY SATURATION — the 10th occurrence of
a word adds far less than the 2nd. Raw TF-IDF grows unboundedly and
rewards keyword stuffing; BM25 saturates.
b (≈0.75) controls LENGTH NORMALIZATION — long documents shouldn't win
just by containing more words.
Being able to say "BM25 is TF-IDF plus saturation and length normalization" in one sentence is the level of fluency expected.
47.2 Vector search internals
HNSW builds a multi-layer proximity graph: sparse long-range links at the top for fast traversal, dense short-range links at the bottom for accuracy. Search descends greedily.
Parameters and their real effects:
m (16-64) max links per node. ↑ = better recall,
more memory (m × 8 bytes × N), slower build
ef_construction (128-512) candidate list size at BUILD time.
↑ = better graph quality, much slower indexing
ef_search (m..1000) candidate list at QUERY time. ↑ = better recall,
linearly slower. THE runtime recall/latency dial.
Memory: vectors (d × 4 bytes × N) + graph (m × 8 × N)
Deletes are soft — HNSW cannot cheaply remove a node without damaging
graph connectivity. Deleted nodes are marked and skipped, and the index
degrades until rebuilt. HIGH-CHURN CORPORA ARE HNSW'S WEAK SPOT — say so.
| Index | Memory | Recall | Build | Filter support | Use when |
|---|---|---|---|---|---|
| Flat (exact) | 1× | 100% | none | perfect | <1M vectors, or post-filter subsets |
| HNSW | High | 95-99% | slow | good (filtered traversal) | The default |
| IVF-Flat | 1× + centroids | tunable via nprobe | fast | good | Large, memory-available |
| IVF-PQ | 10-50× smaller | 80-95% | fast | poorer | Memory-constrained, billions |
| ScaNN | Middle | High | Middle | good | Google's; strong recall/speed frontier |
| DiskANN | Low RAM, SSD-resident | High | slow | good | Billion-scale on a budget |
47.3 Learning to rank and the multi-stage funnel
500M docs
│ RETRIEVAL (recall-oriented, cheap: BM25 + ANN) → ~1000
│ LIGHT RANKING (fast model, few features) → ~200
│ HEAVY RANKING (LTR / cross-encoder, many features) → ~20
│ RE-RANKING (diversity, freshness, business rules) → 10
▼
The principle: each stage is 10–100× more expensive per document and operates on 10× fewer documents. Total cost stays flat while quality rises. Getting the funnel wrong — running an expensive model too early — is the most common architectural mistake in search and recsys.
LTR approaches: pointwise (predict a relevance score — simple, ignores that ranking is relative), pairwise (RankNet — learn which of two docs is better), listwise (LambdaMART/LambdaRank — directly optimizes NDCG; LambdaMART with gradient-boosted trees remains the strong baseline and often beats neural rankers on tabular feature sets). Neural: cross-encoders (BERT over query+doc concatenated — best quality, must run on a short candidate list), ColBERT (late interaction — token-level matching with precomputed doc embeddings, a middle ground).
Features that matter: query-document (BM25 per field, semantic similarity, exact-match indicators), document (PageRank-ish authority, freshness, quality signals, length), query (length, intent class, language), and user/context (personalization, location, device, session history). Feature freshness and training/serving skew is where LTR systems actually break — the feature store exists for this.
47.4 Evaluation done right
OFFLINE
NDCG@k — graded relevance with position discounting. The default.
MRR — for known-item search (one right answer)
Recall@k — for the RETRIEVAL stage specifically; a ranking metric on
a candidate set you never retrieved is meaningless
MAP — multiple relevant docs, binary relevance
ONLINE
Interleaving — team-draft: merge two rankings, attribute clicks to the
system that contributed the clicked item. 10-100x more sensitive than
A/B because it removes between-user variance. THE tool for ranking
experiments, and underused outside search teams.
A/B — needed for effects interleaving can't see (engagement over time,
revenue, retention)
Guardrails — latency, zero-result rate, abandonment, query reformulation
rate (a rising reformulation rate means users aren't finding things,
even if CTR looks fine)
THE TRAP: offline/online correlation is often weak. Validate that your
offline metric predicts online outcomes before trusting it to gate
launches. Many teams optimize NDCG for a year and move no business metric.
Position bias and counterfactual evaluation: click logs are biased — position 1 gets clicked regardless of relevance. Correct with a position-based propensity model and inverse propensity scoring, or collect unbiased data via randomized interleaving. Mentioning IPS marks you as someone who's done real relevance work.
48. Leadership Scenarios — Full Worked Answers
Each scenario below gives the structure of a strong answer, the actual words where wording matters, the trap, and the follow-up. These are the role-plays from §8.4, §23, and §25 with answers attached.
48.1 "Your strongest engineer is toxic in code review."
Structure: first move → information → the line.
"First move is a private conversation, quickly — within a day or two of noticing, not at the next 1:1 three weeks out, because every day it continues the team learns that it's acceptable.
Before that conversation I'd gather specifics. 'You're being harsh' is unactionable. I'd have two or three actual review comments and the effect they had — a junior who stopped opening PRs, or a change that got reverted because nobody wanted to push back. SBI: situation, behavior, impact.
In the conversation I'd assume good intent and say so, because usually there is some: strong engineers who are harsh in review are often protecting quality and don't see the cost. I'd say something like: 'Your review standards are one of the reasons this codebase is good, and I don't want to lose that. But the way three of these landed made people less likely to ship, and that costs us more than the bugs you're catching. Here's what I'd like to change.'
Then I'd make it concrete rather than a vibe request: adopt the comment taxonomy —
blocking:/suggestion:/nit:— so severity is explicit instead of inferred from tone; require that a blocking comment proposes an alternative; and move style entirely to automation so those comments stop existing.The line I wouldn't cross: if it continued after clear feedback, it stops being a coaching problem and becomes a performance problem, and I'd say that explicitly at the second conversation rather than letting it drift. Being technically excellent doesn't exempt someone from how they affect the team, and if I let it, I've told everyone else that the rules are negotiable for strong performers."
The trap: framing it as "be nicer." That's unactionable and reads as a personality critique. Concrete process changes give the person a way to succeed.
Follow-up — "What if they push back and say the juniors are just too sensitive?" → Move from opinion to data: review latency on their PRs, how often juniors request them as a reviewer, the pattern of who stopped contributing. And separate the two claims: the standard can be right and the delivery can be costly. You're not asking them to lower the bar.
48.2 "An engineer has missed three commitments in a row."
"I'd start from the assumption that I don't know why, because there are four very different causes and they have opposite fixes: they're blocked and not escalating; they're overloaded with invisible work; the estimates were unrealistic and I signed off on them; or it's a genuine capability or engagement problem. Diagnosing before acting is the whole game here.
The conversation opens with observation, not accusation: 'The last three commitments slipped. I want to understand what's happening — walk me through the last one.' Then I mostly listen.
If it's blockers — my failure. I own that I wasn't creating enough safety to escalate, and we set a rule that anything blocked more than a day comes to me.
If it's estimation — partly my failure too. We break work smaller until items are under a week and use historical cycle time rather than optimism.
If it's genuine underperformance, then I get very specific: what the expectation is, what the gap is, what support I'm providing, and by when I need to see change. In writing, so there's no ambiguity. And I'd say plainly that this is a performance conversation, because the cruellest thing a manager does is deliver a soft signal and then act on a hard one.
The principle underneath all of it: no surprises at review time. If this ends up in a performance plan in three months, it should be the fourth conversation about it, not the first."
Follow-up — "They say the work is boring." → That's useful information, not an excuse. Boredom in a strong engineer is a retention risk with a fix; boredom as a reason for missed commitments is still a commitment problem. Address both, separately: renegotiate what they work on, and keep the reliability expectation.
48.3 "A PM promises a date to a customer without asking you."
"Two problems, and I'd deliberately separate them: the immediate date, and the process that produced it. Solving only the first guarantees a repeat.
On the date — I don't lead with 'that's impossible.' I lead with options and costs: 'Here's what we can deliver by that date, here's what it would take to deliver all of it, and here's what we'd be trading.' Usually a reduced scope that meets the customer's actual need exists, and finding it is more useful than being right about process.
On the process — that's a separate, calm conversation, not in front of the customer or the team: 'I want to make sure you can commit to dates confidently. To do that you need me in the room before the number goes out. What would make that easy?' Framing it as helping them be more credible works far better than 'don't do that again.'
If it repeats after that, I'd escalate — but with data, not grievance: the specific commitments, what they cost, and a proposed working agreement. And I'd bring my director in as a peer conversation with the PM's manager, not as a complaint."
The trap: protecting the team by being obstructive. You'll win the battle and lose the partnership, and the PM will start routing around you.
48.4 "Your director wants a date you can't commit to."
"I never answer with a flat no, and I never give a date I don't believe. Both destroy credibility, just on different timelines.
What I give is a distribution and its drivers: 'Based on our cycle time over the last quarter, I'm 50% confident in October 15th and 85% confident in November 5th. The variance is dominated by two things: the vendor integration, which I don't control, and whether we get the second backend engineer. If we descope the reporting module, the 85% date moves to October 20th.'
That does three things at once: it's honest, it's actionable — the director now has levers — and it demonstrates I'm forecasting rather than guessing.
If they need the earlier date regardless, then it's an explicit trade conversation: scope, quality, or people. I'd put the options in writing with what each costs, so the decision is made with open eyes and there's a record of what we agreed. And then I'd commit to whatever we choose without relitigating it.
The one thing I hold firm on is not committing to something I believe is impossible, because the cost lands later and larger — on the team's trust and on the director's own credibility upward."
Follow-up — "They say 'I need you to make it work.'" → "I will make the best version of this work, and I'll tell you now what I think we'll be trading, so you're not surprised in six weeks." Commit to effort and honesty, not to arithmetic you don't believe.
48.5 "Two teams are building the same service. You have no authority over either."
"Influence problem, so I start with understanding rather than a proposal, because arriving with a solution to a problem I haven't diagnosed is how you get politely ignored.
I'd talk to both leads separately and find out what each is actually solving. Often they're not building the same thing — they're building two things that look similar from outside and differ in a requirement that matters. If that's true, the answer isn't consolidation, it's a clear boundary and a shared interface, and I've saved everyone a fight.
If it is genuine duplication, I'd write it up: what each system does, where they overlap, what the duplication costs in maintenance and cognitive load, and two or three options — one team owns it and the other consumes; extract a shared library; or keep both with an explicit boundary and a sunset date for one. Options with costs, not a verdict. Nobody adopts a conclusion they weren't part of reaching.
Then I'd get the two leads in a room with the document and let them react. My goal is for one of them to propose the answer, because a decision they own survives and a decision I impose gets quietly ignored.
If they can't agree, I escalate — and escalation is a tool, not a failure. I'd bring both leads and the written options to the shared manager, framed as 'we've narrowed to two paths and need a decision-maker,' not as a complaint about the other team. DACI is worth naming here: the problem is usually that no one is the named approver."
48.6 "Review queue depth doubled after the AI tooling rollout."
"Expected, and it's the specific failure mode I'd have been watching for — AI moves the bottleneck from writing code to reviewing it, and the industry data shows throughput up and change failure rate up together.
I'd measure before acting: time-to-first-review, merge time, PR size distribution, and change failure rate split by AI-assisted and not. My hypothesis is that PR size grew, because it's suddenly cheap to generate 800 lines.
Then the interventions, roughly in order of leverage:
- PR size limits. Under ~400 lines. This is the single biggest lever, because review quality collapses past that and large PRs get rubber-stamped — which is the actual risk, not the queue depth.
- Author accountability. The rule I'd set explicitly: you must be able to explain every line you submit as if you wrote it. AI assistance doesn't transfer authorship of the understanding.
- Raise the automated bar so humans review design and correctness, not mechanics: coverage requirements on new code, stricter static analysis, contract tests.
- Review SLA and reviewer rotation so the load doesn't concentrate on two seniors, which is where it always lands.
- Label AI-assisted PRs and track their change failure rate separately — that's the number that tells you whether the speed is real.
What I'd tell my director: 'We're getting the velocity gain, and it's showing up as review debt. Here's the instrumentation and here's what I'm changing. I'd expect merge time to normalize in six weeks and I'll report the change failure rate split either way.' Naming the risk before it becomes an incident is the whole job."
48.7 "Make the case for 25% reliability investment to a product-focused VP."
"I'd never argue this on engineering aesthetics — 'we need to pay down tech debt' loses every time to a feature with a revenue number attached. I'd translate it into their units.
Three moves:
- Cost of the status quo, quantified. 'We spent 340 engineer-hours on incidents last quarter — that's 1.5 engineers of capacity we didn't spend on roadmap. Change failure rate is 18%, so roughly one in five releases costs us a rollback and a re-do.'
- Tie it to a business metric they already own. 'Our p99 on checkout is 2.4 seconds. Every 100 ms historically correlates with X% conversion on our funnel. This isn't reliability work, it's revenue work.'
- Make it bounded and measurable. Not 'we need 25% forever' — 'I want 25% for two quarters, targeting change failure rate under 8% and incident hours under 100. If we hit that, it drops to 15% steady-state. If we don't, you should question my plan.' An open-ended ask is easy to refuse; a bounded one with a success criterion is a bet.
And I'd offer the error budget framing as the durable mechanism: when we're within budget we ship fast, when we've burned it we fix reliability. That converts a recurring argument into a policy we agreed on in advance, and it's much easier to get agreement in calm times than during an incident."
48.8 "An engineer wants promotion; they're one level of scope short."
"The worst thing I can do is be vague, because vagueness reads as 'no' without giving them anything to act on, and that's how you lose good people.
I'd be direct and specific: 'I think you'll get there, and I don't think the packet would succeed this cycle. Here's the gap.' Then name it concretely against the rubric — usually at this boundary it's scope and influence, not technical skill: they're executing excellently on work someone else defined, and the next level requires defining it and pulling others along.
Then the part that matters: I own half of the gap. If they haven't had cross-team scope, that's partly because I haven't assigned it. So I'd leave the conversation with one or two specific opportunities — own the migration that touches three teams, run the design review process, mentor two engineers through their own growth — chosen because they generate the exact evidence the packet needs.
And a timeline with a checkpoint: 'Let's look again at the end of Q1. Here's what I'd expect to be able to write about you by then.'
The thing I'd avoid is the false promise — 'do these things and you'll be promoted' — because calibration involves other people and I don't control the outcome. What I can promise is that I'll advocate with specific evidence, and that I'll tell them early if I think it's not going to land."
48.9 "You inherit a team with low morale after a reorg."
"First 30 days is listening, and I'd say that to the team explicitly so my lack of decisions doesn't read as absence.
1:1 with everyone in week one, with three questions: what's working, what's broken, and what do you want that you're not getting. I'd take notes and look for the pattern — after eight conversations, the two or three real problems are obvious, and they're rarely what leadership thinks they are.
Then I'd pick one visible, achievable fix and ship it inside the first month. Not the biggest problem — the one I can definitely deliver. Morale after a reorg is fundamentally a belief problem: people don't believe things will improve. One delivered fix buys more credibility than the best strategy document.
In parallel I'd be honest about what I can't change. Reorgs usually come with decisions that are final, and pretending otherwise burns trust when reality arrives. 'That decision isn't reversible. Here's what I can affect, and here's what I'll push on.'
Around day 60 I'd share back what I heard, what I'm doing about it, and what I'm not — closing the loop is what turns a listening tour into something other than theater. The failure mode of listening tours is that people tell you things and nothing visibly happens, which is worse than not asking."
48.10 "You've been down-leveled in an offer."
"First, separate the emotion from the decision — the down-level might be right, or it might be an evidence problem.
I'd ask the recruiter a specific question, not a general one: 'What signal was missing for the higher level?' Usually the answer falls into one of three buckets: I demonstrated execution but not scope; a specific round went badly; or the team's level need is genuinely lower and it's not about me.
If it's an evidence problem, I'd ask whether additional signal can be considered — a conversation with a hiring manager focused on cross-org work, or a written artifact. That request is normal and is sometimes granted; the worst outcome is a polite no.
If the level is real, then it's a straightforward decision about the actual job: is the scope interesting, is the trajectory credible, and is there a written commitment on a level review timeline? A verbal 'you'll get promoted quickly' is worth nothing; a documented review at six months with named criteria is worth a lot.
And I'd negotiate level before compensation, because the band follows the level and arguing dollars inside the wrong band is fighting the wrong fight."
49. Complete Drill Bank — Answer Key
Compressed answers to all 66 drills. If you can expand any of these to two minutes with an example from your own work, you're ready.
AI/LLM (1–6)
- Prefill vs decode bottleneck — prefill processes all tokens in parallel (matrix-matrix, high arithmetic intensity → compute-bound); decode generates one token at a time (matrix-vector, must read all weights per token → memory-bandwidth-bound). §41.1.
- Confidently wrong RAG — bisect: did retrieval return the right chunk? If no → chunking, embedding, or query understanding. If yes → generation ignored it: check context position (lost in the middle), prompt grounding instructions, and context length. Separate retrieval metrics from generation metrics or you can't tell. §43.2.
- Fine-tune vs retrieval — fine-tune for form (tone, format, domain vocabulary, output structure) and for latency/cost at high volume; retrieve for facts (anything that changes, anything permissioned, anything needing citation). Never fine-tune to teach facts you'll need to update.
- 20-step agent eval — task-level success rate with programmatic checks, not per-step accuracy. Plus steps-to-completion, cost per successful task, human-intervention rate, unsafe-action rate. §44.3.
- Indirect prompt injection defense — least privilege first (the injected instruction has no capability to abuse), structural separation of data from instructions, egress allowlists, output validation, and breaking the lethal trifecta (private data + untrusted content + egress). §44.3.
- Cut LLM spend 60% — prompt caching → model routing with escalation → context trimming (often improves quality) → semantic caching (gated) → output length limits → batch API → distillation. §41.6.
Context/Graph (7–9) 7. 3,000-token prompt of business rules — that's knowledge in the wrong place. Move it to retrievable context with governance, keep the prompt for interaction design. §16.1. 8. Graph over vector — multi-hop relational questions, explanation/auditability requirements, entity-centric domains. Not for lookup-shaped questions over churning corpora. §16.5. 9. 128k context budget — explicit allocation: system + tools + retrieved + history + output reserve. Critical instructions at start and end. Compact history into structured state, isolate sub-agents. §16.3.
Streaming/Reactive/APIs (10–15)
10. Kafka exactly-once — idempotent producer (PID + sequence) + transactions + read_committed. Covers Kafka-to-Kafka only; it does not make your database write or external API call exactly-once. §17.1.
11. flatMap vs concatMap — flatMap interleaves, unbounded concurrency by default, order not preserved; concatMap serializes, preserves order. Use concatMap when order matters, flatMapSequential for concurrency with ordered emission. §17.2.
12. Virtual threads vs WebFlux — for a new service on JDK 21+, virtual threads give the scalability with blocking-style code and far lower complexity. Reactive still wins for genuine streaming with backpressure across a network boundary. Watch pinning on synchronized. §17.2.
13. GraphQL N+1 — DataLoader batches within a request tick and caches per request. Caching doesn't solve it because the problem is per-request resolver fan-out, not repeated identical queries across requests. §17.3.
14. L4 LB breaks gRPC — gRPC multiplexes many requests over one long-lived HTTP/2 connection; an L4 balancer balances connections, so all requests pin to one backend. Fix: L7 balancing, client-side LB, or a service mesh. §17.4.
15. Watermarks — they answer "have I seen everything for this window yet?" in event time. max_event_time − allowed_lateness. Enables windows to close and results to emit despite out-of-order arrival. §17.1.
Caching/Edge (16–18)
16. Cache stampede — request coalescing (single-flight), probabilistic early expiration, jittered TTLs, lock-and-refresh with stale-while-revalidate. §18.4.
17. Complex-dependency invalidation — surrogate keys / cache tags: tag responses with the entities they depend on, purge by tag. §18.4.
18. SSE vs WebSocket for tokens — SSE: unidirectional is all you need, works with existing HTTP infra and auth, built-in reconnect with Last-Event-ID. WebSocket adds bidirectional capability you don't use and infrastructure friction you don't want. §18.2.
Compute/K8s/OS (19–23)
19. Healthy but slow pod — CPU throttling (CFS quota) → check container_cpu_cfs_throttled_seconds; then GC pauses; then DNS (ndots:5); then noisy neighbor; then a slow dependency. Order by cost to verify. §19.3.
20. Removing CPU limits helps latency — limits enforce a CFS quota; a bursty service hits the quota and gets throttled hard even when the node is idle. Requests guarantee the floor; limits cap the ceiling you often want to exceed briefly. §19.3.
21. Serverless vs containers math — serverless wins below ~30–40% steady utilization; always-on wins above. Compute GB-seconds vs instance-hours and include ops cost. §19.1.
22. fsync and durability — a write returning means it's in the page cache, not on disk. fsync forces it to stable storage. Databases fsync the WAL before acknowledging a commit; skipping it trades durability for throughput. §19.4.
23. eBPF for intermittent latency — attach to syscall tracepoints and kernel functions to histogram latency without instrumenting the app: bpftrace for one-off histograms, bcc tools (biolatency, runqlat, tcpconnlat) for standard questions. Zero-instrumentation, production-safe. §19.4.
Storage (24–28)
24. DynamoDB table for 5 access patterns — enumerate patterns first, then design PK/SK to satisfy the primary ones with begins_with queries, GSIs (sparse where possible) for the rest. §46.3.
25. Cassandra range query started timing out — tombstones. Deletes accumulate as markers; a range scan reads them all. Check tombstone warnings, the workload pattern (queue-shaped?), and compaction strategy. §46.1.
26. Mongo shard key — high cardinality, even write distribution, and present in your common queries. Avoid monotonic keys (hot shard). Hashed for distribution, ranged for range queries — you rarely get both. §20.3.
27. Zero-downtime reindex — build the new index alongside, backfill with refresh_interval: -1, dual-write, verify with a diff, atomically swap the alias, keep the old for rollback. §20.4.
28. Postgres write skew — REPEATABLE READ is snapshot isolation and permits it. Two transactions read overlapping data, write disjoint rows, jointly violate an invariant. Fix with SERIALIZABLE (plus retry logic) or SELECT ... FOR UPDATE to materialize the conflict. §40.3.
Security (29–33)
29. PKCE for confidential clients — defends against authorization-code interception even where a secret exists; OAuth 2.1 makes it mandatory. Defense in depth costs nothing. §21.1.
30. Revocation with stateless JWTs — short TTLs (5–15 min) plus refresh rotation; a revocation list checked at refresh; or a jti/version claim checked against a fast store for high-value operations. Accept a bounded window or you've reinvented sessions. §21.1.
31. Critical CVE, first 4 hours — inventory (do we run it, where, what version), exposure (internet-facing? reachable code path?), mitigate (WAF rule, feature flag, network policy), patch, verify, communicate. Prioritize by KEV/EPSS × exposure, not CVSS alone. §21.4.
32. Zero trust in 60 seconds — verify every request explicitly regardless of network location; least privilege; assume breach. Sequencing: identity → device → workload → network → data. §21.2.
33. CVSS vs EPSS — CVSS scores theoretical severity; EPSS predicts real exploitation probability; CISA KEV lists what's actively exploited. Patch KEV first, then EPSS × exposure. §21.4.
Frontend (34–36) 34. Redux holding server data — migrate server state to TanStack Query (caching, staleness, invalidation, optimistic updates are its job), keep Redux for genuine UI state. Usually deletes half the store. §22.2. 35. Bad INP — long tasks blocking the main thread on interaction. Profile with the Performance panel; break up long tasks, defer non-critical work, virtualize lists, reduce re-render cascades from context. §22.3. 36. Micro-frontends worth it — independent teams needing independent deploy cadence at real scale. Otherwise the coordination and bundle costs exceed the benefit. §22.3.
Leadership (37–42) — see §48 for full answers.
Architecture & delivery (43–52) 43. Burn-rate alerts — alert on budget consumption rate, multi-window multi-burn-rate (e.g. 14.4× over 1h AND 5m to page). Long window confirms significance, short window confirms it's current. §28.1. 44. Prometheus cardinality explosion — an unbounded label (user ID, request ID). Fix at the Collector: label allowlists, drop/bucket high-cardinality dimensions, route those questions to traces. §28.2. 45. Testing 40 microservices — consumer-driven contract testing (Pact) in CI, plus schema breaking-change detection (buf/OpenAPI diff). Full-environment integration suites don't scale. §30.3. 46. Deploy vs release — deploy moves bits, release exposes users; decoupled by feature flags. Lets you deploy continuously and release deliberately. §30.4. 47. Split a 15-person team — by cognitive load and stream alignment (Team Topologies), not by technical layer. Layer-based teams force every feature through three backlogs. §29.3. 48. Modular monolith vs extraction — default to the monolith with enforced module boundaries; extract when a specific force demands it (independent scaling, independent deploy cadence for separate teams, fault isolation, regulatory separation). §29.2. 49. Experiment won target, tripped latency guardrail — don't ship. Guardrails exist to prevent exactly this trade. Investigate whether the latency cost is inherent or fixable, then re-run. §30.5. 50. "Can't lose data, can't be down" — extract RTO and RPO as numbers, then show the cost curve: backup/restore → pilot light → warm standby → active-active. The business picks a point on it. §32. 51. Chaos-test an untested assumption — state the steady-state hypothesis, inject the specific fault (AZ loss, dependency latency), measure blast radius against the hypothesis, with an abort switch. §28.3. 52. Down-leveled — §48.10.
Patterns & contracts (53–60) 53. Repository, then argue against it — it decouples the domain from persistence and enables in-memory testing; but a generic repository over a capable ORM adds a layer without decoupling (ORM semantics leak through). §42.5. 54. CQRS adoption ladder — Level 0–4; place the team, and note that most need Level 1–2 and adopt Level 4. §42.1. 55. Vendor 99.5%, we sell 99.9% — you've promised what you can't structurally deliver. Options: redundancy (second vendor, failover), graceful degradation that keeps you up without them, caching to survive their outages, or renegotiate the SLA. §36. 56. Genuine unknown — state the boundary, reason from adjacent knowledge, name how you'd find out. Never bluff; never stop at "I don't know." §38.2. 57. 90 seconds with the CTO — ask about strategy, not the team: how engineering shows up in company strategy, or the under-appreciated bet. §37.5. 58. SCOR → STAR — Situation+Complication → S; chosen option and execution → T/A (compress alternatives to one sentence); Result + reflection → R. §36. 59. Red flags to probe — pick three from §37.9 and use §37.3's peer questions to surface them without adversarial framing. 60. Scar-tissue story in a caching answer — 30 seconds, attached to a technical point: the cache that hid a correctness bug for months because the stale path was never exercised.
Deployment (61–66) 61. Canary vs A/B confusion — canary asks "is it safe" (operational metrics, minutes, asymmetric rollback); A/B asks "is it better" (product metrics, weeks, power analysis). Every A/B variant rides through canary first. §39.4. 62. Canary schedule at 200 QPS — at 1% you're getting 2 QPS, which detects only gross failures. Start at 10%, extend bake time, and accept that subtle regressions need the full rollout plus strong monitoring. Compute the minimum detectable effect and say what you can't catch. §39.3. 63. Shadow-testing a service that sends email — mirror reads freely; for side effects, either route to a sandbox provider, suppress at an outbound gate keyed on the shadow flag, or make the shadow path structurally incapable of egress. Never rely on the code "knowing" it's a shadow. §39.1. 64. Rollback broke on cache format — the process failure was not versioning the cache key with the serialization schema, so the old version read new-format entries. Fix: schema-versioned cache keys and rollback testing as part of the release checklist. §39.5. 65. Mobile crash at 40% rollout — halt the staged rollout immediately (store-level), flip the server-side feature flag off to disable the code path for already-updated clients, then diagnose. Server-side kill switches are why mobile releases need flags. §39.2. 66. Rename a column across a 3-version window — expand: add the new column, dual-write both. Migrate: backfill, switch reads to the new column, deploy through all versions. Contract: stop writing the old, verify no readers, drop. Each phase is independently deployable and rollback-safe. §39.5.
50. Closing Note on Using This Document
Reading it once is not the point. Three passes, three purposes:
- Pass one — inventory. Skim every section heading and mark anything you couldn't explain to a peer for two minutes. That's your study list. Everything else is maintenance.
- Pass two — production. Work the marked sections with the drills (§25, §49). Say the answers out loud. Write the code snippets from memory. An answer you've only read is not an answer you have.
- Pass three — retrieval. In the last two weeks, use only §31 (frameworks), §37 (questions to ask), §49 (drill answers), and your own 16 stories. That's the night-before layer.
The document's failure mode is comprehensiveness creating the illusion of preparation. The only real signal is whether you can produce the answer under time pressure, out loud, to a skeptical stranger. Everything here is in service of that, and none of it substitutes for the mocks.
Document complete: Parts I–V. Part I–IV are the map and the mechanics; Part V is the expanded technical and leadership reference with worked answers, code, production case studies, and primary sources.
51. Gap Register & Expansion Queue
An honest per-section audit against the target quality bar (definition → mechanics → code/example → production case study → tradeoff debate → follow-up Q&A → references). This doubles as the work queue for the Claude Code build described in the companion prompt file.
Depth legend: ✅ at target · 🟡 partial (insight present, treatment thin) · 🔴 outline only (terms named, not taught)
| § | Topic | Depth | Highest-priority missing pieces |
|---|---|---|---|
| 15 | AI/GenAI/LLM foundations | 🟡 | Transformer walkthrough with shapes; RoPE/ALiBi explained not named; fine-tuning ladder with a worked LoRA example; sampling strategies with output demonstrations |
| 16 | Context/prompt/graph eng. | 🔴 | A full context-budget worked example; a GraphRAG build walkthrough with real extraction prompts; entity-resolution treatment; LangGraph state-machine code |
| 17 | Streaming/reactive/APIs | 🔴 | Kafka exactly-once end-to-end walkthrough with producer/consumer code; a Reactor pipeline debugged step-by-step; DGS resolver + DataLoader full example; gRPC service with deadline propagation shown; saga-vs-2PC debate expanded |
| 18 | CDN/caching/real-time | 🔴 | WebSocket backplane implementation sketch; cache-stampede code (singleflight); a real Cache-Control decision walkthrough per asset class; CDN purge strategy worked example |
| 19 | Compute/K8s/kernel | 🔴 | CFS throttling demonstrated with numbers; a full pod-debugging transcript; namespace/cgroup hands-on commands; io_uring vs epoll explained with a benchmark; JVM-in-container sizing walkthrough |
| 20 | Storage systems | 🟡 | §46 covers engines; still missing: Cassandra data-modeling worked example (query-first), Mongo aggregation walkthrough, Iceberg table lifecycle demo, Spanner/CockroachDB read-write transaction trace |
| 21 | Security | 🔴 | Full OAuth authz-code+PKCE sequence diagram with every parameter explained; a JWT validation implementation with each check justified; STRIDE threat model worked on a real system; SSRF exploitation-and-defense walkthrough; Zanzibar/ReBAC model explained with tuples |
| 22 | Frontend | 🔴 | Reconciliation walkthrough; RSC boundary worked example; TanStack Query migration before/after; INP debugging transcript |
| 23 | Leadership playbook | 🟡 | §48 covers 10 scenarios; remaining: skill-matrix template filled in; a real capacity-planning spreadsheet walkthrough; calibration-meeting simulated transcript; 1:1 agenda examples |
| 24 | AI cross-cutting | 🟡 | Per-layer claims need one concrete example each; the six leadership questions need full §48-style answers |
| 28 | SRE/observability | 🟡 | OTel Collector config walkthrough; a burn-rate alert in PromQL; a chaos game-day runbook; k6 script with open-model load |
| 29 | DDD/Team Topologies | 🟡 | Event-storming walkthrough on a real domain; context-map worked example; an inverse-Conway reorg case study |
| 30 | Delivery engineering | 🟡 | A Pact contract test end-to-end; flag lifecycle code; a power-analysis calculation shown |
| 31 | Frameworks card | ✅ | Complete for its purpose |
| 32 | DR | 🟡 | A full DR runbook example; failback reconciliation walkthrough |
| 33–38 | Offer/questions/seasonality | ✅/🟡 | Largely complete; add 2–3 more worked negotiation dialogues |
| 39 | Deployment | 🟡 | ACA scoring worked example; an Argo Rollouts manifest annotated |
| 45 | Classic designs | 🟡 | Each needs expansion from key-insight to §43-length transcript (10 designs × ~1,500 words) |
| 46–47 | Storage/retrieval expanded | 🟡 | LTR feature-engineering worked example; a training-data pipeline for judgments; ScaNN/DiskANN mechanics |
| 49 | Drill answer key | 🟡 | Each 3-sentence answer needs a 2-minute spoken-form version |
Estimated remaining work at the target bar: ~450–700k words. This is a book build, and it belongs in the Claude Code loop described in claude-code-reference-build-prompt.md, not in further chat appends.
PART VI — THE ENGINEERING MANAGEMENT WORKSHOP
Built to the universal quality standard: every topic defined, motivated, shown mechanically, grounded in a worked example, proven with named companies, argued with alternatives, and closed with follow-ups answered several ways. This part is the one most candidates are weakest on, because it's the part you can't learn from LeetCode.
52. Team Architecture & Organizational Design
52.1 The Spotify Model — and why knowing it failed is the real signal
What it claims to be. A 2012 whitepaper by agile coaches Henrik Kniberg and Anders Ivarsson, Scaling Agile @ Spotify, describing four structures:
| Unit | Definition | Analogous to |
|---|---|---|
| Squad | Small cross-functional autonomous team owning a feature area end to end; has a Product Owner and a long-term mission | A Scrum team / stream-aligned team |
| Tribe | A collection of squads (<100 people, per Dunbar) working in a related area, led by a Tribe Lead | A department |
| Chapter | People with the same skill within a tribe (all backend engineers), led by a Chapter Lead who is their line manager | A functional discipline |
| Guild | A cross-organization community of interest (security, testing) — voluntary, no reporting lines | A community of practice |
The two intended axes: alignment (leaders set the problem) and autonomy (squads choose the solution). "Aligned autonomy."
Why this is on the exam. Because it's the most-copied org model in software, and the seasoned answer is that it never actually worked — including at Spotify.
The evidence, which you should be able to cite precisely:
- Kniberg published a post titled "No, I didn't invent the Spotify model" (2015), explaining the practices emerged from many people and were never designed as a transferable framework. By 2016 he was telling audiences directly not to copy it, on the grounds that Spotify's way of working changed constantly, so any copy would be a copy of something that no longer existed.
- Former Spotify employee Jeremiah Lee published Failed #SquadGoals (2020) after joining post-growth: the famed squad model was only ever aspirational and never fully implemented, and he watched leadership incrementally transition to more traditional management structures as the company tripled to ~3,000 people in 18 months.
- The whitepaper was a snapshot of one company's practices at one moment, not a model. Other companies — Zalando, Typeform, BT among those who've spoken publicly — tried it and pulled back.
The four specific failure modes (this is the content, not the trivia):
- Matrix without decision rights. The Product Owner owned the "what," the Chapter Lead owned the "how." When those conflict — and they always do at the point where scope meets technical approach — the model specifies no arbiter. In practice this produced informal power struggles the model never addressed.
- Autonomy without alignment produced fragmentation. Squads chose their own frameworks, deployment practices, and monitoring. The result: an inconsistent codebase, high maintenance burden, and engineers unable to move between squads — which destroys the flexibility autonomy was supposed to buy.
- It assumed no dependencies, and dependencies exist. The model worked passably for feature development, where squads had clean boundaries, and badly for platform work, where a change affects everyone. Declaring teams autonomous does not make the dependency graph go away.
- It scaled coaching, not capability. Spotify didn't have enough agile coaches for every team, and many teams lacked the underlying practice knowledge to make autonomy productive. Autonomy handed to a team without the capability to use it is abandonment.
What to actually keep. The ideas are sound; the packaging was the problem. Strip the vocabulary and you get: cross-functional teams owning a durable mission, functional communities for craft development, and explicit tension management between alignment and autonomy. Those are Team Topologies' stream-aligned teams and communities of practice, with better-defined interaction modes and an explicit cognitive-load constraint (§29.3). Team Topologies is the more rigorous descendant, and preferring it is the defensible position.
How to answer when asked "what do you think of the Spotify model?" — three framings, pick by interviewer:
To a hiring manager (concise, positioned): "I'd be cautious. The model was a 2012 snapshot that Spotify itself never fully implemented and moved away from — Kniberg, who documented it, spent years telling people not to copy it. The underlying ideas are good: cross-functional teams with durable missions and functional communities for craft. I'd rather reach for Team Topologies, which gives you the same benefits with explicit interaction modes and a cognitive-load constraint that the squad model lacked."
To a director probing org-design judgment (analytical): "The interesting thing about the Spotify model isn't the structure, it's the failure mode — it's a case study in what happens when you grant autonomy without alignment mechanisms. Squads picked their own tooling, the codebase fragmented, and engineers couldn't move between teams, which was the exact flexibility the structure was meant to create. And the matrix never named a decision-maker when the PO's 'what' collided with the chapter lead's 'how.' So when I look at any org design, the two questions I ask are: where does a disagreement get resolved, and what's holding the technical choices coherent across teams."
To a peer engineer (candid, shorter): "It's a cargo cult. Spotify doesn't use it and arguably never did. The good parts survive in Team Topologies with better definitions."
Follow-up: "So how would you structure a 40-person org?" → Start from the value streams, not the org chart: what are the 4–6 durable slices of customer value? Make those stream-aligned teams of 6–9, each owning its slice end to end including on-call. Add a platform team only when the stream teams' cognitive load is demonstrably dominated by infrastructure — and treat it as a product with adoption metrics, not a mandate. Use enabling teams temporarily for capability gaps. Then apply the inverse Conway maneuver: if you want a modular architecture, you must have modular teams first, because the architecture will mirror the org whether you plan it or not.
Follow-up: "What if leadership has already mandated squads and tribes?" → Use the vocabulary, fix the mechanics. You don't win by fighting the nouns. Quietly install what the model omits: a named decision-maker per squad (DACI), a technical standards baseline that squads must meet (paved road, not golden cage), and an explicit dependency register with owners. The seasoned move is making a flawed structure work rather than demanding a re-org.
Further reading: Kniberg & Ivarsson, Scaling Agile @ Spotify (2012); Kniberg, No, I didn't invent the Spotify model (2015); Jeremiah Lee, Failed #SquadGoals (2020); Skelton & Pais, Team Topologies (2019).
52.2 The alternatives, and what each is actually optimizing
| Model | Core unit | Optimizes for | Fails when | Named practitioner |
|---|---|---|---|---|
| Two-pizza teams + single-threaded leader | Team small enough to feed with two pizzas; one leader whose only job is that initiative | Ownership clarity and speed; removing the "everyone's job is nobody's job" failure | Coordination costs across many small teams; duplicated effort | Amazon |
| Team Topologies | Stream-aligned / platform / enabling / complicated-subsystem, with three interaction modes | Bounded cognitive load and explicit inter-team contracts | Requires genuine platform investment to work as designed | Widely adopted post-2019 |
| Handbook-first, DRI-based | Documented ownership per decision; async by default | Remote scale, transparency, low meeting load | Requires real writing discipline; slow if writing is weak | GitLab |
| Context not control | Highly senior individuals given context, not process | Speed with exceptional people; minimal process overhead | Breaks with less-experienced staff; opaque to outsiders | Netflix |
| Shape Up | 6-week cycles, fixed time / variable scope, appetite instead of estimates | Avoiding estimation theater and endless backlogs | Needs real autonomy over scope; poor fit for committed roadmaps | 37signals/Basecamp |
| Feature crews | Temporary cross-functional team formed per feature, disbanded after | Focus on one outcome | Loses long-term ownership; on-call orphans | Microsoft (historically, DevDiv) |
The staff-level insight to state: none of these are interchangeable, because each is solving that company's specific bottleneck. Amazon's constraint was coordination overhead at scale — hence single-threaded ownership. GitLab's was being all-remote across every timezone — hence handbook-first. Netflix's was that they hire only very senior people — hence minimal process. Copying a structure without inheriting the constraint it solved is the single most common org-design mistake, and the Spotify model is its most famous instance.
The one universal: Conway's law. Your architecture will mirror your communication structure. If you need three teams to ship one feature, that's an architecture problem wearing an org costume, and vice versa.
52.3 Microservices, cloud native, and the org consequences
The claim interviewers are testing: do you understand that microservices is an organizational decision with technical consequences, not the reverse?
Definition, precisely. Microservices: independently deployable services, each owned by one team, communicating over a network, each with its own data store. The load-bearing words are independently deployable and its own data store — remove either and you have a distributed monolith, which has all the costs and none of the benefits.
Why it exists. Not for scale — a monolith scales horizontally fine. It exists to let many teams deploy independently without coordinating releases. That's the actual benefit. If you have three teams, you probably don't have that problem.
The organizational preconditions, which is the real answer:
- One service per team, not one team per many services. A team owning 15 microservices has a distributed monolith with extra YAML.
- Each team owns the service in production, including on-call. Without this, you've split the code and centralized the pain.
- A platform capable of supporting independent deploys: CI/CD per service, service discovery, observability, and a paved road. Without a platform team, microservices means every team reinvents deployment — a real, common, expensive failure.
- Team size ≥ 4 per service for a sustainable on-call rotation. This alone caps how many services a 40-person org can responsibly own.
The 2026 position to hold. The pendulum has swung. The defensible default is a modular monolith with enforced internal boundaries — module APIs, no cross-module database access, independently testable — extracting a service only when a specific force demands it: independent deploy cadence for separate teams, independent scaling of a genuinely different load profile, fault isolation for a critical path, polyglot requirements, or regulatory separation. Amazon's own Prime Video team published a widely-discussed case of consolidating a serverless microservice architecture back into a monolith for a 90% cost reduction; the lesson isn't "microservices bad," it's that the right granularity is workload-specific.
Cloud native, defined without buzzwords. Applications designed for elastic, unreliable, API-driven infrastructure: containerized, dynamically orchestrated, stateless where possible, configuration externalized, observable by default, and designed to survive individual instance loss. The CNCF definition adds the practices — declarative APIs, immutable infrastructure, service meshes. The 12-factor app is the concise checklist; know that it predates secret managers and treat its config guidance accordingly.
Follow-up: "When would you NOT go cloud native?" → Predictable steady-state load where reserved instances beat elasticity economically; workloads with hard hardware dependencies (GPU topology, specialized NICs, licensed appliances); regulated environments where the operational surface of Kubernetes is itself a compliance burden; and small teams for whom the operational complexity exceeds the benefit. "We run three services on ECS Fargate and don't need Kubernetes" is a mature answer, not a naive one.
53. Sizing, Estimation & Assigning Work
53.1 The techniques, mechanically
Planning Poker. Descended from Wideband Delphi; named by James Grenning (2002) and popularized by Mike Cohn.
Mechanics:
1. PM/DRI reads the item and answers clarifying questions
2. Everyone selects a card SIMULTANEOUSLY and reveals together
← the simultaneity is the entire point: it prevents anchoring
on the loudest or most senior voice
3. High and low estimators explain their reasoning
← this is where the VALUE is. The number is a byproduct;
the discovered disagreement is the product
4. Re-estimate. Converge or timebox to two rounds and take the higher.
Scale: modified Fibonacci — 1, 2, 3, 5, 8, 13, 20, 40, 100, ?, ∞
Why non-linear: precision is impossible at scale, so the scale
forces coarseness. You cannot argue 7 vs 8.
8+ means "break it down." 20+ means "we don't understand it yet;
spike it."
When it earns its time: new teams, unfamiliar domains, high-uncertainty work, and any item where you suspect the team has different mental models. When it's waste: a mature team on familiar work, where the meeting costs more than the information produced.
T-shirt sizing (XS/S/M/L/XL). Coarse, fast, for epics and roadmap-level items where numbers imply false precision. Its real virtue: nobody tries to sum t-shirts into a commitment.
Affinity estimation. For estimating a large backlog fast (50–200 items in an hour):
1. Print/card every item. Place one reference item on a wall.
2. SILENT sorting: everyone places items left (smaller) to right
(larger) relative to what's already there. No talking.
3. Talking phase: anyone may move an item once and must say why.
4. Draw size boundaries across the resulting spectrum, assign values.
The silent phase is what makes it fast and anchor-resistant. Best tool for an initial backlog or a re-baseline.
GitLab's issue weights — a real, publicly documented alternative worth citing. GitLab doesn't use story points; it uses weights on issues, and the process is explicit: engineering managers assign an engineer to break the work down and apply weights, in collaboration with the DRI, before the milestone starts. Weighing at the front of the build track is what lets product managers make prioritization tradeoffs and lets the team confirm they've scoped the right amount for a milestone. The scheduling decision and the estimation decision are deliberately coupled.
Shape Up's appetite — the inversion worth knowing. Instead of "how long will this take," ask "how much time is this worth?" Set a fixed appetite (2 weeks or 6 weeks), then shape the solution to fit. Scope is the variable, time is fixed. This eliminates estimation entirely, and it works only if the team has genuine authority to cut scope — which is why it fits product companies with autonomous teams and fits poorly where scope is contractually committed.
Forecasting from throughput (the modern alternative). Don't estimate; measure.
Take the last 8-12 weeks of completed items. Two numbers:
- THROUGHPUT: items completed per week
- CYCLE TIME distribution: percentiles, not the mean
Forecast by Monte Carlo: sample historical throughput 10,000 times
to simulate finishing N remaining items.
Output: "85% confident we finish 30 items in 7 weeks or less."
Why this beats story points: it uses observed reality rather than
predicted effort, it produces a probability rather than a date, and
it cannot be gamed by inflating estimates.
Precondition: items must be roughly similar in size — which you get
free from the "break down anything over a week" rule.
The #NoEstimates argument, stated fairly: if you slice work to a consistent small size, counting items forecasts as well as estimating them, and you save all the estimation effort. The counter: many organizations genuinely need a date for external commitments, and "we'll tell you when it's done" isn't an answer to a customer contract. The synthesis, and the position I'd take: estimate coarsely for prioritization, forecast from throughput for dates.
53.2 The Goodhart problem — the seasoned critique you must be able to deliver
Story points fail predictably, and knowing the failure mechanism separates a lead from someone repeating a certification:
- Points get converted to hours by management. The moment someone computes "our velocity is 40 points = 40 points of capacity," points have become time with extra steps, and their whole purpose (decoupling estimate from duration) is dead.
- Velocity becomes a target. Goodhart's law: when a measure becomes a target, it ceases to be a good measure. Teams inflate estimates, and velocity rises with zero change in delivered value. The number goes up and nothing improves — this is the tell.
- Cross-team comparison. Points are calibrated per team and meaningless across teams. Comparing them is the most common misuse and the fastest way to destroy their honesty.
- They measure effort, not value. A 13-point item that nobody uses is worse than a 1-point item that fixes a top complaint.
The line worth having ready: "I use estimates to have the conversation, not to have the number. If our estimation practice isn't surfacing disagreements about scope and approach, it's ceremony and I'd stop doing it."
53.3 Breaking down an epic — a full worked example
This is the skill interviewers probe with "how would you break this down," and most candidates answer at a level of generality that proves nothing. Do it concretely.
The epic: "Add saved searches to the platform."
Step 1 — Slice vertically, never horizontally.
WRONG (horizontal — layer by layer):
□ Database schema for saved searches
□ Backend API for saved searches
□ Frontend UI for saved searches
□ Tests
Why it's wrong: nothing is shippable until all four are done. No
feedback, no partial value, and the integration risk lands at the end
when there's no time left.
RIGHT (vertical — thin slices through every layer):
□ 1. User can save the current search with a name and see it in a list
(schema + endpoint + minimal UI, no editing, no sharing) [MVC]
□ 2. User can run a saved search from the list
□ 3. User can rename and delete a saved search
□ 4. Saved searches sync across devices
□ 5. User can share a saved search with their team
□ 6. Email alert when a saved search has new results
Each slice is independently shippable and independently valuable.
Step 2 — Apply INVEST to each slice. Independent, Negotiable, Valuable, Estimable, Small, Testable. Slice 1 passes. Slice 6 is not Small (email infrastructure, scheduling, unsubscribe handling, digest logic) and gets broken further.
Step 3 — Find the MVC. GitLab's framing — the Minimal Valuable Change — is the useful discipline: what is the smallest change that still delivers value? Slice 1 is it. GitLab's own guidance is that engineers should push back toward smaller scope at any point in the lifecycle when they can see an opportunity to cut, because engineers understand what edge cases cost. That's the sentence to internalize: cutting scope is an engineering responsibility, not just a product one.
Step 4 — Identify risk and sequence for learning. Which slice teaches us the most about whether this is right? Ship slice 1 to 5% of users and instrument it. If nobody saves a search, slices 2–6 shouldn't be built.
Step 5 — Now estimate. Weight or point the slices. Anything over a week goes back to step 1.
Step 6 — Assign deliberately (§53.4).
Follow-up: "The PM insists all six slices ship together as one release." → Separate deploy from release (§30.4). Build and merge the slices incrementally behind a feature flag, release them together if the product needs a single launch moment. You get incremental integration and incremental review; they get the launch. Nobody has to lose.
53.4 Assigning work — the part nobody teaches
Assignment is your highest-leverage recurring decision and most leads make it by availability. The four inputs:
- Growth. Does this stretch someone toward their next level? Target ~70% familiar / 30% new. More than that and you're setting up failure; less and you're wasting a development opportunity.
- Risk. Critical-path work with a hard date goes to someone who has done something similar. You do not learn on the migration that must land in three weeks.
- Bus factor. If one person has done all the payments work for two years, the next payments task goes to someone else with them pairing. Deliberate knowledge distribution is a lead responsibility that never appears in a sprint plan.
- Glue work distribution. The coordination, documentation, onboarding, and incident follow-up that makes teams function accretes on whoever is most conscientious — and it's non-promotable. Tanya Reilly's framing. See it, name it, rotate it, and value it in calibration. Volunteering this concept unprompted is one of the strongest people-leadership signals available.
The mechanism: maintain a skill matrix — each engineer × each competency (domain areas, systems design, code quality, communication, ownership, mentoring), scored 1–4. Review it quarterly. Assign against the gaps, not the calendar.
Search Ingest Infra SysDesign Mentoring On-call
Alex (Sr) 4 2 3 3 2 4
Priya (Mid) 2 4 2 2 1 3
Sam (Jr) 1 2 1 1 1 2
Jo (Staff) 3 3 4 4 3 4
Reads immediately:
• Search is a bus-factor-1 risk on Alex → pair Priya on the next
search item even though Alex is faster
• Sam needs a scoped ownership opportunity, not more tasks
• Jo should be mentoring, not writing the ingest code they'd write
fastest — that's the classic staff-engineer misallocation
• Nobody is growing toward Alex's on-call depth except Jo
Follow-up, asked several ways — "How do you decide who works on what?"
The concise version (screen): "Deliberately, against a skill matrix — not by who's free. Every assignment is either a growth investment or a risk decision, and I try to know which one I'm making."
The version showing depth (hiring manager): "Four inputs: growth — is this 70/30 familiar-to-new for them; risk — does the deadline allow learning; bus factor — am I concentrating knowledge further; and glue-work fairness — the coordination and onboarding work that's invisible and non-promotable tends to land on the same conscientious person unless I rotate it. I keep a skill matrix per person per competency so those decisions are visible rather than vibes, and I review it quarterly."
The version with a story (director): "I had a team where one engineer owned all of the search relevance work for two years. He was the fastest at it every single time, so every search ticket went to him — which was locally optimal and a bus-factor-one disaster. When he took a month of parental leave we couldn't ship a relevance change. After that I started pairing someone on every search item even when it was slower, and I made the skill matrix explicit so I couldn't kid myself. The lesson I took is that assignment by who's fastest is a decision to concentrate risk, and it compounds silently."
54. Jira & Azure DevOps — Making the Tool Tell the Truth
The framing that earns respect: the tool is not the process. A tracker's only jobs are (1) making work visible, (2) making flow measurable, and (3) preserving decisions. Everything else teams do in Jira is usually ceremony that costs more than it returns. Lead with that and you sound like someone who has cleaned up a Jira instance rather than someone who has filled one.
54.1 Hierarchy and hygiene (both tools)
Jira Azure DevOps
───────────────────── ──────────────────────────
Initiative (Premium) Epic
Epic Feature
Story / Bug / Task User Story / Bug
Sub-task Task
Rules that keep it honest:
• An EPIC is an outcome, not a bucket. "Saved searches" is an epic;
"Backend work" is a bucket and it will never close.
• A STORY is a vertical slice with user-visible value, ≤ 1 week.
• A TASK is an implementation step, only when the story genuinely
needs decomposition. Most don't — sub-tasks are where teams
manufacture busywork.
• A BUG carries reproduction steps, expected vs actual, and severity.
If it doesn't, it's a rumor, not a bug.
• If an item has been open 90 days, it's not backlog, it's a decision
you haven't made. Close it or schedule it.
Definition of Ready (before pulling into a sprint): acceptance criteria written; dependencies identified; sized; no known blockers; the team can explain it. Definition of Done (before closing): merged; tests written and passing; deployed to production or behind a flag; observability in place; docs updated; PM has seen it. Put DoD in the board configuration — Azure DevOps supports it per-column on the Kanban board, and Jira teams should put it in the column description. An unwritten DoD means every engineer applies a different one.
54.2 Configuration practices that actually matter
Jira:
- Resist workflow customization. The most common Jira disease is a 14-state workflow with mandatory transition screens. Every state must answer "who acts differently because of this state?" If nobody does, delete it. Five to seven states is plenty: Backlog → Ready → In Progress → In Review → Done (+ Blocked as a flag, not a status).
- Components vs labels: components are curated and ownable (they can auto-assign); labels are free-text and rot within a quarter. Use components for anything you'll report on.
- WIP limits on board columns. The single highest-value board setting and the one almost nobody enables. It makes the queue visible and forces finishing over starting.
- Automation rules worth having: transition to In Progress when a branch is created; transition to In Review when a PR opens; comment on the issue when the PR merges; flag any issue with no update in 5 days; auto-assign by component.
- The three reports that matter: Control Chart (cycle time distribution over time — this is your forecasting input), Cumulative Flow Diagram (where work is piling up — a widening band is a bottleneck), and the Sprint Burndown only if you're doing sprints and only as a within-sprint signal, never as a performance metric.
- Don't use Jira as a document store. Decisions belong in a design doc or ADR with a link from the issue. Jira comments are unsearchable archaeology within six months.
Azure DevOps:
- Area Paths = ownership; Iteration Paths = time. Getting this wrong is the classic ADO mistake. Area paths map to teams and components and should mirror your org; iteration paths are sprints and are shared. A team's board is defined by its area path.
- Link commits and PRs to work items with
AB#123in the commit message or PR description. This gives you automatic traceability and is what makes ADO's release-notes generation and audit story work. Enforce it with a branch policy requiring linked work items. - Branch policies are ADO's strongest feature: required reviewers by path (so the security team must approve
/auth/**), minimum approver count, build validation, comment resolution required, and merge strategy enforcement. Configure these once and code review discipline becomes structural rather than cultural. - Queries (WIQL) and Delivery Plans: saved queries for "my team's items with no activity in 5 days" and "bugs with severity 1 unassigned" are worth more than any dashboard. Delivery Plans give cross-team timeline visibility, which is the one thing ADO does better than Jira out of the box.
- Dashboards: cycle time widget, lead time widget, burndown, and a query tile for stale items. Four tiles. Not twenty.
54.3 The metrics to read, and the ones to refuse
| Read this | Because | Never use this as a target |
|---|---|---|
| Cycle time distribution (85th percentile) | It's your forecasting input and it reflects reality | Average cycle time — hides the tail that actually hurts |
| Cumulative flow diagram | Shows exactly where work queues | — |
| Work item age (in-progress items) | Catches stuck work while it's stuck, not after | — |
| Throughput (items/week) | Forecasting input, un-gameable | Velocity as a target — Goodhart guarantees inflation |
| Blocked time | The lead's actual job (§23.1) | — |
| Review queue depth / time-to-first-review | Usually the largest hidden cost in cycle time | — |
| DORA four keys | Delivery system health | Any of these compared across teams — DORA's own guidance warns against it |
| — | — | Individual commit counts, lines of code, points per person — actively destructive |
The sentence that demonstrates seasoning: "I read the cumulative flow diagram before the burndown, because the burndown tells me whether we'll hit the sprint and the CFD tells me why."
Follow-up asked three ways — "How do you know your team is productive?"
Concise: "I look at flow, not output. Cycle time at the 85th percentile, throughput, and where work is queueing. Output metrics like velocity or commits tell you how busy people are, which isn't the question."
With the tension named: "Three layers. Delivery health from DORA — deployment frequency, lead time, change failure rate, recovery time. Flow health from cycle time and CFD. And human health from DevEx signals — interruptions, cognitive load, whether people feel they can do good work. I need all three, because you can hit great DORA numbers by burning a team out, and that shows up two quarters later as attrition. I'd also flag that DORA alone has become less reliable in AI-heavy teams — throughput rises while stability degrades, so I pair it with a quality guardrail."
With a story: "At one point our velocity was up 30% quarter over quarter and I was pleased with myself until I looked at cycle time, which hadn't moved. What had actually happened was estimate inflation — we'd started sizing everything one Fibonacci step higher after a rough quarter. The number went up and nothing improved. That's when I stopped reporting velocity upward entirely and switched to throughput and cycle time percentiles, which can't be gamed by estimating differently."
55. The GitLab Workshop — Learning from a Company That Published Everything
GitLab's handbook is the largest public corpus on how to actually run an engineering organization — over 2,000 pages, open to the world, and changed by merge request like code. For a team lead interview, it's the highest-density study material available, because you can read the actual policies rather than a conference talk's summary.
55.1 Handbook-first, and why it's a management technique
The practice: if it isn't in the handbook, it isn't policy. Changes go through a merge request — proposed, reviewed, merged — so process changes have an author, a reviewer, a diff, and a history.
Why this works, mechanically:
- It removes the meeting. A question answered in the handbook is answered once, for everyone, forever. A question answered in a meeting is answered for eight people and lost.
- It makes process changes reviewable. You can disagree with a diff. You cannot disagree with a hallway convention.
- It scales across timezones. In an all-remote company, synchronous is the expensive resource. Written is the default.
- It exposes inconsistency. Two contradictory policies are visible when both are written.
How to use this in an interview — this is the transferable version, and it works at any company:
"I run a written-first team. Decisions land in an ADR or a design doc, process lives in the team's README, and changes to either go through review the same way code does. The test I use is: if someone joins next month, can they find out how we work without asking anyone? GitLab's handbook is the extreme version of this and it's worth reading — over two thousand pages, public, and changed by merge request. Most teams don't need that scale, but the principle scales down well."
55.2 The DRI — Directly Responsible Individual
Definition: one named person accountable for a decision or initiative. Not a committee, not a role, a person.
Why it exists: the failure mode it prevents is the decision that everyone discusses and nobody makes. GitLab applies DRIs at every level — a DRI for an epic, a DRI for a handbook page, a DRI for a large-scale initiative — and their product development flow explicitly describes the DRI collaborating with engineering managers and collaborators to break down and weight work.
The nuance that shows understanding: a DRI is not a dictator. They gather input, and they're accountable for making the call and documenting it. This is the same structure as DACI's single Approver (§29.4). The value isn't authority, it's the elimination of ambiguity about who decides.
Transferable practice: on any project, name the DRI in writing at kickoff. When someone asks "who's deciding this?", the answer should already exist.
55.3 Iteration and the MVC
GitLab treats iteration as a company value, not a process, and the operational expression is the Minimal Valuable Change — the smallest change that still delivers value to a user.
The specific mechanics worth stealing:
- Smaller merge requests, always. GitLab's handbook is explicit that to deliver iteratively you must create smaller MRs.
- Engineers are expected to push scope down. The handbook states that engineers are a vital part of the feedback loop with product, because engineers understand what additional behaviors and edge cases cost — and should give that feedback as early as possible, at any point in the lifecycle, when they see an opportunity to cut scope and ship something smaller.
- Weighting happens up front in the build track, with the EM assigning an engineer to break down and weight the work in collaboration with the DRI, so that PMs can make prioritization tradeoffs with real information and the team can confirm the milestone is correctly scoped.
- GitLab runs iteration training for new team members and explicitly acknowledges that iteration doesn't come naturally to everyone and varies across cultures — which is a notably mature piece of organizational self-awareness.
The interview-usable version:
"The practice I'd bring is treating scope-cutting as an engineering responsibility, not just a product one. Engineers see what an edge case costs before the PM does, and the cheapest moment to cut scope is the moment you notice. GitLab formalizes this — they call the unit a Minimal Valuable Change and they expect engineers to push back toward smaller scope at any point in the lifecycle. On my team that looks like a standing question in refinement: what's the smallest version of this that a user would still thank us for?"
55.4 The rest of the transferable set
- Weights over story points. Sizing tied directly to the scheduling decision, applied by the engineer who'll do the work, in collaboration with the person accountable for the outcome.
- Milestones over sprints. Time-boxed release cadence rather than a ceremony-heavy sprint ritual.
- Everything is an issue or an MR. Discussion attaches to an artifact, so context is recoverable a year later.
- Async by default, meetings with agendas and documents. A meeting without a linked agenda document doesn't happen.
- Transparency as a default, with a narrow internal exception. Most companies invert this — internal by default, public by exception — and the inversion is what makes GitLab's handbook possible.
The honest critique to have ready, because an interviewer will probe: handbook-first requires strong writing across the whole organization, and it's slow when writing is weak. It can also produce documentation sprawl where the handbook contradicts itself at scale. And "if it's not in the handbook it's not policy" can become a weapon in disagreements. The transferable version is written-first for decisions and process, not necessarily 2,000 pages.
56. Google, Microsoft, Amazon & Netflix — What Each Proved
56.1 Google — Project Oxygen and Project Aristotle
Project Oxygen (2008 onward) asked what makes a great manager, and it began by testing the hypothesis that managers don't matter much. The data said they do, and the research distilled a set of behaviors into a Manager Feedback Survey where every item begins with "My manager…" — an upward-feedback instrument, rated by the team.
The behaviors (the list has been refined over time; the durable core):
- Is a good coach
- Empowers the team and does not micromanage
- Creates an inclusive team environment, showing concern for success and wellbeing
- Is productive and results-oriented
- Is a good communicator — listens and shares information
- Supports career development and discusses performance
- Has a clear vision and strategy for the team
- Has key technical skills to advise the team
- Collaborates across the company
- Is a strong decision-maker
The two findings that matter for an interview: technical skill ranks eighth, not first — it's necessary to advise credibly but it is not what distinguishes great managers. And coaching ranks first. If you're moving from senior IC to lead, the single most useful reframing available is that your technical depth is table stakes, and the differentiating skill is developing other people.
Project Aristotle (from 2012, led by Julia Rozovsky in People Analytics) asked what makes a team effective. The hypothesis was that the best teams are the best combination of people. The data said otherwise: who is on the team mattered less than how the team interacts. Five dynamics emerged, with psychological safety — the shared belief that you can take interpersonal risks, admit mistakes, ask questions, and disagree without humiliation — as the foundation the other four sit on:
- Psychological safety (foundational)
- Dependability — members reliably deliver
- Structure and clarity — clear roles, plans, goals
- Meaning — the work matters personally
- Impact — the work matters beyond the team
The mechanism a lead can act on, because "build psychological safety" is otherwise unactionable:
- Model fallibility first. Say what you got wrong, publicly, before asking anyone else to. The most-cited Aristotle anecdote is a Google manager who found his team disengaged, called an offsite, and disclosed his stage-4 cancer diagnosis — after which team members began sharing too, and the team's dynamics changed. The general principle isn't disclosure of that magnitude; it's that safety propagates downward from whoever has the most to lose by being vulnerable.
- Make speaking up structurally easy: rotate meeting facilitation, ask the most junior person first, use round-robins rather than open floors, run blameless postmortems and mean it.
- Respond well to bad news, every single time. One punished escalation teaches the whole team more than a year of stated values.
How to deploy this in an interview:
Direct: "Google's Project Aristotle found psychological safety was the strongest predictor of team effectiveness — stronger than who's on the team. And Project Oxygen found technical skill was eighth on the list of what makes a great manager. Both findings pointed me the same direction when I moved into leading: my technical depth gets me credibility, but it isn't the job."
Applied, when asked how you'd fix a struggling team: "I'd look at the Aristotle dynamics in order, because they build on each other. Is it safe to disagree here — do people raise problems, or do problems surface only after they've become incidents? Then dependability, then structure and clarity, which is usually where the tractable problems are: unclear ownership, no definition of done, ambiguous priorities. Meaning and impact are real but they're rarely the binding constraint. And I'd start by modeling it — the fastest way to make it safe to be wrong is to be visibly wrong myself first."
56.2 Amazon — the mechanisms
- Two-pizza teams: small enough to be fed by two pizzas. The point isn't the headcount, it's that communication overhead grows quadratically and small teams don't need coordination mechanisms.
- Single-threaded leader (STL) / single-threaded owner: one leader whose only responsibility is that initiative. Amazon's own diagnosis was that the biggest predictor of an initiative's success was whether someone owned it full-time. This is the most transferable Amazon mechanism — "who is single-threaded on this?" is a question worth asking about any struggling project.
- Working backwards / PR-FAQ: write the press release and FAQ before building. Forces clarity on the customer benefit before a line of code.
- Six-page narratives, no slides. Meetings start with 20 minutes of silent reading. Slides let a speaker hide fuzzy thinking behind bullets; prose does not. Whether or not you adopt it, having a position on written-narrative culture is a strong signal.
- Bar raiser: an interviewer from outside the hiring team with veto power, whose incentive is the long-term bar rather than filling this req.
56.3 Microsoft — engineering system and culture reset
- Abandoning stack ranking (2013) is the more instructive story than anything they added. Forced distribution made teammates competitors, and killing it was the precondition for the collaboration Nadella's growth-mindset culture required. If asked about performance management, "I don't believe in forced distribution, and Microsoft's reversal is the well-documented case" is a defensible, evidenced position.
- One Engineering System (1ES): the drive to a single shared toolchain across a company that had dozens. The lesson is platform-as-product: consolidation succeeds when the shared system is genuinely better, not when it's mandated.
- Feature crews and short sprint cadence in the developer division — cross-functional teams formed around a deliverable, shipping on a fixed rhythm.
- Growth mindset as an operating concept, not a poster: "learn-it-all rather than know-it-all." The interview-usable version is how you treat being wrong in design reviews.
56.4 Netflix — context, not control
- Context not control: give people the information and the strategy, not the process. Requires unusually senior staff, and Netflix is explicit that it's a consequence of their talent density, not a universal recipe.
- Informed captain: a named decision-maker per decision, expected to gather dissent and then decide. Same pattern as DRI and DACI's Approver — three companies independently converging on "name the decider" is itself the lesson.
- The keeper test: would you fight to keep this person? Honest, and genuinely harsh; know it, and have a view. Mine: the test is a useful private prompt for a manager and a corrosive thing to say out loud to a team, because it converts every ambiguity into a threat.
The synthesis worth stating when asked which model you'd adopt: "Every one of these solved the specific company's binding constraint. Amazon's was coordination overhead, so they optimized ownership. GitLab's was all-remote across every timezone, so they optimized written asynchrony. Netflix's was that they only hire very senior people, so they removed process. The mistake is copying the artifact without inheriting the constraint. What I'd actually take is the pattern all four share: name one accountable person, write decisions down, and keep teams small enough that they don't need a coordination layer."
57. Management Workshop — Interview Q&A, Multiple Framings
Each question with three answers: screen (concise, 30–45 seconds), depth (hiring manager, 2 minutes, shows mechanism), and story (director, shows scar tissue). Match the framing to the room.
"How do you structure a team?"
Screen: "From the value streams, not the org chart. Four to six durable slices of customer value, one team of six to nine on each, owning it end to end including on-call. Platform teams only when stream teams' cognitive load is genuinely dominated by infrastructure."
Depth: "I start with Team Topologies' framing: stream-aligned teams as the default, bounded by cognitive load rather than headcount. The constraint that actually matters is how many domains a team can hold in its collective head — past that you get slow onboarding, more defects, and burnout. Then interaction modes get explicit: which relationships are collaboration, which are X-as-a-service. And I'd use the inverse Conway maneuver deliberately — if I want a modular architecture, I need modular teams first, because the architecture will mirror the org whether or not I plan it. What I'd avoid is the Spotify model. It was a 2012 snapshot Spotify never fully implemented and moved away from, and its failure mode is instructive: autonomy without alignment, and a matrix with no named decision-maker."
Story: "I inherited a group split by layer — a frontend team, a backend team, and a data team. Every feature needed all three backlogs to align, so lead time was dominated by waiting, not working. We reorganized into two vertical teams each owning a product area top to bottom. Cycle time roughly halved and, more importantly, the arguments changed from 'whose priority is this' to 'what should we build.' The thing I got wrong: I underestimated how much the specialists would grieve losing their craft community, and I should have set up chapters or guilds from day one rather than bolting them on after people complained."
"How do you estimate work?"
Screen: "Coarsely for prioritization, and I forecast dates from throughput rather than from estimates. Estimation is for having the conversation; the historical cycle time is what I'd actually bet a date on."
Depth: "Three levels. Roadmap items get t-shirt sizes — numbers there imply a precision that doesn't exist. Sprint-level work gets planning poker or weights, and the value there is the disagreement it surfaces, not the number: when two engineers say 2 and 13, we've found a scope misunderstanding and that's worth the meeting. For dates, I use throughput and cycle time percentiles with a Monte Carlo forecast, so I can say '85% confident by November 5th' rather than a single date that's wrong by construction. And I break anything over a week down further, which conveniently makes the items uniform enough that counting works as well as estimating."
Story: "I ran a team where velocity climbed 30% over two quarters and I was quietly pleased until I checked cycle time, which was flat. We'd inflated our estimates after a bad quarter — the number went up and nothing had improved. Goodhart's law, exactly on schedule. I stopped reporting velocity upward and moved to throughput and 85th-percentile cycle time, which you can't game by estimating differently. It also changed the conversation with my director from 'are we going faster' to 'where is work actually waiting,' which turned out to be code review, not development."
"How do you break down a large epic?"
Screen: "Vertical slices, never horizontal layers. Each slice ships independently and delivers something a user would notice. If a slice is over a week, it isn't a slice yet."
Depth: (walk the saved-search example from §53.3 out loud — the six slices, the INVEST check, the MVC, sequencing for learning.) "And the sequencing matters as much as the slicing: I order by what teaches us the most, not by what's easiest. Ship slice one to five percent of users and instrument it — if nobody saves a search, slices two through six shouldn't exist."
Story: "We had a six-month replatforming epic broken down by layer — schema, then services, then UI. Four months in we had nothing shippable and no idea whether the design worked, because the integration risk was all parked at the end. We stopped, re-sliced vertically, and shipped a working thin path in three weeks. It was uglier than the plan and it told us two of our assumptions were wrong, which we'd have discovered in month six otherwise. Since then my rule is that if the plan has no shippable increment in the first three weeks, the plan is wrong."
"How do you make sure work gets done as expected?"
Screen: "Definition of Ready and Definition of Done written down, WIP limits on the board, and a daily scan for anything blocked or aging. Most missed expectations are blocked work nobody escalated, not slow work."
Depth: "Three mechanisms. First, clarity before start — acceptance criteria and a written DoD, because unwritten DoD means every engineer applies a different one. Second, flow visibility — WIP limits and work-item age on the board, so I catch stuck work while it's stuck rather than in retro. Third, blocker triage classified by type: blocked on information, on a decision, on a dependency, or on skill. Each has a different fix and treating them the same is why teams stay stuck. I also track decision latency on myself — if a decision has been sitting with me for two days, that's my failure, not theirs."
Story: "I used to think our delivery problem was estimation. Then I put work-item age on the board and found the median in-progress item sat untouched for four days waiting on review, on a two-day development task. The bottleneck wasn't building, it was queueing — and no amount of better estimating would have found it. We set a four-hour first-review SLA and rotated reviewers, and cycle time dropped by a third without anyone working differently."
"How do you grow the people on your team?"
Screen: "Deliberate assignment against a skill matrix, stretch work at roughly 70% familiar and 30% new, and growth plans mapped to the next level's rubric with named artifacts rather than adjectives."
Depth: "I keep a matrix of each person against each competency, and I assign work to close gaps rather than by who's free. I use a delegation ladder explicitly — do it, do it and tell me, propose then do, decide and inform, own it — and I tell people which rung they're on, because the ambiguity is what makes delegation feel like either abandonment or micromanagement. Feedback is SBI and timely. And for promotion I start two quarters early by assigning work that generates evidence at the target level, because waiting for packet season is how good engineers get stuck. Google's Project Oxygen found coaching was the top behavior of effective managers and technical skill was eighth — that reordering matched my experience moving into leadership."
Story: "I had a strong senior who wanted staff and was one level of scope short — excellent execution, no cross-team influence. My first instinct was to tell him to 'be more strategic,' which is useless advice. Instead I gave him ownership of a migration that touched three teams and deliberately stayed out of the coordination. He hated the first month. Six months later he'd run the design review, negotiated the sequencing with two other leads, and had the artifacts the packet needed. What I learned is that the gap is usually my assignment failure, not their capability failure — they'd never been given work that could generate the evidence."
"How do you handle a team that's resistant to a process change?"
Screen: "Find out why. Resistance is usually accurate information about a cost I haven't accounted for. I'd rather run a bounded experiment with a review date than mandate."
Depth: "I treat it as a design problem, not a compliance problem. First, is the change solving a problem the team actually has? If they don't feel the pain, the change is solving my problem and I should say so honestly rather than pretending. Second, I'd propose it as a time-boxed trial — two sprints, with an explicit success measure and a genuine agreement to revert. That converts a debate about opinions into an experiment with data. Third, I'd give the loudest skeptic a role in designing it, because opposition converted to ownership is worth more than consensus. What I wouldn't do is mandate and hope, because a process people follow reluctantly degrades into ceremony within a quarter."
Story: "I tried to introduce a PR-size limit and got real pushback — people said it would slow them down and add overhead. Rather than argue I asked them to help me measure it: we tracked review latency against PR size for a month. The data was stark — anything over about 400 lines sat in review three times longer and got a fraction of the comments. The team adopted the limit themselves after seeing it, and one of the original skeptics wrote the tooling to warn on oversized PRs. The lesson I took is that I'd been trying to win an argument when I should have been designing a measurement."
"What's the hardest part of being a team lead?"
Screen: "Letting work be done worse than I'd do it, because that's how anyone else gets better. The instinct to take the keyboard is the thing to unlearn."
Depth: "Two things. The first is that my output became invisible — I stopped shipping and started being measured by what other people ship, and the feedback loop went from hours to months. That's genuinely disorienting and it's where new leads either learn to trust process signals or start micromanaging to feel productive. The second is that the hard calls are almost never technical. Telling someone they're not ready for promotion, or that their behavior is costing the team, is harder than any system design, and doing it late is worse than doing it badly."
Story: "The hardest thing I've done was tell a friend on my team that his performance had slipped, in the same conversation where I had to say the word 'formal.' I'd delayed it about two months telling myself I was being kind, and in that time he'd had no idea anything was wrong. When I finally said it, his first reaction wasn't anger, it was 'why didn't you tell me earlier?' — which was completely fair and is the thing I still think about. Now my rule is that if I'm rehearsing a conversation in my head for more than a day, that's the signal to have it, and no one on my team should ever be surprised at review time."
58. Workshop Drills (67–86)
- Structure a 40-person org from scratch. Walk your reasoning and name the model you're not using.
- Your leadership mandated squads and tribes. What do you fix quietly?
- Argue for and against microservices for a 25-person org shipping one product.
- When is a modular monolith the right answer? What force triggers extraction?
- Define cloud native without buzzwords. When would you not go cloud native?
- Run planning poker on an item where two engineers say 2 and 13. What do you do next?
- Estimate a 200-item backlog in an hour. Method and mechanics.
- Forecast a date for 30 remaining items. Show your method and phrase the output.
- Argue against story points to a team that loves them.
- Break down "add saved searches" into vertical slices. Name the MVC.
- Your PM wants all six slices released together. Reconcile it.
- Who gets the critical-path migration and why? Who gets the greenfield service?
- What is glue work, why does it matter, and how do you distribute it?
- Design a Jira workflow with the fewest states that still tells the truth.
- Area Paths vs Iteration Paths in ADO — what breaks if you conflate them?
- Which three tracker reports do you read weekly, and what does each tell you?
- What did Project Aristotle find, and what would you do differently on Monday because of it?
- Technical skill ranked eighth in Project Oxygen. What do you make of that?
- What's transferable from GitLab's handbook to a company that isn't remote?
- Amazon's single-threaded leader, GitLab's DRI, Netflix's informed captain — what's the common finding, and what does it tell you?
PART VII — ANSWERS TO EVERYTHING LEFT OPEN
Parts I–VI named things. This part answers them. Every question posed to an interviewer gets what you're listening for; every requirement stated as a bullet gets the mechanism, the reasoning, and the words to say.
59. Questions to Ask — What You're Listening For
A question is only half the tool. The other half is knowing what a good answer sounds like, what a concerning answer reveals, and what you do next. Asking a sharp question and then nodding at a vague answer wastes the question. For each below: why it works, the green flag, the red flag, your follow-up, and what you do with the information.
59.1 To the hiring manager
"What's the problem in your org that made you open this req — what breaks or stays broken if it goes unfilled for six months?"
Why: The job description is marketing. This asks for the pain, and the pain is the actual job.
Green flag: A specific, bounded answer with consequences. "Our search relevance work has been owned by one staff engineer who's moving to another team. If we don't fill this, three quarters of roadmap work stalls and we lose the institutional knowledge." They've thought about it, they can name the gap, and the scope is real.
Red flag: "We're just growing and always need good people." Either they haven't thought about it, or the role has no defined mandate — which means you'll spend your first six months inventing one while being measured against an expectation nobody wrote down. Also concerning: an answer that describes a person rather than a problem ("we need someone senior") — that's a headcount req, not a role.
Your follow-up: "Who's holding that today, and what happens to them when I arrive?" You're checking whether you're being hired into a vacuum or on top of someone who wanted the job.
What you do with it: This answer is the raw material for your 30/60/90 plan (§11) and for the "what does success look like" alignment in your first week. Write it down verbatim.
"Walk me through the team: tenure, levels, who's strongest at what, and where the gaps are."
Why: Tests whether they know their people, and gives you the real inheritance.
Green flag: They can do it without notes, name specific strengths, and are candid about a gap. "Priya's my strongest systems person, been here three years; Sam joined six months ago and is still ramping; we have nobody strong on the data pipeline side and that's my biggest risk." A manager who knows their team this well is a manager you'll learn from.
Red flag: Vagueness, or only positives. "They're all great" means either they don't know their team or they won't tell you the truth — and both predict a rough first quarter. A second red flag: heavy tenure skew, either everyone under a year (churn) or everyone over five (stagnation and possible resistance to a new lead).
Your follow-up: "Who on the team wanted this role?" Ask it directly. If someone did, you need to know before day one, and how the manager handles the question tells you a lot.
What you do with it: Build a mental skill matrix before you start (§53.4). Also identifies who you should ask to speak with during the loop.
"What did the last person in this seat struggle with?"
Why: The failure mode is usually structural and will be waiting for you too.
Green flag: Specific and self-implicating. "He was very strong technically but never built a relationship with product, so he found out about roadmap changes late and burned out fighting them. I should have coached that earlier." A manager who names their own contribution is one who'll tell you the truth later.
Red flag: Blame with no reflection ("he just wasn't a fit"), or "there was no last person, this is new." The second isn't disqualifying but it means no path has been walked — expect to define the role yourself.
Your follow-up: "What would you do differently in supporting the next person?" This is the question that gets the useful answer.
What you do with it: This is your top risk. Whatever they name, plan explicitly for it in your first 90 days.
"How much of my time do you expect in code, in design, and in people work — at month three and at month twelve?"
Why: This is where TL-vs-EM ambiguity kills careers. A role sold as "technical leadership" that's actually 80% people management, or vice versa, is the single most common mis-hire at this level.
Green flag: Different numbers for the two timeframes, with reasoning. "Month three I'd want you 50% hands-on because you need credibility and context. By month twelve, closer to 20% code, 40% design and technical direction, 40% people — and if you're still at 50% code at twelve months, something's wrong with how we've staffed the team."
Red flag: "Whatever you think is right" — sounds empowering, is actually an absence of a mandate, and you'll be judged against an unstated expectation. Or numbers that don't add to a real job ("you'll be hands-on 80% and also manage five people").
Your follow-up: "And how does the performance conversation work for this role — am I evaluated on what I ship or what the team ships?" The answer to that is the real job description.
What you do with it: If the answer conflicts with what you want, this is the moment to say so. Both of you are better off.
"What's your operating rhythm with your leads — 1:1 cadence, what you want escalated, how you deliver hard feedback?"
Why: You'll spend more time managing this relationship than any other. Compatibility here determines whether the job is sustainable.
Green flag: They have an answer because they've thought about how they manage. "Weekly 1:1s, yours not mine — bring the agenda. Escalate anything that'll slip a committed date or involves another director, and escalate early rather than when you've exhausted options. On feedback, I'll tell you directly and privately within a day or two; I don't save things for reviews."
Red flag: No cadence, or "my door is always open" as the whole answer — that's an absence of structure dressed as accessibility. Also: "I'm pretty hands-off" from a director who's never managed a lead before can mean abandonment.
Your follow-up: "When was the last time one of your leads pushed back on you, and what happened?" This is the highest-signal follow-up in the whole set.
What you do with it: Compare against how you actually work. A weekly-1:1 director and a "ping me when you need me" director are different jobs.
"Where do you and your manager currently disagree about this team's direction?"
Why: It's a bold question and it's fair. You're asking whether they have air cover and whether strategy is settled.
Green flag: A real, specific answer delivered without anxiety. "She wants us to prioritize the enterprise features; I think we have a reliability problem we need to fix first. We haven't resolved it — that's part of why I want someone strong in this role." That's honesty and it tells you exactly what you're walking into.
Red flag: "We're fully aligned." Either untrue, or the manager has no independent view. Both are bad — a lead reporting to someone with no position of their own gets whipsawed by every change from above. Also watch for visible discomfort at the question itself: that tells you disagreement isn't safe in this org.
Your follow-up: "How do disagreements like that usually get resolved here?"
What you do with it: You now know the political weather and can position your first-90-day plan on the side of whichever priority actually gets funded.
59.2 To the director / skip-level
"How do you like to be disagreed with?"
Why: You will disagree with this person. Asking the mechanics up front makes the first real disagreement survivable, and asking it at all signals that you expect to disagree — which is itself a staff-level posture.
Green flag: A specific mechanism, not a platitude. "Bring me the data and an alternative, not just an objection. In a group setting, push back in the room — I'd rather have it out in front of people than get a Slack message afterward. And if I've decided and you still disagree, tell me once more privately, then commit." That's someone who has thought about it and can be worked with.
Red flag: "I love being challenged!" with no mechanism behind it. Everyone says this; the people who mean it can describe how. Follow up hard on this one. Another red flag: an answer that's entirely about channel and never about substance ("just book time with me").
Your follow-up: "Can you give me an example of a time someone changed your mind?" This is the question that separates the real answer from the performance. If they can name a specific instance — what they thought, who pushed, what changed — the earlier answer was true. If they can't produce one, treat "I love being challenged" as decoration.
What you do with it: Calibrate your first disagreement deliberately. Use the channel they described, with the evidence they asked for, on something that matters but isn't existential. You're testing the answer at low stakes.
"What's your escalation threshold — when do you want me to bring you in?"
Why: Both failure modes are expensive. Escalating too early reads as not owning the job; escalating too late means they hear about a problem from someone else, which damages trust permanently. Nobody tells you where the line is unless you ask.
Green flag: Named categories with a bias stated. "Bring me anything that will slip a date we've committed externally, anything involving another director's team where you've already tried once, anything with legal or security implications, and anything about a person's employment. On everything else, decide and tell me in our 1:1. And I'd rather you escalate too early than too late — I'd rather hear it from you than from my peer." That last clause is the tell of a good manager.
Red flag: "Use your judgment" as a complete answer — that's the setup for being criticized later for a judgment call they'd have made differently. Also concerning: a threshold so low it's micromanagement ("check with me before any architectural decision"), or a director who says "never, I trust you completely" — which sounds great and means you'll be alone when something goes wrong.
Your follow-up: "And what's the thing you'd be annoyed to find out about late?" People answer this more honestly than the general version, because it's concrete and slightly negative.
What you do with it: Write the categories down and use them literally for the first quarter. After that you'll have calibrated instinct.
"What did the last person who succeeded in a role like this do differently?"
Why: Success at this level is context-dependent and the pattern is rarely written anywhere. The director has watched several people try; they know the differentiator even if they've never articulated it.
Green flag: A behavioral, specific answer. "The one who really worked spent her first two months just talking to people — engineers, support, sales — before proposing anything. Everyone else came in with a plan in week two and it never fit. And she wrote things down; when there was a disagreement six months later, there was a document." That's a playbook handed to you.
Red flag: Only technical attributes ("she was a really strong engineer") — that suggests the org doesn't distinguish leadership from IC excellence, and the role may not be a real leadership role. Or a generic answer ("hard work, good communication") meaning they haven't observed closely.
Your follow-up: "And the person who struggled — what did that look like?" The negative version often gets the more specific answer, because failures are memorable.
What you do with it: This is close to being handed the rubric. Structure your first 90 days around whatever they describe, and reference it explicitly in your follow-up note.
"What are you accountable for this year that this team materially affects?"
Why: It maps your work to their incentives. Once you know what their number is, you know which of your proposals will get funded.
Green flag: A specific metric or commitment, and a clear line from your team to it. "I've committed to cutting enterprise churn by three points, and the top-cited reason in exit interviews is search quality — which is this team."
Red flag: Vagueness, or an answer that reveals the team isn't connected to anything the director owns. That means low priority, low headcount, and low protection during a reorg. It's not disqualifying, but you should know it.
Your follow-up: "What would have to be true at the end of the year for you to feel this team over-delivered?"
What you do with it: Frame every subsequent proposal in these terms. Reliability investment framed as churn reduction gets funded; framed as tech debt it doesn't (§48.7).
"When this team missed or slipped in the past year, what was the real cause — and what changed afterward?"
Why: Tests whether the org learns. The "what changed" half is the important half.
Green flag: A causal story with a structural fix. "We committed to a date based on an estimate from before we understood the vendor integration. What changed is we now do a spike before committing to anything with an external dependency."
Red flag: Blame directed at individuals or another team, or a cause with no subsequent change — that's an org where postmortems are theater and the same slip is coming again on your watch.
Your follow-up: "Did that change stick?"
"What behavior gets people promoted here in practice — and what behavior gets tolerated that you wish didn't?"
Why: Two-part question doing real work. The gap between the answers is the culture.
Green flag: Honest on both halves, and the gap is small. "Promotions go to people who take on ambiguous cross-team problems and finish them. What I tolerate that I shouldn't is a couple of senior people who are dismissive in design reviews — I've addressed it but not hard enough." That's a director with self-awareness who'll be honest with you.
Red flag: Refusal to answer the second half, or a large gap between stated values and tolerated behavior. If heroics get promoted and burnout gets tolerated, you now know the job.
Your follow-up: None needed. Let the silence sit; people often add to this one unprompted.
"If I'm sitting here in a year and this hire clearly worked, what happened? And what's the most likely way it fails?"
Why: Forces a concrete success definition and, more valuably, gets them to name the risk they're already worried about.
Green flag: Both halves answered specifically. The failure half is the gift: "The most likely failure is you spend a year building the perfect platform and product feels you didn't help them ship." Now you know exactly what to guard against.
Red flag: Only the success half answered, or a failure mode that's entirely about you ("if you're not technical enough") with no acknowledgment of organizational risk.
What you do with it: Address the named failure mode directly in your close and in your follow-up note. It's the objection you now get to pre-empt.
59.3 To peer engineers and future reports
"Walk me through your last production incident — how did it go, and did the postmortem action items actually happen?" Green: A specific incident, a blameless account, and at least one action item they can point to as shipped. Red: Can't remember one (either nothing breaks — unlikely — or incidents aren't discussed), or action items that "usually get done" with no example. Follow-up: "Who ran it?" — tells you whether incident command is a role or a scramble.
"How long does a one-line change take to reach production, end to end?" Green: They know the number. Under a day is excellent, under a week is normal, and knowing it at all means the team measures flow. Red: "It depends" with no range, or a number in weeks with no discomfort about it. Follow-up: "Where does most of that time go?" — the answer is almost always review or environment queueing, and it tells you your first improvement target.
"What's the piece of the codebase everyone avoids, and why is it still like that?" Green: Named without hesitation, with a real reason (no owner, no tests, high risk, one person understands it). Every codebase has one; a team that can name it has healthy self-awareness. Red: "Nothing really" — either not candid with a candidate, or not looking. Follow-up: "What would it take to fix?" — tests whether improvement feels possible here.
"When you disagreed with a technical direction recently, what happened?" Green: A concrete instance where disagreement was heard, whether or not they won. Red: Long pause, or "you just do what you're told." That's the psychological-safety answer (§56.1) and it's the most important thing you'll learn all day.
"What would you fix first if you had a month of unscheduled time?" Green: Immediate and specific — they've been thinking about it, which means they care. Red: No answer, which usually means disengagement rather than contentment.
To future reports — "What do you want from your next lead that you're not getting today?" Green: Specific and actionable — career conversations, faster decisions, shielding from interruptions. Red: "Nothing, things are fine" from someone whose manager just left. Probe gently once, then let it go. What you do with it: This is your first-90-days list, and asking it during the loop means you arrive already knowing.
59.4 To the VP / CTO
"How does engineering show up in company strategy — cost center, product differentiator, or the product itself?" Green: A clear, honest answer with evidence, including an unflattering one delivered honestly ("we're a cost center to the CFO and I'm working on that"). Red: A flattering answer contradicted by everything else you heard in the loop — compare against headcount trends and whether engineering leadership is in strategy conversations.
"What's the bet the company is making that you think is under-appreciated?" Green: Genuine enthusiasm and specifics. This question makes executives talk, and how they talk about strategy tells you whether there is one. Red: Generic market language with no specific bet named.
"How is the org thinking about AI structurally — product capability, productivity layer, or both — and who owns that call?" Green: A named owner and a stated position, including "we've deliberately not made that call yet, and here's why." Red: Enthusiasm without ownership — which means the AI work is unassigned and will land on whoever volunteers, possibly you, without a mandate.
"What would make you say, two years from now, that engineering leadership hiring in this era was a success?" Green: An answer about capability and outcomes rather than headcount.
59.5 The universal closers
"What's the question I should have asked about this role that I haven't?" Green: They give you something real, and it's often the most valuable minute of the interview. Red: "You covered everything" — a missed opportunity, not a red flag. Use it in the second-to-last interview, not the last, so you have time to act on the answer.
"Based on this conversation, is there anything about my fit you're still unsure of? I'd rather address it now." Why it works: Converts a silent objection into an answerable one. Interviewers write feedback within a day; an unaddressed doubt becomes a written doubt. Green: They name something and you get to respond. Red: Deflection ("no, you did great") — accept it gracefully and move on; some interviewers are instructed not to give live feedback. Use it once per loop, with the hiring manager or in the final round. After: if they name something you answered weakly in the moment, address it properly in the follow-up note (§38.7) — that recovery has rescued candidacies.
"What made you stay?" Why it works: Works on anyone with tenure, and the pause before the answer is data. Green: Specific and personal — the people, a particular problem, growth they didn't expect. Red: A long pause, then a benefits-and-comp answer. That's someone staying for the paycheck, and if you hear it twice in one loop, weigh it.
59.6 Reverse due diligence — what to do when the red flags appear
Patterns, not single data points. Three or more across a loop is a signal.
| Pattern | What it predicts | Probe with |
|---|---|---|
| Interviewers describe the team differently | No shared reality; you'll be arbitrating from day one | "How does this team decide what to work on?" asked of three people |
| Nobody can define success for the role | An unwinnable mandate | "What does success look like at six months?" to everyone |
| Two predecessors gone in 18 months, no explanation | Structural problem you'll inherit | "What's changed since they left?" |
| Every problem answered with "we need great people" | The problem isn't headcount and they don't know it | "What have you tried that didn't work?" |
| HM can't describe their own manager's expectations | No air cover | "How does your manager measure this team?" |
| Postmortem items "usually get done," no examples | Reliability theater | "Name one that shipped" |
| All authority routes through one person | You'd be senior hands, not a lead | "Who decided the last significant architecture change?" |
| Visible contempt between product and engineering | Partnership is broken; you'll spend your time on it | Ask each side about the other |
| Can't explain why the level is what it is | Leveling chaos follows you in | "How was this level determined?" |
And the decision rule: you are allowed to decline an offer on this evidence. The most expensive career mistake at this level isn't failing a loop — it's passing one and taking the wrong job.
60. Stated-But-Unexplained — The Answers
Bullets across Parts II–IV assert a requirement without explaining the mechanism, the reasoning, or what to say. Each one below gets the full treatment: what it means, why it's true, how it's implemented, what happens without it, and the candidate's answer.
60.1 Agent determinism and debuggability (from §15.4)
The bullets as originally stated: "Full trace per step: inputs, tool calls, outputs, tokens, cost, model version" and "Replay capability — you cannot debug agents without it." True, and useless without the reasoning.
Why agents are uniquely undebuggable. In a normal service, a bug is reproducible: same input, same code, same output. An agent violates all three assumptions at once. The model is stochastic — the same prompt can produce a different tool call. The trajectory is path-dependent — step 14 depends on the output of step 13, so a single divergence early makes everything after it incomparable. The failure is often silent — the agent completed 20 steps, returned HTTP 200, and did the wrong thing. And the model is not yours — the provider can update it underneath you, changing behavior with no deploy on your side.
The consequence: the trace is not observability, it is the only source of truth about what happened. With a normal service you can re-run the request. With an agent, if you didn't capture it, it is gone permanently.
What "full trace" actually means, field by field, and why each field earns its place:
@dataclass
class StepTrace:
run_id: str # groups the whole trajectory
step_index: int # ordering — traces arrive out of order
parent_step: str|None # sub-agent lineage; without this, sub-agent
# traces are orphaned and unreadable
# --- The exact inputs. Not a summary. The bytes sent. ---
model: str # "claude-sonnet-4-6-20260514" — PINNED VERSION.
# "sonnet" is not a version. When behavior changes
# overnight with no deploy, this field is how you
# prove it and which support ticket you file.
system_prompt_hash: str # hash, plus the prompt in a versioned store
prompt_version: str # git sha of the prompt template
messages: list # full context AS SENT, post-assembly
tools_offered: list[str] # WHICH tools were in scope this step —
# "why didn't it use the search tool?"
# is usually answered here: it wasn't offered
temperature: float
seed: int|None
# --- What came back ---
output_text: str
tool_calls: list # name + arguments, exactly as emitted
stop_reason: str # "max_tokens" here means TRUNCATION, which
# looks like a reasoning failure and isn't
# --- Tool execution, separately from the model's request ---
tool_results: list # what the tool ACTUALLY returned
tool_latency_ms: dict
tool_errors: list
# --- Economics and performance ---
input_tokens: int
cache_read_tokens: int # cache hit rate per step; a drop means a
# prompt change broke prefix stability
output_tokens: int
cost_usd: float
ttft_ms: int
total_latency_ms: int
# --- Decisions your code made ---
policy_decisions: list # which gates fired: authz, approval,
# schema rejection, budget check
retrieved_doc_ids: list # RAG provenance: the answer to
# "was this a retrieval failure or a
# generation failure?" — the single most
# common diagnosis and you cannot make it
# without this field
The separation that matters most: log the model's requested tool call and the tool's actual result as distinct events. Half of agent bugs are "the model asked for the right thing and the tool returned garbage," and if you log them together you cannot tell that from "the model asked for the wrong thing."
What replay means, precisely — and it isn't re-running the agent. Three distinct capabilities, increasingly useful:
- Trace inspection — read what happened. Table stakes.
- Deterministic replay of the harness — re-execute your orchestration code against the recorded model responses and tool results. The model isn't called; you're testing your own code paths. This catches parsing bugs, state-machine bugs, and policy-gate bugs, and it is fully deterministic. This is the one most teams skip and the one that pays off daily.
- Counterfactual replay — re-run from step N with something changed (a new prompt, a different model, a fixed tool). The trajectory diverges after N, which is expected and fine: you're asking "does the new version get past the step where the old one failed?", not diffing the whole run.
class ReplayHarness:
"""Replays orchestration logic against recorded model/tool outputs.
Fully deterministic — no provider calls, no cost, runs in CI."""
def __init__(self, trace: RunTrace, override_from: int|None = None):
self.trace = trace
self.override_from = override_from # None = pure replay
def model_call(self, step, request):
if self.override_from is not None and step >= self.override_from:
return real_model_call(request) # counterfactual branch
recorded = self.trace.steps[step]
# Assert the harness produced the same request it did originally.
# A mismatch means YOUR code changed behavior — often the actual bug.
assert_request_equivalent(request, recorded.messages)
return recorded.response
def tool_call(self, step, name, args):
if self.override_from is not None and step >= self.override_from:
return real_tool_call(name, args)
return self.trace.steps[step].tool_results[name]
Determinism: what you can and cannot have. You cannot make the model deterministic — temperature=0 reduces variance but doesn't eliminate it (batching, hardware, and provider-side changes all introduce nondeterminism, and most providers say so explicitly). What you can make deterministic is everything you own: the context assembly, the parsing, the state machine, the policy gates, the retry logic, the termination conditions. So the design rule is: push all logic into the deterministic layer and treat the model as an untrusted, replayable I/O boundary. That's the same discipline as hexagonal architecture (§35.4) — the model is an adapter, not the core.
What you lose without this, concretely: a user reports the agent deleted the wrong record last Tuesday. Without traces you have logs saying "delete_record called with id=447" and no way to know whether the model hallucinated the ID, retrieval returned the wrong document, the prompt template changed, or the provider silently updated the model. You will not solve it. You will add a guardrail and hope. That's the failure this bullet was pointing at.
Cost and privacy, since an interviewer will raise them: full traces are expensive (prompts are large) and contain user data. The production answer is tiered: sample ~1% of successful runs, keep 100% of runs that errored, escalated to a human, hit a policy gate, or were thumbs-downed, redact PII at the collector (§28.2), and set retention by tier — 7 days hot for everything sampled, 90 days for failures, and a permanent golden set of interesting traces that becomes eval material.
The candidate's answer, three ways:
Screen (45s): "Agents are stochastic and path-dependent, so you can't reproduce a bug by re-running it — the trace is the only record of what actually happened. I'd capture every step: the exact context sent, the pinned model version, tools offered, the requested tool call and the actual tool result as separate events, tokens and cost, and the retrieved document IDs. And I'd build replay of the orchestration layer against recorded responses, so my own code is deterministically testable even though the model isn't."
Depth (2 min): (as above, plus) "The distinction I'd emphasize is between the parts I control and the part I don't. I can't make the model deterministic — temperature zero reduces variance but doesn't remove it, and providers are explicit about that. So I push everything else into a deterministic layer: context assembly, parsing, the state machine, policy gates, termination conditions. The model becomes an untrusted, replayable I/O boundary, which is really just hexagonal architecture applied to a probabilistic dependency. Then replay has three levels: read the trace, deterministically re-run my harness against recorded responses — that's the one that catches most real bugs and runs in CI for free — and counterfactual replay from step N with a changed prompt or model to check whether a fix gets past the failure point. On cost, I'd sample successes at one percent and keep a hundred percent of failures, escalations, policy-gate hits, and thumbs-downs, with PII redacted at the collector."
Story: "We had an agent that started failing a specific task type, and the only thing that changed was nothing on our side — no deploy. Because we pinned and logged the model version per step, we could see the provider had rolled a new model snapshot, and because we had traces from before and after we could show exactly which step's tool selection changed. Without the version field in the trace we'd have spent a week suspecting our own prompt. That's when I stopped treating model version as metadata and started treating it as a deployed dependency with a changelog."
Follow-ups:
- "How do you test an agent in CI without calling the model?" → Replay level 2 against a library of recorded traces. Every past production failure becomes a regression test. This is the agent equivalent of a fixture-based test suite and it's fast and free.
- "How do you know the agent got better after a change?" → Not from one trace. Task-level success rate on the eval suite (§44.4), plus steps-to-completion and cost per successful task. Traces diagnose individual failures; evals measure aggregate quality. Conflating them is the common mistake.
- "What if the trace itself is huge?" → Store prompts by content hash in object storage and reference them from the span; identical system prompts dedupe to one blob. Traces become small pointers.
60.2 "Idempotency keys, exactly-once as at-least-once + dedupe" (§5.1)
What it means. True exactly-once delivery is impossible across a network: the sender can't distinguish "the message was lost" from "the ack was lost," so it must either retry (risking duplicates) or not (risking loss). Every real system chooses at-least-once and removes the duplicates at the receiver. "Exactly-once semantics" as marketed always means at-least-once delivery plus deduplication somewhere.
Mechanism. The client generates a unique key per logical operation; the server records it atomically with the effect.
BEGIN;
INSERT INTO idempotency_keys (key, request_hash, status)
VALUES ($1, $2, 'in_progress')
ON CONFLICT (key) DO NOTHING;
-- 0 rows affected means we've seen this key: return the stored response
-- (or 409 if still in_progress — a concurrent retry)
... perform the effect ...
UPDATE idempotency_keys SET status='complete', response=$3 WHERE key=$1;
COMMIT;
The details that show experience: the key comes from the client (server-generated keys can't survive a client-side retry after a timeout); same key with a different body returns 422 rather than silently overwriting, which catches client bugs; keys expire (24h is Stripe's convention); and the key must be recorded in the same transaction as the effect or you've just moved the race.
Candidate's answer: "I'd say exactly-once doesn't exist on the wire — you get at-least-once plus idempotent receivers, and the honest version is 'effectively once.' Practically that means client-supplied idempotency keys on every mutating endpoint, recorded in the same transaction as the effect, with same-key-different-body returning an error rather than overwriting."
60.3 "Blast radius and cell-based architecture" (§5.1)
Definition. A cell is a complete, independent instance of your stack — compute, data, cache — serving a subset of customers. Cells don't share state. A cell failure affects only its tenants.
Why it exists. Horizontal scaling makes you bigger; it doesn't make failures smaller. In a single large deployment, one poison-pill request, one bad config, or one hot tenant degrades everyone. Cells convert a total outage into a partial one.
Mechanism: a thin routing layer maps tenant → cell (by hash or explicit assignment); deploys roll cell by cell with bake time (§39.1); cells are sized so the largest is a tolerable blast radius (commonly 5–10% of traffic); and there's usually a small "canary cell" of internal or volunteer tenants that gets every change first.
Real usage: AWS is explicit about cell-based architecture in its Well-Architected guidance and uses it internally; Slack's and Shopify's "pods" are the same pattern applied to multi-tenant SaaS, where a pod is a full vertical slice of the stack for a subset of shops.
The cost to name: cells multiply operational surface (N of everything), make cross-tenant features hard, and create uneven utilization. You adopt cells when the cost of a total outage exceeds the cost of running N stacks — which is a business calculation, not an engineering preference.
Candidate's answer: "Scaling out makes you bigger, not safer. Cells make the blast radius a design parameter instead of an accident — I'd size a cell at the largest outage I'm willing to have, route tenants deterministically, and deploy cell by cell with an internal canary cell first. The cost is N of everything and painful cross-tenant features, so I'd only do it where a full outage is genuinely unacceptable."
60.4 "Backpressure, load shedding, admission control" (§5.1) — three different things
Candidates use these interchangeably. They aren't.
| Backpressure | Load shedding | Admission control | |
|---|---|---|---|
| What | Signal upstream to slow down | Drop work you've already accepted | Refuse work at the door |
| Where | Between components you control | Inside the overloaded service | At the entry point |
| Mechanism | Bounded queues, request(n) (§17.2), TCP window, consumer lag | Priority-based dropping, timeouts, queue-depth triggers | Concurrency limits, rate limits, queue-time SLO |
| When it works | Closed systems where the producer can slow down | You're already saturated and must protect the core | You can predict capacity |
| Fails when | The producer is the public internet and cannot be told anything | Everything is equally important (fix your priorities) | Capacity is unpredictable |
The key insight: backpressure is useless against user traffic — you cannot ask the internet to slow down. Against the internet you need admission control and shedding. Backpressure is for internal pipelines.
Do this in the right order: admission control first (cheapest — you never pay for the work), then shed by priority when saturated, then backpressure internally. And shed the newest work, not the oldest — the oldest requests have already consumed resources and may have a user waiting on a partially-streamed response.
Real usage: Netflix's concurrency-limits library derives limits adaptively from observed latency using TCP-congestion-control-style algorithms rather than static thresholds — the reference implementation, and worth naming because static concurrency limits are always wrong at some traffic level.
Candidate's answer: "They're three layers, not synonyms. Admission control refuses at the door; shedding drops accepted work by priority when I'm saturated; backpressure signals upstream in pipelines I control. Against public traffic only the first two apply, because you can't tell the internet to slow down. I'd want adaptive limits derived from observed latency rather than static thresholds — Netflix's concurrency-limits is the model — because any fixed number is wrong at some point on the traffic curve."
60.5 "Feature stores: online/offline parity, point-in-time correctness" (§6.3)
The problem, concretely. You train a model on features computed in a batch job over historical data. You serve it with features computed in a streaming path at request time. The two computations drift — different code, different windows, different null handling — and your model degrades in production while offline metrics look fine. That's training/serving skew, and it's the leading cause of "the model was great in the notebook."
Point-in-time correctness is the subtler one. Building training data by joining today's feature values onto historical labels leaks the future into the past: you're training the model on information it won't have at inference. The result is spectacular offline metrics and a useless model. A feature store's core job is the point-in-time-correct join — for each label at time T, retrieve the feature values as they were at T.
Mechanism: one feature definition compiled to both a batch path (writes to an offline store, typically the warehouse/lakehouse) and a streaming path (writes to an online store, typically Redis or DynamoDB); the offline store is append-only with event timestamps so as-of joins work; the online store holds only current values for low-latency serving.
Named systems: Uber's Michelangelo popularized the pattern; Feast is the open-source reference; Tecton, Databricks Feature Store, and SageMaker Feature Store are the commercial versions.
Candidate's answer: "Two problems, and people usually only name the first. Skew is when the batch and serving computations differ — the fix is a single feature definition compiled to both paths, not two implementations you try to keep in sync. Point-in-time correctness is worse because it's silent: if you build training data by joining current feature values onto historical labels, you've leaked the future and your offline metrics will look excellent for a model that can't work. That's what the as-of join in a feature store is actually for."
60.6 "Sticky sessions can starve the canary" (§39.2)
Why. You set a canary weight of 5%. Session affinity pins each user to the instance they first hit. Returning users are already pinned to stable instances, so only new sessions can land on the canary — and if most of your traffic is returning users, actual canary traffic might be 0.5%, not 5%. You then declare the canary healthy on a tenth of the sample you thought you had.
How to detect: measure the canary's observed request share, never the configured weight. If configured and observed diverge, your canary analysis is invalid.
How to fix: cohort by a stable user hash rather than by session affinity, so a deterministic 5% of users (not sessions) go to the canary and stay there — which also fixes the flapping problem (§39.2). Or drain affinity before the canary. Or, if the app genuinely needs server-side session state, fix that instead: externalize sessions to Redis and the whole class of problem disappears.
Candidate's answer: "I'd verify the canary's observed traffic share rather than trusting the configured weight — affinity means returning users are already pinned elsewhere, so you can be analyzing a tenth of the sample you think you have. The clean fix is cohorting on a stable user hash instead of session affinity, which also stops users flapping between versions mid-session."
60.7 "Deleted nodes degrade HNSW until rebuilt" (§47.2)
Why deletes are hard. HNSW is a proximity graph where search quality depends on connectivity. Removing a node would require repairing every edge that routed through it — expensive and potentially graph-fragmenting. So implementations use soft deletes: mark the node, skip it in results, but keep traversing through it.
The degradation: search still costs the same (you traverse deleted nodes to reach live ones), ef_search effectively shrinks because deleted candidates consume the candidate list, and recall drops. At 20–30% deleted, quality is materially worse. In Lucene-based systems the recovery mechanism is segment merging, which physically drops deleted docs and rebuilds the graph for that segment.
What to do: monitor deleted-vector ratio as an operational metric; force merge or rebuild past a threshold (10–20%); for genuinely high-churn corpora, prefer time-partitioned indices you can drop wholesale (same instinct as TWCS in §46.1), or use IVF where deletes are cheaper.
Candidate's answer: "HNSW deletes are soft, because removing a node from a proximity graph would require repairing every edge through it. So deleted vectors still cost you traversal and eat your candidate list, and recall degrades as the deleted ratio climbs. I'd track that ratio as an SLI and rebuild past ten or twenty percent — and if the corpus is genuinely high-churn, I'd question HNSW and look at time-partitioned indices I can drop whole, or IVF."
60.8 "Prefer composition over deep inheritance" (§35.1)
Why, mechanically. Inheritance couples subclass to superclass implementation, not just interface — a change in the base class silently changes every descendant (the fragile base class problem). It's also single and permanent in most languages: you choose one parent at compile time forever. And deep hierarchies violate Liskov quietly, because each level adds preconditions the base contract didn't have.
Composition instead: the object holds a collaborator and delegates. You can swap it at runtime, hold several, and test with a double. Strategy, Decorator, and dependency injection are all composition.
The honest counter: inheritance is right for genuine is-a relationships with stable contracts — framework extension points, sealed hierarchies (algebraic data types), and template methods where the variation is truly a step in a fixed algorithm. Blanket "inheritance is bad" is as unthinking as deep hierarchies.
Candidate's answer: "Inheritance couples you to the parent's implementation and you get one parent forever, so a base-class change ripples into every descendant. Composition gives you swappable collaborators and testable seams. I'd still use inheritance for genuine is-a with a stable contract — sealed hierarchies and framework extension points — but I default to composition, and if a hierarchy is more than two deep I'd want to know why."
§59–60 close the "stated but not answered" gap for the highest-traffic items. The same treatment is owed to every remaining bullet in Parts II–IV; §51's gap register enumerates them, and the Claude Code build applies this exact pattern — mechanism, reasoning, consequence of absence, candidate's answer in three registers, follow-ups — to each one.