← All topics

Architecture

FAANG-style system design and production scenarios — purely advanced, answered in STAR format with real data points.

Basics (0)
Advanced (33)
Explain 3-tier architecture — then design it production-grade on AWS with numbers.

The concept: presentation (web/UI) → application (business logic) → data (persistence), each tier independently scalable, communicating only with adjacent tiers — the separation that lets you scale app servers without touching the database, and secure the data tier behind two layers of blast radius.

users → CloudFront + WAF presentation — public subnets ALB across 3 AZs · TLS terminates here · only tier with internet exposure application — private subnets ASG/ECS across 3 AZs · stateless · SG accepts only from ALB data — isolated subnets RDS multi-AZ + read replicas + ElastiCache · SG accepts only from app tier
each tier in its own subnet class with security groups chaining — the internet can only ever touch tier 1

Production-grade on AWS, with the numbers interviewers want:

  1. Edge: CloudFront (static assets + TLS + DDoS absorption) + WAF (rate rules, OWASP managed rules) — cuts origin traffic 60-80% for typical web apps
  2. Presentation: ALB across 3 AZs, HTTP→HTTPS redirect, connection draining 30s
  3. Application: stateless services (sessions in ElastiCache/DynamoDB — statelessness is what makes horizontal scaling work), ASG target-tracking at 60% CPU with 3-AZ spread, scale-out in ~2-3 min
  4. Data: RDS multi-AZ (sync standby, ~60-120s failover), read replicas for read-heavy paths, ElastiCache in front (a 90% cache hit rate shrinks the DB problem 10x)
  5. Security chaining: SG-references not CIDRs (app-SG accepts from alb-SG) — topology-proof rules; data subnets have no route to anywhere but the app tier

The 'why' to articulate: independent scaling (Black Friday scales tier 2 by 5x, tier 3 barely moves thanks to cache), failure isolation (an app-tier deploy can't expose the DB), and clear security gradients. The honest limits: it's a deployment topology, not a modularity strategy — a 3-tier monolith is still a monolith; and tier-3 (the stateful tier) remains the scaling boundary that eventually forces the harder conversations (sharding, CQRS, purpose-built stores).

Design an advanced Kubernetes platform with DR and aggressive cost optimization — the CTO wants 99.95% availability at 60% of current spend. (STAR)

Situation: 3 production EKS clusters (single-region), ~$180K/month compute, availability incidents from node failures and one AZ event; leadership mandate: 99.95% with a 40% cost cut — the classic 'better AND cheaper' that's actually achievable because most K8s estates waste 50-70% of what they buy.

Task: re-architect for measured resilience and measured efficiency — both numbers on dashboards, neither taken on faith.

Action — the cost half (where the 40% comes from):

  1. Utilization forensics first: requests-vs-usage analysis showed cluster-wide CPU utilization at 11% of requests — the classic over-request tax. Right-sizing (VPA recommendations + enforced request reviews) recovered ~30% of nodes alone
  2. Karpenter + spot-first for the stateless tier: consolidation-aware provisioning, diversified spot pools (6+ instance families), on-demand floor for the disruption-sensitive 20% — compute price down 55-65% on the spot-eligible majority. PDBs + graceful-shutdown discipline made 2-minute spot reclaims a non-event (measured: <0.01% request impact)
  3. Bin-packing and scheduling hygiene: topology-spread over anti-affinity-everywhere (anti-affinity was forcing one-pod-per-node sprawl), namespace quotas ending the 'requests as wishes' culture, and Graviton migration for the compatible 70% (another 15-20% price-perf)
  4. The valleys: dev/staging scale-to-near-zero off-hours (schedules + cluster autoscaler), batch workloads queued into spot-capacity valleys via Kueue

Action — the availability half:

  1. Blast-radius architecture: 3-AZ node groups with enforced zonal spread (the AZ incident had found 80% of one service in one AZ), PDBs on everything tier-1 (CI-enforced), and priority classes so platform components preempt batch, never vice versa
  2. DR design to a written RTO/RPO (15 min / 5 min for tier-1): warm standby in a second region — cluster pilot-light (control plane + minimal nodes, Karpenter scales on failover), GitOps as the replication mechanism (ArgoCD applies the same desired state to both regions — cluster rebuild is not the DR plan; re-pointing traffic is), data layer replicated per store (Aurora Global ~1s RPO, S3 CRR, ElastiCache rebuilt-on-failover as accepted cache loss), Route 53 health-check failover with pre-decided automation for tier-1
  3. Prove it or it's fiction: quarterly game days — actual regional failover of a real service cohort (first drill: 42 minutes, found 6 hardcoded region strings and an IRSA gap; third drill: 11 minutes) — the drill is the availability engineering

Result: spend $180K → $68K/month (-62%, beating the target); availability 99.97% over the following two quarters including one real AZ degradation (zonal spread + PDBs held; customers unaware); failover rehearsed to 11 minutes against the 15-minute RTO; and the utilization dashboard (requests vs usage per team) became the standing mechanism that keeps it from regressing.

What they're testing: that you know cost and resilience aren't opposed (waste funds redundancy), that DR is a rehearsed number not an architecture diagram, and that the durable win is the mechanisms — enforced spread, quotas, game days, utilization showback — not the one-time cleanup.

Design a URL shortener for 100M new links/month and 10B redirects/month — the classic, answered like a senior.

Scope the numbers first (the senior habit): writes ~40/s (peaks 400/s — trivial); reads ~4K/s average, 40K/s peak — a 400:1 read-heavy system where the redirect path is everything. Storage: 100M/month × ~500B ≈ 50GB/month, 3TB over 5 years — small. This is a latency and availability problem, not a big-data problem — saying so is the first senior signal.

The write path: ID generation is the only interesting decision — pre-generated key ranges (a key service handing out blocks to app servers — no per-write coordination) or base62-encoded IDs from a distributed sequence (Snowflake-style). 7 chars of base62 = 3.5 trillion keys — space is a non-issue; avoid hash-the-URL schemes (collision handling + same-URL-same-key is usually an anti-feature for analytics). Custom aliases: uniqueness-checked inserts, rate-limited (they're the abuse vector).

The redirect path (where the design lives):

  1. Cache aggressively: hot links follow a hard power law — ~1% of links take ~90% of traffic; Redis with ~1 day TTL absorbs the bulk (hit rate 95%+), CDN/edge caching in front for the truly viral (301 vs 302 decision: 301 lets browsers/CDN cache but kills click analytics and mutability — most products choose 302 deliberately; explain the trade)
  2. Store: DynamoDB/Cassandra-shaped KV (key → URL, owner, created, flags) — single-digit-ms reads, effortless replication; multi-region active-active reads (redirects are the availability-critical path — the shortener being down breaks every link ever created; that asymmetry justifies multi-region for reads even if writes stay single-region)
  3. Availability math: 99.99% on redirects = the cache+store+LB chain each engineered past it; degrade gracefully (stale cache serves during store blips — a slightly-stale redirect beats an error)

Analytics without touching the hot path: redirect emits an async event (Kinesis/Kafka) → stream aggregation → click counts/geo/referrer dashboards — never synchronous DB writes on the redirect (the #1 way candidates ruin their own design). At 4K/s the event pipeline is routine.

The operational layer that distinguishes staff-level answers: abuse handling (malware URL scanning at creation + a kill-list check on redirect — one Bloom-filter lookup, because your domain's reputation is the product), link expiry/GC policy, per-key rate limits, and the observability cut: P99 redirect latency, cache hit rate, and key-service health as the three dashboards that matter.

One-liner close: 'a tiny write system attached to a global read system — pre-allocated keys, cache-first 302 redirects, async analytics, and multi-region reads because every link ever minted is an SLA.'

Design a rate limiter — as a library, as a service, and at the edge; algorithms, distributed state, and what you'd actually ship.

Algorithms in one breath each: fixed window (simple; 2x burst at boundaries), sliding window log (exact; memory per request — no), sliding window counter (weighted blend of two windows — accurate enough, O(1); the practical default), token bucket (rate + burst allowance — the semantics most APIs actually want: 'sustained 100/s, bursts to 500'), leaky bucket (smooths egress; right for downstream-protection shaping).

The distributed problem (the real interview): limits shared across N gateway nodes need shared state — the design spectrum:

  1. Centralized counters (Redis + Lua): atomic check-and-increment scripts; accurate, adds ~1ms + a dependency on the hot path. Redis Cluster shards by key naturally (per-user keys distribute) — hot global keys (one big customer) need key-splitting (N sub-buckets summed)
  2. Local + async sync: each node enforces locally against its share, syncs deltas periodically — zero added latency, bounded over-admission during sync gaps (quantify it: N nodes × sync interval × rate = worst-case overshoot; usually fine — rate limiting protects aggregate capacity, and ±5% accuracy at zero latency beats exact at +1ms for most tiers)
  3. The hybrid most real systems ship: local token buckets for the fast path + central reconciliation, with fail-open vs fail-closed decided per tier (Redis down: fail-open for user requests with alerting — availability over precision; fail-closed for expensive/abuse-sensitive operations like auth attempts and money movement)

Where it lives:

  • Edge/CDN rules for the crude volumetric tier (cheapest place to shed garbage)
  • Gateway middleware for per-user/per-API-key product limits (the main enforcement point — one implementation, all services covered)
  • Library/sidecar for service-to-service budgets (protecting internal dependencies — often via adaptive/concurrency limits rather than fixed rates)

The product-design half people skip:

  1. Response contract: 429 + Retry-After + rate-limit headers (remaining/reset) — well-behaved clients need the information to behave well; SDKs honor it automatically
  2. Limit taxonomy: per-user, per-key, per-IP (careful: NAT), per-tenant-tier (the paying customer's limit is a product attribute — config in the billing system, not YAML), and global emergency brakes (load-shedding overrides when the platform itself is drowning)
  3. Observability: limited-request rate by key/tier (a spike = attack or a customer's runaway retry loop — both actionable), near-limit warnings surfaced to customers proactively (the support-ticket preventer)

What I'd ship: token-bucket semantics, sliding-window-counter accounting, Redis+Lua central state with local-bucket fast path, per-tier fail-open/closed policy, standard headers, and limits-as-product-config — boring, accurate-enough, and debuggable at 3am.

One-liner: 'token buckets for semantics, Redis-Lua or local-with-sync for state, fail-open by default and fail-closed where money moves — and remember rate limiting is a product surface with headers, tiers, and dashboards, not just a counter.'

Design a chat/messaging system (WhatsApp-scale mechanics at Slack-scale numbers): delivery, ordering, presence, and offline sync.

Scope: 10M DAU, ~50 messages/user/day → ~6K msg/s average, 60K/s peak; groups to 10K members; mobile + web; offline-first expectations.

The connection layer: persistent connections (WebSocket) to a gateway fleet — each node holding ~100K-1M connections (memory + FD tuned); a connection registry (Redis-shaped: user → gateway node) for routing; graceful drain on deploys (the gateway fleet deploys daily — reconnect storms are a designed-for event with jittered backoff, not an incident).

The message path (the ordering + delivery core):

  1. Sender → gateway → message service: assign per-conversation sequence number (the ordering primitive — a per-conversation counter via the storage layer's conditional writes; global ordering is neither needed nor sane), persist first (message store: Cassandra/DynamoDB-shaped, partitioned by conversation, clustered by sequence), then fan out
  2. Fan-out: look up recipients' gateways in the registry → push. Small groups: fan-out-on-write. The 10K-member group: fan-out-on-write to online members' gateways + fan-out-on-read for the rest (their next sync pulls) — the hybrid that avoids 10K writes per celebrity-group message
  3. Delivery semantics — at-least-once + idempotent client: client-generated message IDs dedupe retries; sent/delivered/read receipts as separate lightweight events flowing the reverse path. Exactly-once is a lie; idempotency is the truth — say this

Offline sync (the part that separates real answers): every client tracks a per-conversation cursor (last-seen sequence); reconnect = 'give me everything after cursor X in my conversations' — the sequence numbers make sync a range query, not a diff algorithm. Push notifications (APNs/FCM) for offline recipients carry a wake-up, not the payload (E2E and payload-size reasons both).

Presence (deceptively expensive): naive presence = N² update storms. Reality: heartbeat → registry TTLs for state; subscription-based fan-out (you get presence only for conversations on screen), debounced transitions (flapping mobile connections would otherwise dominate all traffic), and honest staleness (presence is eventually-consistent decoration, not a delivery guarantee — design UX accordingly).

The hard corners to volunteer:

  1. Multi-device: each device its own cursor + connection; messages fan to all the user's devices; read-state merges (read on phone clears badge on laptop — via the receipt events)
  2. E2E encryption (if in scope): server becomes a blind router of ciphertexts — search, moderation, and multi-device key distribution all get materially harder; state the trade explicitly rather than hand-waving both
  3. Storage growth: 6K/s × ~1KB ≈ 500GB/day — tiering (hot store 90 days → object storage archive), and retention as a product/compliance decision the architecture must support per-tenant
  4. Abuse/moderation path, media via presigned-URL upload to object storage (never through the message path — only the reference travels)

One-liner: 'per-conversation sequences give ordering and make offline sync a range query; registry-routed gateways move the messages; hybrid fan-out tames big groups; receipts and presence are separate eventual-consistency planes — and idempotent clients, not exactly-once fantasies, are what make delivery honest.'

Design a distributed job scheduler / async task platform (the internal 'run this later, reliably' service every company builds).

Requirements shape: one-off delayed jobs + cron-style recurring, at-least-once execution with idempotency support, priorities, retries with backoff, ~50K job executions/minute peak, and multi-tenant fairness (one team's backfill must not starve everyone).

The core loop:

  1. Job store as the source of truth (Postgres at this scale — boring wins): jobs table (id, type, payload, state machine: scheduled → claimed → running → succeeded/failed/dead, run_at, attempts, priority, tenant); state transitions are the audit log
  2. Scheduling = an index scan, claiming = the concurrency problem: dispatchers poll run_at <= now AND state = scheduled — and claim atomically: UPDATE ... SET state='claimed', worker=me WHERE id IN (SELECT ... FOR UPDATE SKIP LOCKED LIMIT N)SKIP LOCKED is the two-word answer to thundering-herd claiming on shared tables; alternatively push claimed work through a queue (SQS/Kafka) and let the queue do distribution (store decides when, queue does who)
  3. Workers: pull claimed jobs, execute with heartbeat leases (running jobs renew; a dead worker's lease expires → job reclaims to scheduled with attempts++) — this lease-reclaim loop IS the crash-safety story
  4. Retries + the dead-letter contract: exponential backoff with jitter, per-job-type retry policies, terminal failures → dead-letter state with full context + alerting; idempotency as a first-class contract (at-least-once means re-execution happens: job payloads carry idempotency keys; the platform documents 'your handler WILL run twice someday' and provides the key plumbing to make that safe)

Cron/recurring: recurrence rules stored once; a materializer generates the next concrete execution row when one completes (or slightly ahead) — never pre-generate a year of rows; handle the classic traps explicitly: DST transitions (store TZ-aware rules), missed windows during platform downtime (policy per job: run-once-now / skip / run-all-missed — make teams choose at registration, because each is right for someone), and overlap policy (previous run still going when next fires: skip / queue / kill-and-replace — again, per-job declaration).

Multi-tenancy + fairness: per-tenant concurrency quotas + weighted fair claiming (the claim query respects per-tenant in-flight caps) — priority alone inverts into starvation; quotas are what actually protect the platform. Rate-limit job submission too (the runaway-producer incident is a when).

Scale evolution to narrate: Postgres + SKIP LOCKED comfortably reaches ~10-50K/min executions; beyond that, partition the jobs table by time/tenant, move claiming to a real queue, and shard dispatchers — but say that most companies never outgrow the boring version, and premature Kafka-ification is the common failure. Observability: queue depth per tenant/type, schedule lag P99 (promise-vs-actual run time — the SLO), retry/dead-letter rates, and per-type duration trends.

One-liner: 'a state-machine table with SKIP LOCKED claiming, lease-heartbeat crash recovery, per-type retry/overlap/missed-run policies, tenant fairness quotas, and idempotency as documented contract — reliability comes from the state machine and the leases, and Postgres takes you much further than the résumé-driven alternatives.'

Design a news feed / timeline system: fan-out strategies, ranking integration, and the celebrity problem.

Scope: 50M DAU, follow-graph median ~200 / P99 ~50K / celebrities 10M+, ~5K posts/s, feed read P99 < 200ms — a read-optimized system (reads outnumber writes ~100:1) with a write-amplification landmine.

The two pure strategies and why neither survives alone:

  1. Fan-out-on-write (push): post → write into every follower's precomputed feed store (Redis lists/sorted sets per user). Reads are O(1) list fetches — beautiful — but a 10M-follower post = 10M writes: the celebrity problem turns one tweet into a write storm (10M × even 1KB = 10GB of feed writes for one post)
  2. Fan-out-on-read (pull): feed = query your followees' recent posts at read time, merge, rank. Writes are O(1); reads do a K-way merge across ~200 authors' recents — fine per-read, but 50M DAU × multiple opens = enormous repeated merge compute, and P99 suffers on large followee sets

The hybrid everyone actually runs: push for normal accounts (the 99.9% with bounded followers — async fan-out workers off the post event, eventually-consistent within seconds), pull for celebrity content (accounts above a follower threshold flagged: their posts are not fanned out; instead, read-time merges 'your precomputed feed' + 'recent posts from your followed celebrities' — a bounded merge since users follow few mega-accounts). The threshold is empirical (fan-out cost curve vs read-merge cost — typically 10K-100K followers).

Ranking integration (where the modern architecture lives): the feed store holds candidates, not final order — read path: fetch ~500 candidates (precomputed + celebrity-merge + recommended/injected) → feature hydration (engagement signals, author affinity, recency, content features — a fast feature store lookup) → ML scoring service (a latency budget of ~50ms for the model pass) → business-rules re-rank (diversity injection, seen-post suppression via a per-user 'seen' Bloom filter, ads slotting) → serve + cache briefly. The pipeline is candidates → features → score → policy — say it as a pipeline and the design conversation goes anywhere you want.

The supporting cast:

  1. Feed store sizing: cap precomputed feeds (last ~800 entries — beyond that, pagination falls through to pull) — feeds are caches, not archives; the post store (source of truth) backs everything
  2. Consistency posture: your own post appears instantly (write-through to own view — read-your-writes for the author); others' feeds converge in seconds (nobody can perceive it); deletes must propagate fast (the fanned-out copies problem: tombstone check at render beats chasing 10M list entries)
  3. Failure degradation ladder: ranking service down → recency-ordered candidates (a worse feed beats no feed); feed store down → pull-mode fallback at reduced K; the feed is the product — availability outranks freshness outranks ranking quality, in that order

One-liner: 'push for the many, pull for the mighty, and rank at read time over hydrated candidates — feeds are bounded caches over a post store, deletes need tombstones not chases, and the degradation ladder (recency beats nothing) is part of the design.'

You're asked to cut cloud spend 30% in a quarter without harming reliability. Give the program, the levers, and where the bodies are buried. (STAR)

Situation: $2.4M/month AWS across 60 accounts, bill growing 8%/month against flat traffic — the growth itself was the tell: spend decoupled from demand means waste compounding. No cost ownership below the CTO line.

Task: -30% ($720K/month) in a quarter, zero reliability regressions, and — the part I added — mechanisms so it doesn't grow back.

Action — sequenced by effort-to-savings ratio:

  1. Week 1-2, the corpse sweep (~8-10% typically): unattached EBS volumes and elderly snapshots, idle load balancers, unused elastic IPs, stopped-instance storage, orphaned dev environments from departed projects, over-retained logs (CloudWatch never-expire is a slow leak), gp2→gp3 (20% cheaper, one API call), S3 without lifecycle policies. Zero risk, pure archaeology — found $210K/month
  2. Week 2-6, commitment coverage (~10-12%): coverage analysis showed 22% Savings-Plan coverage on a steady-state baseline that justified 70% — bought compute SPs in tranches (not one big bet: laddered commitments preserve flexibility). RDS/ElastiCache reserved coverage same story. This is pricing, not engineering — no workload touched, $260K/month
  3. Week 4-10, right-sizing with evidence (~8-10%): P99-based instance right-sizing (the fleet averaged 12% CPU), K8s request right-sizing (the utilization forensics playbook), non-prod scale-to-zero schedules (nights/weekends = 65% of hours!), and storage-class tiering on the data lake (S3 IA/Glacier by access pattern — access logs showed 80% of objects untouched in 90 days)
  4. Week 6-12, the architectural tier (the honest 'it depends' bucket): NAT gateway data-processing charges → VPC endpoints for the S3/ECR-heavy paths (one team's NAT bill was $40K/month of ECR pulls); cross-AZ transfer audits (chatty services co-located or made AZ-aware); the two services running 24/7 for daily batch → Fargate on schedule. Spot adoption for CI and stateless tiers where the earlier K8s work applied
  5. Throughout — the mechanisms (the actual deliverable): tagging enforcement (untagged = auto-flagged, eventually auto-stopped in dev), per-team showback dashboards, Infracost on PRs (cost-diff visible at design time), budget alerts with team routing, and a monthly FinOps review where teams present their own trends — cost as a first-class engineering metric with social visibility

Where the bodies are buried (the answer's differentiator): data transfer (invisible in most teams' mental models — NAT processing, cross-AZ, cross-region replication), logging/observability vendors ingesting DEBUG from prod, the 'temporary' clusters from 2023, snapshot chains nobody dares delete, and dev environments sized like prod because the template was copied. Also the political body: the biggest single line was a platform team's 'we might need it' capacity buffer — negotiated down with an autoscaling SLA instead of a standing fleet.

Result: -34% ($815K/month) by week 11; zero reliability incidents attributable (the game-day cadence continued through the quarter as the check); growth rate flipped to -1%/month on flat traffic; and 14 of 18 teams under budget the following quarter — the showback dashboards did more durable work than any single optimization.

What they're testing: sequencing sense (pricing and corpses before re-architecture), the data-transfer blind spot, reliability guardrails during cost work, and whether you build the ratchet — visibility, ownership, and PR-time cost signals — or just run a one-quarter diet.

Design a metrics/observability platform (Datadog-shaped) ingesting 50M data points/second: write path, storage, and query.

The workload truths that drive everything: writes are relentless and append-only (50M points/s, no updates), reads are sparse but bursty (dashboards + alert evaluation + incident spelunking), recent data is hot and old data is cold, and cardinality is the real enemy (a metric × label-combination explosion kills these systems, not raw volume).

Write path:

  1. Agents → regional ingest gateways: batched, compressed, authenticated; gateway validates, rate-limits per tenant (ingest protection is tenant fairness), and writes to Kafka (the shock absorber — ingest spikes and downstream hiccups decouple; the buffer is what makes the 'we never drop data' claim survivable)
  2. Stream processors consume: resolve series identity (metric + sorted label-set → series ID via a series index), detect new series (cardinality accounting here — per-tenant series budgets enforced at the gate, because the alternative is one bad deploy adding a request_id label and minting 10M series by lunch), pre-aggregate where policy allows (10s → 1m rollups computed on ingest for the long-retention tiers)
  3. TSDB writes: LSM-shaped time-series storage, partitioned by (tenant, series-hash, time-window) — recent blocks on NVMe, sealed blocks compacted, downsampled (raw 10s: 15 days → 1m: 90 days → 1h: 2 years — the rollup ladder is the cost model), and shipped to object storage with local caching (the Thanos/Mimir/M3 shape: object storage as the infinite cheap tier, compute-local caches as the fast tier)

Query path:

  1. Query frontend: parse (PromQL-shaped), split by time range across storage tiers (last hour from hot blocks, last month from object-store blocks — transparently merged), fan out to per-partition queriers, merge/aggregate — with query cost governance (a 90-day raw-resolution query across 1M series gets rejected or auto-downsampled with a hint; ungoverned queries are how one dashboard takes down the query tier)
  2. Result + block caching (dashboards re-ask identical questions every 30s — cache hit rates are enormous), and alert evaluation as a separate, protected workload (alerting queries run on reserved capacity with a stricter SLO than exploration — the system paging you must not be the system a heavy dashboard can starve; this separation is a design point interviewers reward)

The hard problems to name proactively:

  1. Cardinality governance as a product surface: per-tenant series quotas, top-offender dashboards ('your service added 400K series yesterday'), label allowlists/denylists at ingest — treating cardinality like the billable, finite resource it is
  2. The series index at scale (billions of series): it's its own distributed database (inverted index label→series), and index rebuild/compaction is an operational discipline
  3. Multi-tenancy isolation end-to-end: noisy-neighbor protection at ingest (quotas), storage (partition isolation), and query (per-tenant concurrency budgets)
  4. Self-monitoring: the observability platform needs an independent minimal monitor (who watches the watcher — a small external prober, not itself)

One-liner: 'Kafka absorbs, stream processors index and roll up, an LSM TSDB tiers hot-to-object-storage, queries merge across tiers under cost governance, alerts get reserved capacity — and cardinality budgets, not ingest volume, are where the platform lives or dies.'

Migrate a monolith to services without stopping feature delivery: the strangler program, the seams, and when to stop extracting. (STAR)

Situation: 1.2M-line Rails monolith, 90 engineers stepping on each other (deploy train twice a week, 4-hour test suite, merge conflicts as a lifestyle), database shared by everything — and a failed previous attempt: a 9-month 'rewrite the order system' project that shipped nothing and burned political capital.

Task: restore delivery velocity via decomposition — without a feature freeze (the business explicitly forbade one, correctly), and without repeating the big-bang failure.

Action:

  1. Fix the pain directly first (weeks 1-8): the goal is velocity, not services — services are a means. Test-suite parallelization (4h → 25min), deploy train → continuous deploy of the monolith itself (trunk-based + flags), and modular monolith boundaries (packwerk-style module enforcement inside the repo: explicit interfaces, dependency rules in CI). This alone recovered half the velocity complaint — and the module boundaries became the seam map for extraction. Cheap, reversible, and it de-risked everything after
  2. Extract by the strangler pattern, pain-ordered (months 2-12): candidates ranked by (change frequency × team contention × runtime divergence) — not by architectural aesthetics. First extraction: notifications (high volume, clean seam, low data coupling — a confidence-builder). Then payments (the contention hotspot). Each extraction: new service behind the routing seam (API gateway/monolith proxy routes per-endpoint), dual-run verification where writes were involved (shadow traffic, output diffing — days of 'new path agrees with old path' evidence before cutover), then flag-flip cutover with instant rollback
  3. The data decomposition (the actual hard part — say this): each extracted service got its own store; the shared-table dependencies broke via: views/APIs replacing cross-domain reads, change-data-capture events replacing cross-domain triggers, and a reconciliation job during transition proving the two sources agreed. The order-items table that four domains wrote → one owning service + events, a 3-month project on its own — data seams cost 3-5x the code seams, budget accordingly
  4. Team topology moved with the code: extracted services went to the teams already dominating their change history (Conway alignment — the org chart change is part of the migration); platform team built the paved road (service template, CI/CD, observability defaults) so each extraction got cheaper — extraction #1: 8 weeks; #5: 2 weeks
  5. The stopping rule (the wisdom part): after 7 extractions in 14 months, the remaining monolith (~60% of the code — admin, reporting, low-change CRUD) had no contention problem: one team owned it comfortably, deploys were continuous, change frequency was low. We stopped. The goal was never zero-monolith; 'services where teams collide, monolith where they don't' was the end state, declared explicitly so nobody kept extracting for sport

Result: deploy frequency 2/week → 40+/day across the estate; lead time P50 9 days → 1.1 days; the payments team's merge-conflict rate to ~zero; two later incidents validated the isolation (a notifications outage that pre-migration would have been a full-site event). And the anti-result worth reporting: operational cost rose (7 services × on-call/observability/pipelines) — paid for by the platform paved-road work, but real; decomposition is a trade, not a free lunch.

What they're testing: that you fix the actual pain (velocity) rather than worship the pattern, strangle rather than rewrite, treat data seams as the real cost center, move the org with the code, and — rarest — know when to stop.

Design global multi-region active-active for a transactional system: data topology, conflict handling, and what you'd refuse to build.

First, the refusal (the senior opener): 'active-active' for strongly-consistent transactional writes to the same records from multiple regions is where architectures go to die — physics makes you choose: synchronous cross-region coordination (adding 60-150ms to every write and coupling regional availability) or conflict resolution (pushing correctness into application semantics). Before designing, I interrogate the requirement: most 'active-active' asks are actually 'active-active reads + fast regional failover for writes' — which is buildable without the pain. The design below assumes the ask survives interrogation.

The topology that works — partition ownership (home regions):

  1. Shard the write authority: each tenant/user/entity is homed to a region (by residency requirement, or latency proximity) — writes for that entity go to its home region only; other regions hold async replicas (~sub-second lag). No same-record write conflicts by construction — conflict avoidance beats conflict resolution wherever you can partition
  2. Reads everywhere: every region serves local reads from replicas; read-your-writes for cross-region readers via session tokens/sticky routing where it matters
  3. Failover = re-homing: region loss → its entities re-home to survivors (automated for tier-1, with the replication-lag window as your RPO — Aurora Global/DynamoDB global tables give ~1s); the rare conflict surface is exactly the failover window (writes accepted before re-homing completes) — bounded, auditable, reconciled

Where true multi-writer is unavoidable (the same record, multiple regions, no partition key):

  1. CRDTs / commutative operations for the data shapes that allow it (counters, sets, presence) — mergeable by construction
  2. Last-writer-wins only for data where losing a write is acceptable (say it explicitly — LWW silently discards concurrent writes; it's a business decision wearing a config flag)
  3. Application-semantic resolution for the rest (versioned writes surfacing conflicts to domain logic: 'two edits to the same order → hold for review') — expensive, so keep this surface tiny
  4. Or pay the coordination tax deliberately: consensus-replicated stores (Spanner-style) buy external consistency at write-latency cost — legitimate for low-write-rate/high-correctness domains (inventory counts, financial ledgers) as a narrow tier, not the default

The non-data layers (people forget these fail regionally too): global traffic management (latency-based routing + health-checked failover), config/feature-flag replication, secrets/identity availability per region, and the dependency audit — the multi-region app calling a single-region internal service has single-region availability with extra steps; the audit finds these before the outage does.

The proof obligations: regional evacuation game days (drain a region for real, quarterly), replication-lag SLOs with alerting (your RPO is a live number, not a design constant), failover-window conflict reconciliation reports (should be ~zero; measure it), and per-region capacity to absorb a peer's load (N-1 sizing — 'multi-region' with 50%-each capacity is a cascading failure schematic).

One-liner: 'partition write authority by entity home, replicate async, read local everywhere, re-home on failure — reserve true multi-writer for CRDT-shaped data and consensus stores for the narrow ledger tier; the best conflict-resolution strategy is a topology where conflicts can't happen, and the design isn't done until a region has been evacuated on purpose.'

An architecture review lands on your desk: a team proposes event sourcing + CQRS + sagas for their new order system. Run the review.

The review posture: these patterns are legitimate and heavy — the question is never 'are they good' but 'does this domain's complexity justify this machinery's cost'. The failure mode I'm screening for: résumé-driven architecture where a CRUD problem wears a Kafka costume.

The questions I ask, in order:

  1. 'Show me the requirement that CRUD-plus-audit-log can't satisfy.' Event sourcing's genuine wins: complete rebuildable history as the source of truth (regulatory replay, temporal queries — 'what did this order look like when the dispute was filed'), retroactive bug correction (replay with fixed logic), and domains that are naturally event-shaped (ledgers, trading). If the answer is 'audit trail' — an append-only audit table gives 80% at 20% cost. If it's 'we might need history' — YAGNI with interest
  2. 'Who owns schema evolution?' Events are forever — v1 events replay through today's code in year 3: upcasting strategy, event versioning discipline, schema registry governance. Teams that haven't planned this are planning archaeology. Same for PII in immutable events (GDPR deletion vs append-only — crypto-shredding per-subject keys is the standard answer; if they haven't heard the question, they're not ready)
  3. 'Walk me through the projection rebuild story.' CQRS read models rebuilt from N-million events: how long, and what serves reads meanwhile? Snapshotting cadence? If rebuild is 'stop the world for 6 hours', the operational design isn't done
  4. 'Where does eventual consistency reach the user?' The read-after-write gaps (order placed → order list doesn't show it yet): which UX flows are affected, and what's the mitigation (read-your-writes via command-result data, UI optimism, sync-projection for the critical paths)? Hand-waving here means support tickets later
  5. 'Sagas: show me the compensation matrix.' Every step needs a tested compensating action; the matrix of partial-failure states needs owners ('payment captured, inventory reserve failed → ?'). Orchestration vs choreography chosen deliberately (my default: orchestrated sagas for anything with money — explicit state machines beat emergent behavior when auditors visit). If compensations are 'we'll figure it out', the saga is a distributed bug factory

The verdicts I actually give:

  • Full stack justified: genuinely event-native domain + regulatory replay needs + a team that answered the five questions — proceed, with the platform investments (schema registry, projection tooling, event-store ops) budgeted
  • The common middle: CQRS-lite without event sourcing (transactional writes + CDC-fed read models) or domain events atop a relational core (events as integration, state as truth) — 70% of the benefits, a third of the operational surface. Most teams that ask for the full stack need this
  • Refuse: event sourcing as an audit-log substitute, or sagas where a transactional monolith boundary would do (if all the steps live in one service's database, use a transaction — the saga is for distributed state, not for pattern points)

One-liner: 'the review is five questions — the CRUD-can't-do-it requirement, event evolution, projection rebuilds, consistency-at-the-UI, and the compensation matrix — and the most common right answer is the middle path: relational truth, CDC-fed read models, events at the boundaries, and transactions wherever physics still allows them.'

Design a payments system's reliability architecture: idempotency, exactly-once effects, reconciliation, and the double-charge postmortem you're preventing.

The prime directive: in payments, the failure modes are asymmetric — a dropped request annoys; a duplicated one becomes a refund, a support ticket, and a trust dent. Every design choice below exists because retries are mandatory (networks fail) and retries without idempotency are double-charges.

The idempotency architecture (the load-bearing wall):

  1. Client-generated idempotency keys on every money-moving request (UUID per logical operation, not per HTTP attempt) — persisted server-side with the operation's result: first request executes and records; retries return the recorded result without re-executing. Key scope + TTL designed per operation (a 24h key window covers retry storms; the key store is Redis-backed with durable fallback because losing it during an incident is losing the dedupe exactly when retries spike)
  2. State machines, not booleans: payment intent → authorized → captured → settled/refunded — every transition idempotent and conditionally applied (UPDATE ... WHERE state='authorized' — the WHERE clause is the correctness), every transition event-logged with actor + timestamp (the audit trail is the debugging trail)
  3. The outbox pattern for side effects: 'charge succeeded' must emit events (ledger, notifications, fulfillment) atomically with the state change — same-transaction outbox table + relay, because 'DB committed but event lost' (or inverse) is how downstream systems diverge from money truth
  4. Provider-boundary discipline: the PSP call itself can timeout ambiguously (did the charge happen?) — never blind-retry an ambiguous money call: query the provider's idempotent status endpoint (or use their idempotency keys — pass yours through), and model 'unknown' as an explicit state that a resolver reconciles, not an exception that a retry loop mangles

Reconciliation (the safety net that assumes everything above leaks):

  1. Three-way daily reconciliation: your ledger vs provider reports vs bank settlement files — every discrepancy classified (timing vs real) and aged; unresolved items page a human. This catches the bug classes no unit test imagines (provider double-settlement, currency rounding drift, the webhook you missed)
  2. Continuous invariant checks: sum of ledger movements per account = balance; no negative balances without credit lines; capture ≤ authorization — violated invariants alert immediately, because money bugs compound silently
  3. Webhook handling with the full at-least-once treatment (signature verification, dedup by event ID, reconciliation sweep for missed events)

The double-charge postmortem I'm preventing (tell it as the war story): the classic chain — mobile client timeout at 10s → user taps again → two requests, no idempotency key → both succeed 200ms apart. Every layer above kills it: the key dedupes the second request; the state machine's conditional transition rejects a second capture; reconciliation catches any variant that slips. Defense in depth because each layer will individually fail someday.

The operational tier: PSP failover (multi-provider routing with per-provider health + cost/auth-rate optimization), graceful degradation (PSP down → queue-and-retry for non-instant flows, fail-fast-and-honest for instant ones — never 'accept and hope'), and load-shedding that never sheds the webhook/reconciliation paths (ingesting money-truth outranks accepting new charges).

One-liner: 'idempotency keys end-to-end, conditional state machines, outbox-atomic side effects, explicit ambiguity states at the provider boundary, and three-way reconciliation that assumes the rest leaks — payments reliability is designing for the retry you know is coming and the discrepancy you haven't imagined yet.'

Design search for an e-commerce catalog: 50M products, 5K queries/s, sub-100ms, with merchandising control and an ML ranking roadmap.

The engine layer: Elasticsearch/OpenSearch-shaped inverted index — 50M products is modest (a few hundred GB indexed): ~20-30 shards (size for ~30-50GB/shard, and know that over-sharding is the more common sizing mistake), 2+ replicas for the 5K QPS read load, dedicated coordinating nodes, and index-per-version blue-green (reindex into products_v43, alias flip, instant rollback — mapping changes and analyzer updates become boring).

The indexing pipeline: catalog changes (price, stock, content) → CDC/events → indexing service → bulk writes; freshness tiers by field (stock/price near-real-time — a sold-out product in results is a trust bug — content/attributes minutes-fine); full reindex capability always warm (the blue-green path doubles as disaster recovery and backfill).

The query-understanding layer (where relevance actually lives):

  1. Normalization → spell correction ('running shoos') → synonym expansion (curated + learned: 'sneakers'≈'trainers') → attribute extraction ('red nike running shoes size 10' → brand:nike, color:red, category:running-shoes, size:10 — structured filters beat text matching for precision) → intent classification (navigational 'iphone 15 case' vs exploratory 'gifts for runners' get different retrieval strategies)
  2. Retrieval: BM25 over analyzed text fields (name² > brand > description weighting) + filters, plus vector retrieval for the semantic tail ('shoes for standing all day' matches products no keyword hits) — hybrid with rank fusion; the vector leg is where zero-result rates go to die

Ranking (the staged reality):

  1. Stage 1 ships without ML: BM25 relevance × business signals (sales velocity, margin, stock depth, ratings) via function scoring — honest, debuggable, and the baseline ML must beat
  2. Stage 2 — learning-to-rank: clickstream + conversion data → LTR model reranking the top ~200 candidates (feature vectors: text scores, popularity, price-competitiveness, personalization affinity) — served within the engine (LTR plugins) or as a rerank service inside the latency budget (retrieval 40ms + rerank 25ms + assembly leaves headroom under 100ms P99)
  3. The evaluation discipline that makes ML safe: offline judgment lists + NDCG, online interleaving/A-B on conversion-per-search, and a query-segment dashboard (head/torso/tail performance separately — models routinely win the head and butcher the tail; segment metrics catch it)

Merchandising control (the requirement engineers underweight): business users need — pinning/boosting (campaign products), blocklists, category-page curation, and rule layering that composes with ranking (rules as rerank overrides with audit trails, not index hacks). Plus the search-ops surface: zero-result-rate monitoring by query segment (the top product-gap signal the buying team will ever get), synonym/rule change previews ('what does this rule do to these 50 queries') before publish.

Facets + the long tail: aggregations on filtered sets (facet counts must reflect current filters), and cached results for head queries (the top 1K queries are ~30% of volume — a short-TTL cache carves real capacity).

One-liner: 'inverted index + hybrid retrieval under a query-understanding pipeline, business-signal ranking first and LTR when the clickstream earns it, blue-green indexes for fearless change, and merchandising rules as a composable audited layer — sub-100ms is the easy part; relevance governance is the system.'

The interviewer slides you a napkin: 'Design our system to survive an AWS regional outage. You have 30 seconds of questions, then design.' Show the questioning discipline, then the tiered answer.

The 30 seconds of questions (this IS the evaluation): 'What's the actual RTO/RPO the business signed — minutes, hours, or day? Which flows are revenue-critical vs deferrable? What's the data layer — relational, NoSQL, object? And what's the budget appetite — because the answer is a menu, not a design.' — Demonstrating that DR is a requirements problem before an architecture problem is worth more than any diagram; engineers who skip to Aurora Global Tables have failed a different test.

Then the menu (tiered, with costs and honest numbers):

  1. Backup & restore (RTO: hours-day, RPO: hours; cost: ~+5%): cross-region backup copies (snapshots, S3 CRR), IaC-rebuildable everything, quarterly restore drills. Right for: internal tools, deferrable workloads. The trap: untested backups — the restore drill is the product
  2. Pilot light (RTO: ~1h, RPO: minutes; cost: +10-20%): data replicating continuously (Aurora cross-region replica, DynamoDB global tables), core infra pre-provisioned but scaled to zero/minimal, scale-up + DNS flip on declaration. Right for: most B2B products whose customers tolerate a bad hour
  3. Warm standby (RTO: minutes, RPO: ~seconds; cost: +30-50%): full stack running scaled-down in region 2, taking a trickle of real traffic (the trickle is the design point — a standby that serves 1% continuously is proven; one at 0% is a hypothesis), health-checked Route 53 failover with pre-authorized automation. Right for: revenue-critical consumer products
  4. Active-active (RTO: ~0, RPO: ~0 for reads / partition-dependent for writes; cost: +60-100% and permanent architectural tax): the multi-region topology conversation (entity homing, conflict posture) — right for: the narrow tier where minutes of downtime = headlines

The cross-cutting hard parts (volunteer these unprompted):

  • The failover decision is the weakest link: who declares, on what signal, with what authority at 3am — automated for tier-1 with human override, documented thresholds ('sustained >X% errors for Y minutes'), because the average org loses more RTO to deciding than to executing
  • Dependencies fail regionally too: the DR audit includes third parties, container registries, secrets, CI/CD (can you even deploy during the outage?), and identity — your app being multi-region while Auth0-or-equivalent isn't is a design with a hole
  • N-1 capacity honesty: region 2 must absorb region 1's load on the worst day — 'multi-region' with both at 60% is a cascade schematic
  • Data-layer truth-telling: async replication means the RPO is a live number (lag SLO + alerting), and failover means potentially accepting seconds of loss — the business signs that, in writing, in advance
  • Drills are the deliverable: quarterly evacuation of real traffic for tier-1 — the first drill always finds the hardcoded region string; the third one is boring, and boring is the goal

Close with the meta-point: 'the deliverable isn't the architecture — it's a signed RTO/RPO per service tier, a costed option against each, automation matching the chosen tier, and a drill calendar proving the number quarterly. DR that isn't rehearsed is a diagram, and diagrams don't fail over.'

Design a webhook delivery platform (Stripe-quality) for your SaaS: guarantees, retries, ordering, and the customer-side failure modes you must absorb.

The contract to publish (design starts with the promise): at-least-once delivery, per-endpoint best-effort ordering (with explicit no strict global ordering — promising strict ordering couples your throughput to your slowest customer), signed payloads, retries with published schedule, and a dashboard where customers see their own delivery health — the transparency IS a feature tier.

The pipeline:

  1. Event capture via outbox: producing services write events transactionally with state changes (the outbox pattern again — webhook truth must not diverge from DB truth) → relay to the delivery platform's Kafka backbone
  2. Fan-out + subscription matching: event → matching endpoints (customer subscriptions with event-type filters) → per-delivery records (event × endpoint = a delivery with its own lifecycle — the granularity that makes per-endpoint isolation possible)
  3. Delivery workers with per-endpoint isolation (the core design point): deliveries queue per endpoint (or per customer shard) — one customer's dead endpoint must never head-of-line-block others; per-endpoint concurrency caps (don't hammer their server), circuit breakers (endpoint failing → back off aggressively, don't burn workers), and worker pools that a single pathological endpoint cannot exhaust
  4. Retry policy: exponential backoff with jitter over ~72h (Stripe-style published schedule: immediate, 1m, 10m, 1h, ... daily), terminal → dead-letter visible to the customer ('these 40 events failed permanently — replay?') with self-service replay (the support-ticket killer feature)

The customer-side failure modes you're absorbing (the question's heart):

  1. Slow endpoints: response timeout (5-10s hard), and slow ≠ failed handling (their 8s handler shouldn't eat retries — but does eat their concurrency slot; publish that math)
  2. Flapping/intermittent: circuit-breaker states smoothing the retry storm; delivery-health scoring surfaced to them
  3. The thundering-herd-on-recovery: endpoint dead 6h then healthy → 6h of queued deliveries; drain rate-limited (their recovery must not be your re-kill), oldest-first within ordering constraints
  4. Ordering reality: per-endpoint sequential delivery degrades throughput on slow endpoints — the design answer: sequence numbers in the payload (customers reorder/dedupe on their side; you document idempotent-consumer patterns) + best-effort ordering, rather than strict serialization. Teach the contract, don't fake the guarantee
  5. Security hygiene you enforce for them: HMAC signatures with rotation support, timestamp tolerance (replay-attack window), TLS-only, and egress through dedicated IP ranges they can allowlist

Platform-side operations: delivery-lag SLO per priority tier (P99 first-attempt < 5s), backlog-depth alerting per shard, event-storm protection (a bug emitting 10M events → per-producer rate limits + a bulk-cancel tool — you will need it), payload size caps with 'fat payloads → fetch-by-reference' guidance, and multi-tenant fairness quotas end-to-end.

One-liner: 'outbox-sourced events, per-endpoint isolated queues with circuit breakers and published backoff, sequence numbers over fake ordering promises, customer-visible health with self-service replay — a webhook platform is 20% delivery and 80% absorbing the internet's worst endpoints without letting them touch each other.'

You inherit a system doing 200ms P50 / 4s P99 and the CEO says 'make it fast.' Walk the latency-engineering discipline end to end.

First, translate the demand into an SLO worth engineering against: 'fast' → which endpoints, which percentile, at what cost? The P50/P99 gap (20x!) is the diagnostic headline — a healthy service runs P99 ≈ 3-5× P50; 20x means something intermittent dominates the tail: queueing, GC, lock contention, cold caches, retries, or a bimodal dependency. Tail latency is where user pain lives (at 30 requests/session, most sessions eat at least one P99) — and the tail is what pages you.

The diagnosis sequence (evidence before optimization):

  1. Distributed-trace the percentiles separately: sample traces at P99, not averages (averages lie; the P99 trace shows which span balloons). The usual suspects rank-ordered from real systems: a dependency's tail amplified by fan-out (call 10 services, your P99 is roughly their P99s compounded — fan-out amplification is the silent killer: at 10 parallel calls, you hit a per-call P99 ~10% of the time), retry storms adding whole extra round-trips, connection-pool exhaustion (queueing invisible to app metrics — pool wait time is the metric nobody graphs), GC pauses (bimodal histogram signature), cold cache paths, and lock convoys on hot rows
  2. Histogram everything (never averages): per-dependency latency distributions, pool wait times, GC pause distributions, queue depths — the shape (bimodal? long uniform tail? spikes?) names the disease

The fix toolbox, by mechanism:

  1. Cut the critical path: parallelize sequential calls (the waterfall→fan-out refactor is often -40% P50 alone), move non-essential work async (audit logging, analytics, notifications — off the request path), cache with intent (per-object TTL + stampede protection — a cache miss storm is a P99 event)
  2. Tame the tail specifically: hedged requests (send a second attempt when the first exceeds ~P95; take the winner — the classic Google trick, cuts P99 dramatically for idempotent reads at ~5% extra load), tight timeouts + budgets per hop (a 4s P99 usually hides a 3.8s timeout somewhere — timeout ladders must descend along the call chain), retry budgets (retries capped as % of traffic — retry storms convert one slow dependency into systemic collapse), and connection-pool right-sizing with wait-time alerting
  3. The data layer: the N+1 queries the ORM hid, missing indexes surfacing under scale (slow-query log P99, not average), hot-partition remediation, and read-replica offloading — the database is the final boss of most latency stories
  4. Load-shedding as latency protection: past the knee of the latency-throughput curve, everything queues — admission control (shed early, cheaply, fairly) keeps the served requests fast; a system at 95% utilization has no P99, only a queue

The program wrapper: latency budgets per hop (the 300ms endpoint budget decomposed: gateway 10, auth 20, service 80, DB 100, headroom 90 — now regressions localize), CI perf gates on the critical endpoints (the P99 regression caught in canary, not in the CEO's demo), and a weekly latency review while the program runs — latency work regresses without a ratchet.

Result shape from running this honestly: the typical outcome is P99 4s → 400-600ms (tail fixes: hedging, timeouts, pool sizing, one N+1 massacre) and P50 200 → 120ms (parallelization + caching) — and the durable artifact is the budget decomposition + dashboards that make the next regression a 10-minute diagnosis.

One-liner: 'trace the P99 not the average, hunt the intermittent (queues, GC, retries, fan-out amplification), fix the tail with hedging-timeouts-budgets, cut the path with parallelism and async, shed load before the knee — and institutionalize per-hop budgets so "fast" survives the quarter after the CEO stops asking.'

Design the architecture-decision process itself: ADRs, review boards vs paved roads, and how a 200-engineer org makes good decisions without a bottleneck. (The meta-question.)

The failure modes at both extremes (name them first): the Architecture Review Board that sees every design → weeks of queue, reviews by people without context, architecture theater where slides pass and reality diverges; versus full autonomy → 14 message brokers, security groups from folklore, and every team relearning the same $200K lessons. The design problem: scale judgment without centralizing it.

The system I build:

  1. ADRs as the decision substrate: lightweight records (context, options, decision, consequences — one page) in the repo they affect, PR-reviewed like code. The discipline isn't documentation for its own sake — it's that writing the options forces the thinking, and the archive turns 'why is it like this' from archaeology into a link. Metric that matters: ADRs written before implementation vs after (after = fiction)
  2. Tiered decision routing (the core mechanism): classify decisions by blast radius —
    • Type 1 (one team, reversible): team decides, ADR records it, nobody approves — autonomy is the default
    • Type 2 (crosses teams / hard to reverse / new external dependency): async design review — the doc circulates to a relevant reviewer pool (2-3 senior engineers with domain context, not a standing committee) with a 5-day SLA; objections block, silence approves
    • Type 3 (org-shaping: new datastore class, auth model, region strategy): the real architecture forum — but it meets on these only, maybe monthly, with the principal engineers who own the consequences The routing rules are published; misrouting is cheap to correct; and the default is the lowest tier (escalation requires a reason, de-escalation doesn't)
  3. Paved roads replace most reviews: the strongest architectural governance is a golden path so good that deviation is rare and deliberate — blessed stacks (the supported database/queue/framework menu), service templates with the decisions pre-made, and platform teams treating the road as a product. Now the review question shrinks from 'is this design good' to 'why are you leaving the road?' — a far cheaper question, answered in the ADR. Deviations aren't forbidden; they're owned (you leave the road, you own the snowplowing)
  4. Feedback loops that keep it honest: post-implementation ADR reviews on a sample (did the consequences section come true? — the org's calibration training), incident postmortems back-linking the ADRs that enabled the failure mode (decisions have observable outcomes; connect them), and pruning: ADRs superseded explicitly, the graveyard visible (a decision log full of silently-dead decisions teaches people to ignore it)
  5. The people layer: principal engineers as itinerant reviewers embedded in Type 2 flows (office hours, not gates), an explicit apprenticeship path (seniors co-reviewing to grow the reviewer pool — the bottleneck is always qualified reviewers, so manufacture them), and the cultural rule that review comments are questions with reasons, never vetoes from altitude

The metrics of a healthy system: decision lead time by tier (Type 2 P50 < 1 week), deviation rate from paved roads (rising = the road needs work, not the teams), ADR-before-build ratio, and repeat-incident classes traceable to ungoverned decisions (should trend to zero).

One-liner: 'route decisions by blast radius — autonomy for the reversible, async expert review for the crossing, a real forum only for the org-shaping — pave roads so most reviews become "why leave it", record everything as ADRs reviewed like code, and grow reviewers deliberately, because the scarce resource was never process; it's calibrated judgment, and the system's job is to compound it.'

Design a feature-flag and experimentation platform: evaluation architecture, consistency, and why 'percentage rollout' is harder than it looks.

The two products sharing one substrate (say this upfront): feature flags (operational: rollout control, kill switches — needs speed and reliability) and experimentation (statistical: A/B measurement — needs assignment rigor and analytics). Conflating them badly serves both; sharing targeting/evaluation infrastructure serves both well.

Evaluation architecture (the latency decision that shapes everything):

  1. Server-side: local evaluation with streamed rules — SDKs hold the full ruleset in memory (fetched at boot, updated via streaming/poll seconds), evaluate in-process (microseconds, zero network on the request path). The flag service being down means stale rules, never blocked requests — the availability posture that makes flags safe to put on every code path
  2. Client-side (browser/mobile): evaluate server-side at session start, ship the resolved assignments (payload of booleans, not the ruleset — rules leak targeting logic and bloat bundles); re-evaluate on meaningful context change
  3. The relay/edge tier for fleet scale: SDKs → relay proxies (regional rule caches) → control plane — the control plane serves rule changes, never sits on request paths

Why percentage rollout is harder than it looks (the interview's teeth):

  1. Stickiness: 10% rollout must be the same 10% every request — hash(user_id + flag_salt) % 100 < 10 gives deterministic assignment with no state; per-flag salts decorrelate flags (without salts, the same 10% of users get every experiment — a chronically-experimented-on cohort that biases everything)
  2. Ramp monotonicity: 10%→25% must keep the original 10% (bucket thresholds expand, never reshuffle) — reshuffling mid-rollout both breaks UX (features flickering off) and destroys experiment validity
  3. Identity is a policy question: anonymous → logged-in transitions (pre-login assignment by device ID, post-login by user ID — the migration moment needs a rule), multi-device consistency (user-keyed hashing gives it; device-keyed doesn't — choose per flag), and B2B's sharp edge: tenant-consistent flags ('10% of organizations, never a mixed org' — two users in one company seeing different UIs generates the worst support tickets)
  4. Rollout ≠ experiment: a ramp with monitoring answers 'is it safe'; only randomized assignment with holdouts answers 'did it work' — teams shipping at 100% then asking for impact numbers get archaeology, not answers

The experimentation half: assignment events logged (exposure logging at actual evaluation, not page load — exposure-dilution is the classic analysis bug), metrics pipeline joining exposures to outcomes, sequential-testing/CUPED-style variance reduction for faster decisions, guardrail metrics auto-monitored (the experiment improving clicks while tanking latency should stop itself), and a results UI with the statistics pre-interpreted (the platform's job includes preventing p-hacking by construction — locked analysis windows, pre-registered metrics).

Governance (the flag-debt machinery from the Git discussion, now platform-enforced): flag types with lifecycles, expiry dates + stale-flag reports wired to CI, kill-switch flags exempted and documented, audit logs on every rule change (a flag flip is a production deploy — treat its blast radius accordingly: staged rule rollouts, approval gates on high-traffic flags, and the 'who changed what when' trail because flag-flip incidents are real incidents).

One-liner: 'local evaluation with streamed rules so flags never block requests, salted deterministic hashing for sticky monotonic ramps, identity policy decided per flag (user, device, or tenant), exposure-logged experiments with guardrails — and lifecycle governance baked in, because the platform that makes flags easy to create owes the org the machinery that makes them die.'

Kafka vs SQS vs EventBridge vs Kinesis — the event-backbone decision for a 30-team organization, and the governance that matters more than the broker.

The technology decision (quickly, because it's the smaller half):

  1. SQS (+SNS): queues for task distribution — decoupled work, per-message ack, DLQs, effectively-infinite scale with zero ops. The default for 'service A hands work to service B'. Not for: replay, ordering beyond FIFO-lite, fan-in analytics
  2. Kafka (/MSK): the log — replayable, ordered-per-partition, multi-consumer, retention-as-time-travel: the backbone for event sourcing streams, CDC, analytics feeds, and 'many consumers, same events, different pace'. The cost: partition management, consumer-group operations, capacity planning — real platform work (MSK reduces, doesn't eliminate)
  3. EventBridge: the router — schema-aware pub/sub with content filtering, SaaS-event ingestion, and target fan-out; the integration fabric for 'this domain event, delivered to whoever registers interest' at low volume-per-rule. Not a throughput backbone; per-event cost and latency profile suit control-plane events, not data firehoses
  4. Kinesis: Kafka-shaped managed streaming with AWS-native ergonomics — right when you want log semantics without Kafka ops and within AWS gravity; shard economics and consumer-model limits are the trade

The 30-team answer is a portfolio, not a pick: SQS for work queues (most inter-service traffic), Kafka/Kinesis as the domain-event backbone (the streams multiple teams consume), EventBridge at the integration edges (SaaS in, cross-account routing) — with the platform team owning the backbone and paving the SQS road.

The governance that outweighs the broker choice (the senior half):

  1. Schema contracts with enforcement: a schema registry, compatibility rules (backward-compatible evolution as the default; breaking changes = new topic/version with migration windows), and CI validation on producers — because the org-scale failure mode is never 'Kafka fell over'; it's 'team A changed a field and six consumers silently broke.' Event schemas are APIs and get API governance
  2. Topic/stream taxonomy and ownership: naming conventions (domain.entity.event), every topic catalog-owned (the service-catalog discipline again), documented consumer registration (who reads this? — the answer must be queryable when you need to change it), and tiering (which streams are tier-1 with paging vs best-effort)
  3. The consumer contracts: at-least-once + idempotent consumers as the org default (documented, templated), DLQ conventions with replay tooling, lag SLOs with per-consumer-group alerting, and poison-message handling patterns in the paved road — every team reinventing these is 30 slightly-wrong implementations
  4. Event design review for the backbone: thin events (IDs + refs — consumers fetch current state) vs fat events (full payloads — consumers decoupled from producer APIs but schemas ossify): pick per stream deliberately; PII policy per topic (events are copies — data governance rides along); and the 'events are forever' retention/replay policy
  5. Cost and multi-tenancy: per-team throughput attribution on the shared backbone, quotas against the runaway producer, and the FinOps view (Kafka clusters and EventBridge per-event pricing have very different cost curves at different volumes — the portfolio assignment is partly an economics decision)

One-liner: 'queues for work, logs for events-of-record, routers for integration — the portfolio is straightforward; the organization-scale risk lives in schema evolution, ownership, and consumer discipline, so spend the platform effort on the registry, the catalog, and the paved-road consumer template rather than on broker benchmarks.'

Design CI/CD-to-production for a regulated fintech: change controls, segregation of duties, and deploy velocity that survives the auditors. (STAR)

Situation: payments company, PCI-DSS + SOC2 + local financial regs; the incumbent process: change advisory board meets Tuesdays, deploys require three signatures and a change ticket, lead time 2-3 weeks — and the actual effect: engineers batched changes into giant risky releases (the process created the risk it claimed to manage), plus a shadow path of 'emergency' changes that bypassed everything (40% of all changes, naturally).

Task: continuous delivery with control evidence that satisfies auditors — the thesis to sell internally: automated controls are stronger controls, and DORA-style velocity and compliance are allies, not enemies.

Action:

  1. Reframe the control objectives with compliance (weeks 1-4): sat with auditors/compliance and mapped what each control actually requires — segregation of duties = 'no single person authors and releases unreviewed changes' (satisfiable by enforced PR review + pipeline-only deploys — not by wet signatures); change approval = 'documented, authorized change with rollback plan' (satisfiable by PR + automated evidence bundle). Getting compliance to sign the mapping was the unlock — everything after is engineering
  2. The pipeline as the control plane: branch protection (no direct pushes, CODEOWNERS on money paths, required reviews), pipeline-only prod access (humans hold zero prod deploy credentials — OIDC'd pipelines only; SoD achieved structurally), and the evidence bundle generated per deploy: PR link + approvals, test/scan results, artifact digest + provenance chain, deploy timestamp + actor, rollback plan (the previous digest) — written to immutable storage. The audit binder became a query; the auditors got read access to the dashboard instead of quarterly screenshot archaeology
  3. Risk-tiered change classes (the CAB replacement): standard changes (the 90%: pre-approved pattern — service deploys through the full pipeline) flow continuously with no human gate beyond PR review; elevated changes (schema migrations, payment-flow logic, new external integrations) add a second domain-owner approval in the PR + staged rollout requirements; emergency path exists but is loud (break-glass pipeline, auto-created incident ticket, mandatory next-day review — measured, and the measure went to leadership monthly). The CAB itself → a monthly retrospective on change-failure metrics instead of a pre-approval gate
  4. Deploy safety as compliance evidence: progressive delivery (canary + automated rollback on golden-signal regression) — reframed for auditors as 'automated control effectiveness monitoring with automatic remediation' (they loved it: it's a detective and corrective control in their language), plus digest-pinned promotion (the artifact tested is bit-identical to the artifact deployed — an integrity control no manual process ever truly gave them)
  5. The cultural rollout: two lighthouse services first, auditor walkthrough of the working system mid-quarter (objections surfaced early — their main ask was better retention labeling on evidence, trivially added), then fleet migration with the old path sunset dated

Result: lead time 2-3 weeks → same-day for standard changes (P50 4 hours); deploy frequency 2/month → 15/day across the estate; change-failure rate dropped 60% (small changes fail less — the DORA correlation, now demonstrated internally); the shadow 'emergency' path fell from 40% of changes to 3% (when the paved road is faster than the workaround, the workaround dies); and the next SOC2 audit closed with zero change-management findings and one auditor comment — 'this is the best evidence trail we've seen' — worth quoting because it converts the next compliance conversation.

What they're testing: whether you treat compliance as a requirements-engineering problem (map objectives, don't cargo-cult rituals), build controls as pipeline structure rather than human ceremony, keep an honest-but-loud emergency path, and measure the outcome in both DORA and audit-findings currency.

Design a notification platform: email/SMS/push/in-app across 30 product teams — preferences, batching, and the 'why did I get 47 emails' incident class.

The platform boundary (the design's first decision): product teams emit notification intents ('user X: order-shipped, context {...}') — the platform owns everything after: preference resolution, channel selection, rendering, rate governance, delivery, and measurement. Teams calling Twilio directly is the anti-state; the platform is the egress for human-touching messages, because every cross-cutting concern below only works with a single chokepoint.

The pipeline:

  1. Intent ingestion: typed events with a registered notification type catalog (type → default channels, priority class, template refs, preference category) — new notification types are a reviewed registration, not a code path (this catalog is where governance lives)
  2. Preference + policy resolution (the correctness core): user channel preferences (per category × channel matrix), legal/consent state (marketing vs transactional split — CAN-SPAM/GDPR consent checks enforced here, not trusted to callers), quiet hours by user timezone, and channel fallback logic (push unregistered → email; critical + all-optional-channels-off → the mandatory-channel override for security/legal messages, documented and narrow)
  3. Rate governance + batching (the 47-emails killer):
    • Per-user frequency caps by priority class (marketing: N/week; product: N/day; transactional/security: uncapped) — enforced platform-wide, which is the structural fix for 'every team independently decided to send one reasonable email'
    • Digest batching: low-priority intents accumulate into per-user digests (hourly/daily by preference) — '12 new comments' as one push, not twelve; batching windows per notification type in the catalog
    • Cross-team dedup/collapse: same-entity updates within a window collapse (three status changes → latest-state notification)
  4. Rendering + delivery: template service (versioned templates, localization, per-channel constraints), then channel adapters (ESP, SMS gateway, APNs/FCM, in-app inbox store) with per-provider health, failover (multi-ESP), and per-channel rate/cost controls (SMS is money — budget alerts per team)
  5. The feedback plane: delivery/open/click events flowing back (per-type engagement dashboards — the data that tells a team their notification is ignored by 97% of recipients), bounce/complaint handling (auto-suppression lists — deliverability is a shared resource one team's spam can poison), and unsubscribe handling that actually propagates (the compliance incident otherwise)

The '47 emails' incident anatomy (tell it as the design justification): a retry loop in one team's service re-emitted intents without idempotency keys; three other teams shipped features the same week, each adding 'just one' notification; nobody had the cross-team view. The platform kills each vector: idempotency keys on intents (dedupe window), per-user caps (the 47 becomes 3 + a digest), and the catalog review (the cross-team view exists before shipping). The residual incident class — a legitimate event storm (incident notifications during an outage) — gets storm detection (per-type volume anomaly → auto-collapse into summary notifications + page the owning team).

Numbers to anchor scale: 30 teams × ~10M users ≈ 50-200M intents/day; the pipeline is Kafka-shaped with per-user ordering keys (preference resolution and batching need per-user serialization); P99 transactional latency <30s, digest jobs windowed; in-app inbox as its own store (read/unread state, retention) since it's a product surface, not just a channel.

One-liner: 'teams emit typed intents; the platform owns preferences, consent, caps, batching, rendering, and delivery through one governed pipeline — per-user frequency caps and idempotent intents are the structural fix for notification spam, the type catalog is where governance lives, and deliverability is managed as the shared resource it actually is.'

Storage engine internals for architects: B-trees vs LSM-trees — why it determines which database you pick, with the workload math.

Why an architect needs this: 'Postgres vs Cassandra' debates are mostly this question wearing product names — the storage engine determines the write/read/space trade-offs, and matching engine to workload is the actual decision.

B-trees (Postgres, MySQL/InnoDB, most relational): data in fixed-size pages organized as a balanced tree; writes modify pages in place (via WAL for durability + buffer pool for caching).

  • Read-optimized: point reads and range scans are O(log n) page walks with excellent locality — predictable, fast, no read amplification
  • The write cost: random writes dirty random pages (write amplification via page rewrites + WAL), heavy write loads fight checkpoint storms, and page splits fragment. Updates are cheap-ish (in place), but sustained high-ingest random writes are where B-trees strain

LSM-trees (Cassandra, RocksDB-based systems, ClickHouse-adjacent, Lucene conceptually): writes append to an in-memory memtable → flushed as immutable sorted files (SSTables) → background compaction merges files across levels.

  • Write-optimized: every write is sequential (memtable + WAL append) — ingest rates B-trees can't touch, natural fit for SSDs (no in-place rewrites)
  • The costs: read amplification (a point read may consult memtable + several SSTables — bloom filters mitigate), compaction as a background tax (CPU/IO that competes with your workload — compaction stalls are the LSM signature incident; leveled vs size-tiered compaction is choosing which amplification you pay), and space amplification (obsolete versions await compaction)

The workload math that decides:

  1. Write:read ratio and write pattern: sustained high-volume writes (metrics, events, logs — 100K+/s) → LSM territory; read-dominant with complex queries → B-tree. The crossover: B-trees handle far more write load than folklore suggests (a tuned Postgres ingests tens of thousands/s) — choose LSM for the workload shape, not the brand aesthetics
  2. Read pattern under LSM: point-reads-by-key with bloom filters = fine; wide scans across recently-written data = amplification pain; 'read-modify-write' patterns lose LSM's advantage entirely (the read is still there)
  3. Latency consistency: B-trees degrade smoothly; LSMs are fast until compaction debt bites (P99 spikes during compaction storms) — latency-SLO-critical systems on LSM engines need compaction as a monitored, capacity-planned workload (the operational surface people don't budget)
  4. Deletes are not free in LSM: tombstones persist until compaction (the Cassandra tombstone-scan incident is a genre) — delete-heavy workloads need explicit design (TTL-partitioned data beats row deletes)

The composite reality: modern systems mix (Postgres for transactional truth + ClickHouse/LSM for the analytics firehose; RocksDB inside stream processors), and indexes are engines too (every secondary index is another write-amplification payer regardless of engine).

One-liner: 'B-trees pay at write time for cheap predictable reads; LSMs pay at read-and-compaction time for cheap sequential writes — size the decision on write rate, read shape, delete pattern, and P99 tolerance, and remember compaction is a workload you're hiring, not a detail.'

The staff-engineer estimation interview: 'How many servers does it take to serve 1B video views/day?' — show the Fermi discipline.

Why they ask: not for the number — for whether you can decompose, state assumptions out loud, sanity-check against reality, and know which resource binds. The discipline is the answer.

Step 1 — shape the load: 1B views/day ÷ 86,400s ≈ 11.5K views/s average; peak factor 2.5x (diurnal + regional concentration) → **30K view-starts/s peak**. A 'view' isn't a request — it's a session: say 5-minute average watch at 5 Mbps → each concurrent viewer consumes 5 Mbps of egress. Concurrent viewers: 1B views × 5 min ÷ 1,440 min/day ≈ 3.5M average concurrent, peak ~8-9M.

Step 2 — find the binding resource (the senior move — it's bandwidth, not CPU): 8M concurrent × 5 Mbps = 40 Tbps peak egress. That number reframes the whole question: no origin fleet serves 40 Tbps — this is a CDN problem; 95%+ of bytes must come from edge caches (popular content follows a steep power law — the head 1% of videos is most of the traffic, ideal for caching).

Step 3 — size the layers:

  1. Edge (CDN): 40 Tbps ÷ 100 Gbps effective per edge server ≈ **4,000 edge servers** at peak (distributed across ~100+ PoPs) — whether owned (YouTube/Netflix build this) or bought (everyone else)
  2. Origin/mid-tier: cache misses + long-tail ≈ 5% of egress = 2 Tbps → ~200-400 origin-shield/storage servers backed by object storage
  3. The request-plane (the part people forget): view-starts hit APIs — playback auth, manifest generation, personalization, analytics beacons: 30K starts/s + heartbeats (8M concurrent × 1 beacon/10s = 800K events/s to the analytics pipe) → maybe 300-600 app servers (at a few thousand RPS each for lightweight API work) + the analytics ingestion tier (Kafka-shaped, ~100 brokers' worth at that event rate)
  4. Transcoding (if uploads exist), storage (say 500M videos × 1GB across renditions = 500PB — object storage economics), and the database tier for metadata (heavily cached, read-mostly — modest)

Step 4 — sanity-check against known reality: Netflix peaks in double-digit Tbps and runs ~thousands of OCA edge appliances — our 40 Tbps/4K-servers estimate is the right order of magnitude for a YouTube-scale property. Passing this check out loud is worth more than precision.

The answer to give: '~4-5K edge servers dominated by egress bandwidth, a few hundred origin and app servers, and an analytics pipe that's its own subsystem — order 5-10K machines total, with the insight that the binding constraint is Tbps of edge egress, not compute, which is why this business is a CDN with a website attached.'

What they're testing: decomposition (views → concurrency → bandwidth), identifying the binding resource, the power-law caching insight, remembering the control plane, and calibration against real-world systems — deliver assumptions loudly, arithmetic simply, and conclusions with error bars ('within 2-3x, which is what this method buys').

Design the API layer for a public platform: versioning, deprecation, gateway architecture, and the contract discipline that keeps 10,000 external developers happy.

The gateway architecture (the table stakes, quickly): edge → API gateway owning: authn (API keys/OAuth), rate limiting per tier, request validation against schemas, routing, and the telemetry plane (per-endpoint, per-consumer metrics) — one enforcement point for cross-cutting policy, with the gateway config itself as code (the routes/policies repo, reviewed like everything else).

Versioning strategy (the religious war, settled pragmatically):

  1. URL-path major versions (/v2/) — visible, cacheable, unambiguous; the pragmatic public-API default (header-based versioning is cleaner in theory and invisible in debugging)
  2. Majors are rare, expensive events — the discipline is making them almost never needed: additive-only evolution within a major (new fields, new endpoints — never renaming, removing, or changing semantics), explicitly documented as the compatibility contract ('we will add fields; your parser must tolerate unknown fields' — stated in the terms, enforced by their SDKs)
  3. The contract is machine-checked: OpenAPI specs as source of truth, CI breaking-change detection on every PR (spec-diff tooling failing the build on removed fields/changed types) — compatibility by tooling, not by reviewer vigilance

Deprecation as a product process (where platforms earn or torch trust):

  1. Published policy: minimum support windows (12-24 months for majors), Deprecation/Sunset headers on responses, changelog + advance notices
  2. Usage-driven retirement: per-consumer telemetry answers 'who still calls v1' — targeted outreach to the top consumers (the 20 integrations driving 80% of legacy traffic get white-glove migration help; the long tail gets tooling and time), staged brownouts near sunset (scheduled 5-minute v1 outages that make ignored emails impossible to ignore — announced, humane, effective)
  3. The metric: migrations completed without incident tickets — a deprecation that surprises anyone is a process failure

The developer-experience layer (the actual competitive surface):

  1. SDKs generated from the spec (drift-proof by construction), sandbox environments with realistic test data, webhooks done right (the earlier design), and error responses as a designed format (machine-readable codes, human-readable messages, request IDs for support correlation — error DX is where developers form opinions)
  2. Idempotency keys on all mutating endpoints (the payments discipline generalized — every serious API consumer retries), pagination/filtering conventions consistent across all endpoints (learn once, use everywhere), and rate-limit headers that let clients self-regulate
  3. Docs as a product: reference generated from spec (always accurate), guides hand-written (always helpful), and a changelog developers can subscribe to

The internal governance that makes the external promises keepable: API design review for new public surface (consistency board — the one place a standing review body earns its keep, because public API mistakes are forever), internal services never coupled to public API shapes (the public API is a facade with its own models — internal refactors must not ripple out), and consumer-facing SLOs with status page honesty.

One-liner: 'path-versioned majors made rare by additive-only evolution, breaking-change detection in CI, deprecation as telemetry-driven product work with real support windows, and DX — errors, idempotency, SDKs, docs — treated as the product surface it is; a public API is a promise with 10,000 witnesses, so make the promises structural.'

Design a data platform: operational stores to warehouse/lakehouse, CDC, and the 'analytics said X but production says Y' reconciliation problem.

The flow architecture:

  1. Extraction via CDC as the backbone: Debezium-shaped log-based capture from operational stores (Postgres WAL, MySQL binlog, DynamoDB streams) → Kafka — log-based because it's low-impact (no query load on prod), complete (every change, including deletes — polling-based extraction misses deletes, the classic silent corruption), and ordered per key
  2. Landing + modeling layers (the medallion shape): raw/bronze (immutable CDC events, schema-on-read, the replayable source of truth), staging/silver (deduplicated, type-cast, SCD handling — current-state tables materialized from the change stream), marts/gold (dbt-modeled business entities: orders, customers, revenue — the layer analysts touch). Lakehouse substrate (Iceberg/Delta on object storage) for the volume tiers + warehouse compute (Snowflake/BigQuery-shaped) where the SQL workloads live — increasingly the same system
  3. Orchestration + quality gates: dbt tests (uniqueness, referential integrity, freshness) as CI on the transformation layer, data contracts at the source boundary (producing teams own schema stability of what they emit — the schema-registry discipline extended to analytics), and freshness SLOs per mart with alerting ('revenue mart < 1h stale' as a paged promise)

The 'analytics says X, production says Y' problem (the question's heart — treat it as a system, not a ticket):

  1. The usual roots, rank-ordered from real incidents: timing windows (the report ran mid-pipeline — fix: freshness metadata surfaced in the BI layer, 'data as of 14:02' on every dashboard), semantic drift (production counts 'orders', the mart counts 'completed orders' — fix below), missed deletes/updates (polling-based legacy pipelines — fix: CDC), late-arriving data (events landing after their window closed — fix: watermark policies + restatement windows, documented), and genuine pipeline bugs (dedup logic, timezone handling — the eternal timezone handling)
  2. The structural fix — a metrics/semantic layer: business definitions ('active user', 'net revenue') defined once, in code, version-controlled (dbt metrics/semantic layer tooling) — dashboards and analysts consume the definition rather than re-implementing it in 40 slightly-different SQL queries. Most X≠Y incidents are two honest people with two honest definitions; the semantic layer makes the definition singular and its changes reviewable
  3. Continuous reconciliation as infrastructure: automated row-count and aggregate checks (source vs bronze vs gold) on schedule, anomaly detection on key metrics (revenue dropped 40% at 3am = data incident until proven otherwise — page the data on-call, and yes, there is a data on-call), and a published incident process for data (severity tiers, 'known bad data' banners on affected dashboards — silent bad data is how executive trust dies)

Governance riding along: PII classification propagating through layers (column-level lineage so 'where does email appear' is a query), access tiers per layer (raw = engineers; gold = the company), retention policies per zone, and cost attribution (the warehouse bill by team/model — the FinOps discipline, again).

One-liner: 'CDC into an immutable bronze layer, dbt-modeled silver and gold with tests and freshness SLOs, a semantic layer so business definitions exist exactly once, and reconciliation as scheduled infrastructure with a data on-call — because analytics-vs-production discrepancies are a system property you engineer against, not tickets you whack.'

Zero-downtime database migrations at scale: schema changes, backfills, and the expand-contract discipline on a 2TB table taking 30K writes/minute.

The constraints that shape everything: 2TB and 30K writes/min means: no long locks (a blocking ALTER is an outage), no naive backfill (a single-transaction UPDATE of 2B rows is a WAL explosion and replication-lag incident), and rollback-ability at every step (the migration will be interrupted someday).

The expand-contract choreography (the discipline itself):

  1. Expand: add the new structure alongside the old — new column (nullable, no default on old rows — on modern Postgres, ADD COLUMN ... DEFAULT is metadata-only and safe, but know your engine's lock behavior per DDL type; the lock table is memorized knowledge), new table, or new index (CREATE INDEX CONCURRENTLY, always — and know it can fail-invalid and needs cleanup+retry)
  2. Dual-write: application writes both old and new shapes (behind a flag — instantly reversible), with the write path ordered old-then-new and monitored for divergence
  3. Backfill in throttled batches: keyset-paginated batches (1-10K rows), sleep between batches, replication-lag-aware throttling (the backfill watches replica lag and backs off — the mechanism that separates production-grade from outage-grade), resumable via checkpoint (it will be interrupted), idempotent (it will be re-run). At 2TB expect days — that's fine; the system is fully operational throughout
  4. Verify: continuous consistency checking (sampled old-vs-new comparisons during dual-write + full reconciliation post-backfill) — the gate before any read switches
  5. Read cutover: flip reads to the new shape (flagged, gradual — canary percentage first), soak, watch
  6. Contract: stop old writes, then — after a full confidence window (a week, not an hour) — drop the old column/table. The contract step is where impatience creates the unrecoverable mistake; the old structure is your rollback until the moment it's gone

The operational hazards to name (each is a war story):

  1. Lock acquisition, not lock duration: even a fast DDL needs a brief exclusive lock — behind a long-running query, it queues, and everything queues behind it (the 5-second migration that caused a 5-minute outage). Fix: lock_timeout + retry loops on DDL (fail fast, retry, never queue-block)
  2. Replication lag as the backfill governor (mentioned, worth repeating — read replicas serving stale data during an unthrottled backfill is a customer-visible incident)
  3. ORM/framework defaults are hostile: the innocent AddColumn with default, the NOT NULL addition without a validation phase (Postgres: add constraint NOT VALID, then VALIDATE CONSTRAINT separately — validation scans without blocking) — migration linting in CI (squawk-style) catches the dangerous DDL patterns before review even starts
  4. The application-compatibility matrix: during expand-contract, two app versions run concurrently at every deploy — each migration step must be compatible with the versions on either side of it (the schema-change-and-code-change-in-one-deploy is the classic self-inflicted incident)

Tooling maturity: gh-ost/pt-osc-style tools (MySQL) or native strategies (Postgres) wrapped in a migration platform: linting, staging rehearsal against production-sized data (the 2TB rehearsal finds what the 2GB dev database never will), scheduled execution windows, and the checkpoint/throttle/verify machinery as paved road rather than per-team heroics.

One-liner: 'expand, dual-write, throttled resumable backfill, verify, cut reads, and only then contract — with lock-timeout discipline, lag-aware throttling, CI migration linting, and rehearsal at production scale; the schema change is easy, and the choreography around two live versions of code and data is the actual engineering.'

Design an ML feature store and serving platform — the architecture where data engineering meets 5ms P99 inference.

The problem it exists to solve (state it — most candidates can't): ML features are computed twice — offline for training (batch, historical) and online for serving (real-time, current) — and any skew between the two silently degrades every model. The feature store's job: define once, serve both, guarantee consistency.

The architecture:

  1. Feature definitions as code: transformations declared once (SQL/dataframe DSL — 'user_7d_order_count = count(orders) over 7d') in a registry with ownership, versioning, and documentation — the semantic-layer discipline applied to ML inputs. This single-definition property is the entire point; everything else is plumbing
  2. Offline path (training): definitions executed against the warehouse/lakehouse → point-in-time-correct training sets (the subtle hard part: joining features as they were at each training example's timestamp — leakage from future data is the classic silent model-inflation bug; PIT joins are the feature store's signature capability)
  3. Online path (serving): the same definitions materialized into a low-latency store (Redis/DynamoDB/purpose-built) keyed by entity (user_id, product_id) — batch features refreshed on schedule (hourly/daily materialization jobs), streaming features updated via the Kafka pipeline (click counts, session state — Flink-shaped aggregations writing through)
  4. Serving API: get_features(entity_ids, feature_list) at P99 < 5-10ms (it sits inside the inference request path — model latency budgets are tight and the feature fetch is often the biggest slice): batched lookups, connection pooling, feature-vector caching for hot entities, and graceful degradation (missing features → defaults + a metric, never a failed inference — the availability posture)

The consistency machinery (where trust lives):

  1. Training-serving skew monitoring: distribution comparison between offline values and what serving actually returned (logged at inference) — drift here means the two paths diverged (a timezone bug in one, a late-data policy difference) and the model is eating inputs it never trained on
  2. Freshness SLOs per feature (the 7d-order-count can be an hour stale; the fraud-velocity feature cannot) — declared in the registry, monitored, paged
  3. Backfill correctness: redefining a feature = recompute history + retrain consumers (feature versions are breaking changes with a migration story — the registry tracks which models consume which features, so blast radius is a query)

The platform-org design: features are shared assets (the fraud team's velocity features reused by ranking — discovery via the registry catalog), with ownership and quality contracts per feature group; cost attribution (streaming features are expensive — someone should decide per-feature whether real-time earns its keep vs hourly batch); and the serving tier is tier-1 production infrastructure (it's in the request path of every model — capacity, failover, and game days like anything else).

One-liner: 'define features once in a registry, execute them offline with point-in-time correctness and online through batch+streaming materialization into a millisecond store, monitor skew and freshness as SLOs — the feature store is the contract that keeps training and serving honest, and its serving tier is request-path infrastructure, not a data-team side project.'

The acquisition integration: you're handed a acquired company's platform (different cloud, different stack) and 18 months to integrate. Architect the program. (STAR)

Situation: acquisition closed — their stack: GCP + a Rails monolith + MySQL + their own auth; ours: AWS + services + Postgres + Okta-fronted SSO. 200 of their customers on contracts; 40 of their engineers with retention risk; board expectation: 'one platform' in 18 months. The unspoken truth to surface early: 'integrate everything' is usually the wrong goal — the right goal is one customer experience and one identity, with pragmatic backend convergence.

Task: define what 'integrated' actually means, sequence it by value and risk, and avoid the two classic failure modes: the forced big-bang rewrite that stalls both roadmaps, and the endless 'temporary' dual-stack that becomes permanent.

Action:

  1. Weeks 1-6 — discovery + the integration thesis: architecture archaeology (their system's real state — the acquired team's docs always oversell), dependency and contract mapping, and the tiered integration decision presented to leadership: unify now (identity/SSO, billing, security posture, incident process — the customer-visible and risk-bearing surfaces), converge over the window (data platforms, the product overlap areas), deliberately leave (their internal tooling that works, non-customer-facing systems where migration cost exceeds any benefit — with an explicit 'run indefinitely' owner and budget). Getting leadership to sign the not-integrating list was the political keystone — it converts scope from infinite to bounded
  2. The identity spine first (months 2-5): federated SSO across both products (their auth → OIDC against our IdP, customers migrated by cohort with dual-credential grace windows) — because every subsequent integration (support tooling, admin surfaces, API access) hangs off unified identity; it's the highest-leverage single system. Simultaneously: security baseline parity (their estate onboarded to our scanning/logging/IR — the acquired company is your attack surface from day one, and attackers read acquisition press releases)
  3. Customer-facing unification via facade (months 4-12): rather than migrating their backend, an API/experience facade — unified console, unified API gateway routing to both backends, unified billing events — customers see one product while backends converge underneath at their own pace (the strangler pattern at company scale). Their monolith keeps serving its workloads on GCP — cross-cloud is an ops tax we priced (~$8K/month egress + tooling duplication) and accepted vs a forced $3M lift-and-shift with zero customer value
  4. Data convergence pragmatism (months 6-16): CDC from their MySQL into our data platform (analytics unification early — leadership dashboards spanning both products bought enormous goodwill), product-data migration only where features merged (per-domain, expand-contract, cohort-by-cohort), and the largest overlap product decision — their reporting module vs ours — decided by usage data + a deliberate sunset with 12-month customer notice, not by politics
  5. The people architecture (the actual determinant): their engineers owned the facade and migration work for their systems (retention through agency — nobody stays to watch their system get strangled by strangers), embedded pairs across both platform teams, and their on-call/incident process merged into ours by month 3 (shared operational culture precedes shared architecture)

Result: SSO + security parity by month 5; unified console GA month 11; the deliberate-leave list (9 systems) still running happily with named owners; one product sunset executed with 4 customer escalations (of 200); cross-cloud steady-state cost accepted at ~$95K/year against the $3M migration it replaced; and 34 of 40 acquired engineers retained at month 18 — the metric I'd defend as the program's real success indicator.

What they're testing: whether you scope 'integration' as a decision rather than a default (the not-doing list), sequence by leverage (identity first, security immediately), use facades to decouple customer value from backend timelines, price cross-cloud honestly instead of ideologically, and treat the acquired team as the critical system being integrated.

Design service-to-service resilience: timeouts, retries, circuit breakers, and bulkheads — the composition rules that prevent retry storms and cascade failures.

The system-level truth first: each pattern is simple; composed wrongly they synthesize outages — the retry storm that turns one slow dependency into a platform-wide cascade is the canonical self-inflicted incident, and it's built from individually-reasonable retry configs.

The patterns and their composition rules:

  1. Timeouts — the foundation, and they must descend: every network call has a deadline; along a call chain, each hop's timeout must be shorter than its caller's remaining budget (gateway 2s → service A 1.5s → service B 1s → DB 500ms). Violate this (B's timeout > A's) and B keeps working for a caller that already gave up — wasted capacity at exactly the moment capacity is scarce. Deadline propagation (the remaining budget travels in headers/context — gRPC does this natively) is the mature version: downstream work self-cancels when the top-level request is already dead
  2. Retries — powerful and dangerous, so budget them: retry only idempotent operations, only on retryable errors (timeouts/5xx, never 4xx), with exponential backoff + jitter (synchronized retries are a DDoS you run against yourself), and — the rule that prevents storms — retry budgets: retries capped as a fraction of traffic (e.g., 10-20%); when the budget exhausts, fail fast. The multiplication math to say out loud: 3 layers each retrying 3x = up to 27x amplification on the innermost service during its worst moment — which is why the org-level rule is retry at one layer (usually the edge/client), not every layer
  3. Circuit breakers — stop asking a drowning service: per-dependency failure-rate tracking; past threshold → open (fail fast without calling) → periodic half-open probes → close on recovery. The design subtleties: per-endpoint granularity beats per-service (one slow endpoint shouldn't blacklist a healthy service), and the fallback behavior is a product decision per call site (cached data? degraded response? error?) — a breaker without a designed fallback just converts slow errors into fast ones (sometimes that's the win; decide, don't default)
  4. Bulkheads — partition the blast radius: per-dependency connection pools and concurrency limits (dependency X hanging consumes its pool, not the shared one — the classic outage: one slow downstream exhausts the app's single thread/connection pool and takes down every unrelated endpoint), per-tenant/per-class quotas, and priority tiers (health checks and payments traffic survive what analytics traffic doesn't)
  5. Load shedding + backpressure — the last line: admission control when queues grow (shed early and cheaply at the edge, by priority), queue-depth caps everywhere (unbounded queues convert overload into latency into timeout storms), and honest 429/503s with Retry-After so well-behaved clients self-regulate

The composition doctrine (the senior synthesis): timeouts bound the damage, retries-with-budgets recover the transient, breakers stop the futile, bulkheads contain the spread, shedding preserves the core — and the testing obligation: none of this is real until chaos-tested (inject latency into a dependency in staging; watch whether the composed behavior matches the diagram — it never does the first time; the second-order effects like 'the breaker opened and the fallback stampeded the cache' only appear under injection).

The observability contract: every pattern emits — retry rates per dependency (budget consumption trending = early warning), breaker state changes (paged for tier-1 dependencies), pool saturation, shed-request counts by class — because resilience machinery without telemetry is machinery you discover misconfigured during the incident it was built for.

One-liner: 'descending timeouts with deadline propagation, single-layer jittered retries under a budget, per-endpoint breakers with designed fallbacks, per-dependency bulkheads, and priority-aware shedding — each simple, composed deliberately, chaos-tested for the second-order effects, and instrumented — because cascade failures are almost always the resilience patterns themselves, composed by accident.'

You must present two architecture options to the CTO with a recommendation: build a real decision document — the trade-off analysis format that gets decisions made.

The meta-skill being tested: senior engineers don't present 'here are two options' — they present a decision with its reasoning exposed for challenge. The format below is the one that gets decisions made in one meeting instead of four.

The document structure (one page + appendices, always):

  1. The decision needed, in one sentence, with a deadline: 'Choose the multi-tenancy model for the new platform (shared-schema vs database-per-tenant) by March 15, blocking the Q2 enterprise roadmap.' — decisions without deadlines become standing debates
  2. Context + constraints (5 lines max): the forcing requirements (enterprise isolation demands, 3-person platform team, $X budget envelope, existing Postgres estate) — constraints do more deciding than preferences; surface them
  3. The options, honestly characterized: for each — what it is (2 lines), what it costs (build effort, run cost, team burden — in numbers: '~2 quarters, +$8K/month, one FTE ongoing'), what it risks (the failure modes and their blast radius), and — the credibility marker — the strongest argument against your own recommendation, stated better than its advocates would ('database-per-tenant genuinely simplifies compliance conversations and per-tenant restore; if enterprise deals above $500K/year become 30% of revenue, this analysis flips')
  4. The recommendation with its reasoning chain: not 'option A is better' but 'given constraint X and the reversibility asymmetry, A — because B's benefits materialize only in a future we can adapt toward if it arrives.' Reversibility is the master variable: one-way doors (data models, public APIs, identity) justify weeks of analysis; two-way doors justify a decision today and a revisit trigger — classify the decision explicitly
  5. The revisit triggers (the part almost nobody writes): 'we re-open this if: enterprise tenant count > 50, or a compliance regime demands physical isolation, or per-tenant ops burden exceeds 0.5 FTE' — pre-committed conditions convert 'were we wrong?' politics into 'did the trigger fire?' facts, and they let the CTO say yes faster because the yes is bounded
  6. What I need from you: the explicit ask ('approve A; fund the 2-quarter build; accept the enterprise-isolation risk until trigger') — decision documents without asks produce discussions, not decisions

The anti-patterns this format kills: the false-balance matrix (5 options × 12 criteria × subtle thumb-on-scale — everyone smells it), the advocacy doc (only your option's strengths — destroys trust the first time reality diverges), analysis paralysis theater (20 pages proving diligence instead of one page enabling judgment), and the unpriced recommendation ('A is more scalable' — scalable in what unit, needed when, costing what now?).

The delivery layer: pre-read circulated 48h ahead (the meeting is for challenge, not narration), the strongest skeptic pre-briefed 1:1 (objections surfaced privately improve the doc; objections surfaced performatively in the room harden into positions), and the decision recorded as an ADR the same day with the dissent noted — dissent-recorded-and-overruled is healthy; dissent-suppressed resurfaces as sabotage-by-lethargy.

One-liner: 'one page: the decision with a deadline, constraints before preferences, options priced in effort-dollars-risk, the case against yourself argued honestly, a recommendation hinged on reversibility, pre-committed revisit triggers, and an explicit ask — the goal isn't proving you analyzed; it's making the decision safe to make quickly.'

Design consistent hashing and explain where it actually shows up in systems you operate — from theory to the cache-resharding incident it prevents.

The problem it solves, concretely: distribute keys across N nodes such that when N changes, almost nothing moves. Naive hash(key) % N remaps ~all keys when N changes by one — for a cache fleet, that's a 100% miss storm; for a sharded store, a full rebalance. The incident this causes is a genre: 'we added one cache node and the database fell over.'

The mechanism: map both nodes and keys onto a hash ring (0 to 2³²); each key belongs to the first node clockwise from it. Adding a node claims keys only from its ring neighbors — ~1/N of keys move, the theoretical minimum. Two production-critical refinements:

  1. Virtual nodes (the load-balance fix): raw ring placement is lumpy (one node randomly owns 3x its share) — each physical node gets 100-1000 ring positions; the law of large numbers evens the load, and a node's departure spreads its keys across many successors instead of dumping them on one neighbor (which would cascade). Vnode count is also the heterogeneity dial: bigger boxes get more vnodes
  2. Replication on the ring: key → first R distinct nodes clockwise = the replica set (Cassandra/Dynamo lineage) — the same structure answers both placement and redundancy

Where you actually operate it (the answer's substance):

  1. Cache fleets (memcached/Redis client-side sharding): consistent hashing in the client — node loss = 1/N miss spike, not total. The resharding incident it prevents: scaling the cache tier from 10→12 nodes under naive modulo = ~100% invalidation = the thundering herd hits the database at peak (the exact outage that made consistent hashing famous). With the ring: ~17% of keys move, absorbed quietly
  2. Kafka consumer/partition assignment, Cassandra/DynamoDB partitioning, Envoy/gRPC ring-hash load balancing (session affinity without a session store — 'same user → same backend' surviving fleet churn), CDN request routing, and distributed rate-limiter key placement
  3. The modern variants worth naming: rendezvous (HRW) hashing — simpler, no ring state, each key independently picks its highest-scoring node; great for smaller node sets. Jump consistent hash — near-zero memory, but only supports numbered buckets (shrink-from-the-end) — right for fixed-pool sharding. Maglev — Google's lookup-table variant optimizing for even load + minimal disruption at LB speeds. Knowing when the simple ring loses (very small N, weighted nodes, lookup-rate extremes) is the depth marker

The operational corollaries:

  • Hot keys break every scheme — consistent hashing balances key counts, not key traffic; the celebrity key needs key-splitting (key#1..N sub-keys) or a dedicated tier — no hashing scheme fixes a power law
  • Ring state must agree: client-side rings that drift across a fleet (one deploy's node-list stale) silently double-place keys — centralize ring config or use server-side routing
  • Rebalance rate-limiting: even 1/N movement is a lot of bytes on big stores — throttled migration with dual-read fallbacks during the move

One-liner: 'the ring plus virtual nodes makes membership change cost 1/N instead of everything — it's the reason adding a cache node isn't an outage — and the operating knowledge is the corollaries: vnodes for smoothness, splitting for hot keys, agreed ring state, and throttled rebalances.'

Design the observability strategy itself: the three pillars are a lie, cardinality economics, and what 'good' looks like per dollar at a 60-service company.

The contrarian opener (earned, not edgy): 'metrics, logs, traces' as three parallel pillars is a vendor framing — operationally they're one system answering two questions: 'is something wrong?' (fast, cheap, aggregated — metrics/SLOs) and 'why?' (rich, expensive, sampled — traces/logs/events). Designing them as independent silos is how you get three bills and no answers.

The architecture of 'good' at 60 services:

  1. SLO-first, dashboard-second: every service declares 3-5 SLIs (availability, latency P99, correctness proxy) with burn-rate alerting — the paging surface is SLO burn, full stop; cause-based alerts (CPU high, pod restarted) become diagnostics, not pages (the single change that cuts pager noise 70% in most orgs). Golden-signal dashboards generated from a template per service — hand-crafted dashboards drift; generated ones stay honest
  2. Traces as the connective tissue: OTel auto-instrumentation as paved-road default, tail-based sampling (keep 100% of errors and slow requests, 1% of boring successes — the sampling policy IS the cost model), and trace-IDs stamped into every log line (the log↔trace join is where debugging speed actually lives — a log line without trace context is a clue without a case file)
  3. Logs with a caste system: structured JSON only; ERROR/WARN retained hot and searchable; INFO short-retention; DEBUG never ships from prod (enforced, not requested); high-volume request logging replaced by wide events (one rich event per request — the honeycomb-style model — beats 15 log lines per request for both cost and queryability)
  4. Cardinality economics (where the money goes to die): metrics cost = series count, not data points — label discipline enforced at ingestion (no user_id/request_id labels — those belong in traces), per-team series budgets with showback, and the top-offenders dashboard (one team's histogram-per-customer-per-endpoint 'quick fix' is a five-figure line item). The same discipline for log volume (per-team GB/day budgets) — observability spend runs 5-15% of infra spend; ungoverned it runs 30% and nobody can say why

The organizational layer (what actually determines quality):

  1. Correlation infrastructure: shared resource attributes (service, version, deploy-id, region on everything) so 'what changed' is a filter, not a meeting — deploy markers on every dashboard, because 80% of incidents are deploys and the fastest diagnosis is 'it started at 14:02, what shipped at 14:02?'
  2. Debugging as a designed workflow: alert → linked dashboard → exemplar traces (metrics-to-traces in one click) → correlated logs — walk it as a rehearsed path; if the on-call needs four tools and a prayer, the strategy failed regardless of data volume
  3. Ownership: observability platform team owns pipelines, budgets, and the paved road; service teams own their SLOs and instrumentation quality (reviewed at production-readiness) — and a quarterly 'observability debt' review pruning dead dashboards, unused metrics, and alerts nobody has actioned in 90 days (alert-that-never-pages and alert-that-always-gets-snoozed are both debt)

Per-dollar 'good', concretely: P90 incident time-to-diagnosis under 15 minutes; pager volume < 2 actionable pages/on-call/week with >80% actionable ratio; observability spend 8-12% of infra with per-team attribution; and 100% of tier-1 services with SLOs that a product manager has read — because an SLO nobody outside engineering agreed to is just a graph with anxiety attached.

One-liner: 'design for two questions — detection (cheap, aggregated, SLO-paged) and diagnosis (rich, sampled, correlated) — enforce cardinality and volume budgets like the money they are, make trace-context the join key across everything, and measure the strategy by time-to-diagnosis and pager sanity, not by terabytes ingested.'