System Design interviews represent the single most decisive hurdle in software engineering hiring. While coding interviews measure your tactical problem-solving in a sandboxed IDE, system design evaluates your strategic engineering maturity: how you reason about scale, navigate architectural trade-offs, defend CAP theorem constraints, and build fault-tolerant distributed software that survives catastrophic real-world hardware failures.
Whether you are a junior engineer transitioning to mid-level or a senior engineer aiming for Staff/Principal Architect at FAANG or high-growth tech companies, interviewers evaluate how you break down ambiguity. You must demonstrate how to transition from vague functional requirements to concrete back-of-the-envelope capacity numbers, high-level architectural topology, and low-level deep dives into database sharding, consistent hashing, distributed consensus, and concurrency control.
Who Should Use This Guide?
- College Freshers & Junior Developers (0–2 Years): Build rock-solid foundations in horizontal vs vertical scaling, Latency vs Throughput (Little’s Law), Layer 4 vs Layer 7 load balancers, Monolith vs Microservices decomposition, CAP Theorem (CP vs AP), and eliminating Single Points of Failure (SPOFs).
- Mid-Level Engineers (3–5 Years): Master Low-Level Design (LLD), object-oriented SOLID refactorings, GoF creational/structural/behavioral design patterns, thread-safe in-memory LRU caches, token bucket rate limiters, Clean Architecture, and Concurrency Control (Optimistic vs Pessimistic).
- Senior Backend Engineers (6–10 Years): Dive deep into High-Level Design (HLD), Consistent Hashing with virtual nodes, database sharding strategies, replication lag mitigation, caching strategies (Write-Through vs Write-Behind) and Cache Stampede prevention, Kafka vs RabbitMQ, CDNs, Bloom Filters, and B+ Trees vs LSM Trees.
- Lead & Principal Architects (8–12 Years): Review distributed transactions (2PC vs Saga orchestration/choreography), Event Sourcing & CQRS, distributed locking with Fencing Tokens, Raft vs Paxos consensus, Service Mesh (mTLS), hot/cold data tiering, and celebrity fan-out mitigation.
- Principal & Staff Architects (10–15+ Years): Study 10 complete architectural blueprints for classic real-world problems: TinyURL, Distributed Rate Limiter, Snowflake Unique ID Generator, Real-Time Chat (WhatsApp/Slack), Distributed Key-Value Store (Dynamo style), Web Crawler, YouTube/Netflix Video Streaming, High-Concurrency Flash Sale / Ticket Reservation, Yelp/Uber Proximity Geohash, and Distributed Notification Service.
Latency Numbers Every Distributed Systems Architect Must Know
Memorizing these order-of-magnitude latency numbers (originally published by Jeff Dean) is essential for conducting rapid, confident back-of-the-envelope calculations during live whiteboard interviews:
| Hardware / Network Operation | Typical Latency | Relative Scale Comparison | System Design Implication |
|---|---|---|---|
| L1 Cache Reference | 0.5–1 ns | 1 second (human scale) | Fastest on-chip CPU register / L1 memory access. |
| Branch Mispredict | 3–5 ns | 5 seconds | Pipeline stall caused by speculative execution failure. |
| L2 Cache Reference | 4–7 ns | 7 seconds | Secondary on-die CPU cache access. |
| Mutex Lock / Unlock | 15–25 ns | 25 seconds | In-memory thread synchronization primitive. |
| Main Memory (RAM) Reference | 100 ns | 1.5 minutes | Baseline for in-memory caches (Redis, Memcached). |
| Read 1 MB Sequentially from RAM | 250 µs (0.25 ms) | 3.5 days | Sequential memory throughput is extremely fast (~4 GB/s). |
| NVMe SSD Random Read | 10–50 µs | 12 hours | Modern enterprise NVMe solid-state storage. |
| Read 1 MB Sequentially from NVMe | 1 ms | 1.5 weeks | Sequential SSD reads approach bus limits (~1–3 GB/s). |
| Rotational HDD Seek | 2–10 ms | 1 to 5 months | 100,000x slower than RAM! Avoid random disk I/O. |
| Round-Trip in Same Datacenter | 0.5 ms | 1 week | Internal RPCs between microservices (gRPC / REST). |
| Cross-Country Round-Trip (US-East to US-West) | 60 ms | 2 years | Speed-of-light optical fiber transit delay. |
| Transatlantic Round-Trip (NY to London) | 100 ms | 3.5 years | Why multi-region active-active requires asynchronous replication. |
| Planetary Round-Trip (US to Australia) | 150–200 ms | 6 years | Absolute requirement for Edge CDNs and Point of Presence caching. |
Interactive Tool: System Design Back-of-the-Envelope Estimator
Struggling to calculate QPS, peak write throughput, storage replication overhead, or Redis cache sizing during interview practice? Use RTSALL’s free, interactive System Design Calculator to run instant capacity sizing models with custom DAU, read/write ratios, and 5-year retention projections.
Open System Design Calculator ➔What is System Design, and what is the difference between Horizontal Scaling and Vertical Scaling?
In distributed systems engineering, understanding the limits of scaling is the foundation of every architectural decision:
- Vertical Scaling (Scale-Up):
- Mechanism: Upgrading an existing server (e.g., resizing an AWS EC2 instance from
t3.mediumwith 4GB RAM tor6i.32xlargewith 1TB RAM and 128 vCPUs). - Pros: Zero architectural changes required; no network latency between nodes; immediate performance boost for monoliths and relational databases.
- Cons: Hard physical hardware ceilings (you cannot buy an infinite motherboard); exponential cost curves for high-end server hardware; Single Point of Failure (SPOF) — when the physical machine restarts for OS patches, the entire application is offline.
- Mechanism: Upgrading an existing server (e.g., resizing an AWS EC2 instance from
- Horizontal Scaling (Scale-Out):
- Mechanism: Distributing traffic across multiple commoditized server instances (Docker containers / Kubernetes pods) positioned behind a load balancer.
- Pros: Practically infinite scale; high availability and fault tolerance (if 2 out of 10 nodes crash, 8 continue serving traffic); cost-efficient commodity hardware; seamless autoscaling based on traffic spikes.
- Cons: Requires stateless application architecture; introduces inter-service network latency and serialization overhead; requires complex distributed data management and caching.
// Scalable Stateless Controller: Does NOT store session state in server RAM
[ApiController]
[Route("api/v1/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderRepository _repository;
private readonly IDistributedCache _cache; // Shared Redis Cluster
public OrdersController(IOrderRepository repository, IDistributedCache cache)
{
_repository = repository;
_cache = cache; // All nodes share centralized distributed cache
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetOrder(Guid id, CancellationToken ct)
{
// Any horizontal pod in a cluster of 50 instances can process this request
string cacheKey = $"order:{id}";
var cached = await _cache.GetStringAsync(cacheKey, ct);
if (cached != null)
return Ok(JsonSerializer.Deserialize<OrderDto>(cached));
var order = await _repository.GetByIdAsync(id, ct);
if (order == null) return NotFound();
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(order),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) }, ct);
return Ok(order);
}
}What is the difference between Latency and Throughput? How does Little's Law relate them?
Engineers often confuse latency and throughput, but they represent orthogonal dimensions of system performance:
- Latency (Delay):
The total duration elapsed from when a client dispatches an HTTP request to when it receives the complete response. It includes network transit, TLS handshake, gateway queuing, CPU processing, and database disk I/O.
Latency Percentiles: Never measure system latency using averages! A single 10-second query will skew the average. Professional systems measure percentiles:
p50 (Median):50% of requests are faster than this value.p95:95% of requests are faster than this value.p99:The worst 1% of requests. This represents your most active power users or complex database queries.
- Throughput (Capacity):
The total volume of work a system completes in a given timeframe (e.g., 25,000 QPS or 1.2 Gigabits/second).
- Little’s Law in Capacity Planning:
Formula:
L = λ × WL= Average number of concurrent active requests in the system.λ (Lambda)= System throughput (Requests per second).W= Average latency per request (in seconds).
Example: If an API processes 10,000 QPS and each request takes 50ms (0.05s) to execute, your server cluster must hold
10,000 × 0.05 = 500 concurrent active requests/connectionsin flight simultaneously.
Given:
Target API Throughput (λ) = 20,000 Requests/sec
Average Endpoint Latency (W) = 150 ms = 0.150 seconds
Calculate Required In-Flight Concurrency (L):
L = λ × W
L = 20,000 req/sec × 0.150 sec = 3,000 concurrent requests
If a single ASP.NET Core Linux container safely handles 300 concurrent requests:
Required Container Pods = 3,000 / 300 = 10 Pods (Minimum)
Add 30% Headroom for traffic bursts = 13 PodsCompare Layer 4 (Transport) and Layer 7 (Application) Load Balancers. What are the common balancing algorithms?
Load balancers sit between client devices and backend application clusters, distributing incoming network traffic to prevent any single server from becoming a bottleneck:
| Feature | Layer 4 (L4) Transport Load Balancer | Layer 7 (L7) Application Load Balancer |
|---|---|---|
| OSI Model Layer | Layer 4 (TCP, UDP) | Layer 7 (HTTP, HTTPS, gRPC, WebSockets) |
| Packet Inspection | Inspects only IP address and TCP/UDP port | Terminates TLS, parses HTTP headers, cookies, URL paths |
| Routing Intelligence | Simple connection routing | Route /api/orders to Service A, /api/users to Service B |
| Performance & Latency | Millions of packets/sec, sub-millisecond | Higher CPU overhead due to TLS decryption and HTTP parsing |
| Industry Examples | AWS NLB (Network Load Balancer), IPVS, HAProxy (TCP mode) | AWS ALB, Nginx, Envoy Proxy, YARP, Traefik |
Common Load Balancing Algorithms:
- Round Robin: Sequential assignment ($1 o 2 o 3 o 1$). Best when all servers have identical hardware and requests have uniform processing time.
- Weighted Round Robin: Assigns higher traffic volume to more powerful servers (e.g., Server A gets 3x more requests than Server B).
- Least Connections: Routes the incoming request to the server with the fewest active TCP connections. Ideal for long-lived transactions or file processing.
- IP Hash / Source Hash: Computes a hash of the client’s IP address to map them consistently to the same backend server (useful for stateful sessions).
- Consistent Hashing: Minimizes key remapping when backend servers are added or removed dynamically.
# Upstream clusters for microservices
upstream order_service {
least_conn; # Route to node with fewest active connections
server 10.0.1.10:5000 weight=3;
server 10.0.1.11:5000 weight=1;
server 10.0.1.12:5000 backup; # Active only if others fail
}
upstream user_service {
server 10.0.2.10:5000;
server 10.0.2.11:5000;
}
server {
listen 443 ssl http2;
server_name api.rtsall.com;
# Layer 7 Path-Based Routing
location /api/v1/orders/ {
proxy_pass http://order_service;
proxy_set_header X-Forwarded-For $remote_addr;
}
location /api/v1/users/ {
proxy_pass http://user_service;
proxy_set_header X-Forwarded-For $remote_addr;
}
}Monolithic Architecture vs Microservices: When should you start with a monolith and when to decompose?
The choice between Monolith and Microservices is primarily an organizational and team scaling decision, not merely a technical one (Conway’s Law):
- Monolithic Architecture:
- Pros: Rapid prototyping; simple single-repo deployment; zero network latency between modules (in-memory function calls); trivial ACID database transactions across tables; easy end-to-end debugging.
- Cons: Tight coupling over time; deployment bottleneck (one bug in reporting breaks checkout); slow CI/CD build times (45+ minutes); tech stack lock-in.
- Microservices Architecture:
- Pros: Independent deployment cycles (Team A deploys 10x a day without coordinating with Team B); independent horizontal autoscaling (scale only the payment service, not the entire app); polyglot technology freedom; fault isolation.
- Cons: High operational complexity (requires Docker, Kubernetes, CI/CD pipelines, distributed logging, service meshes); distributed transaction headaches (no multi-table SQL joins or 2PC); network latency and serializing overhead; eventual consistency bugs.
- When to Decompose:
- When engineering team size exceeds 25–50 developers and pull request merge conflicts paralyze releases.
- When specific components have radically divergent scaling profiles (e.g., image video processing needs GPUs while user profile needs minimal RAM).
- When strict regulatory boundaries require isolating payment card data (PCI-DSS) into an isolated perimeter.
MONOLITH ARCHITECTURE:
[Web / Mobile Clients]
│
▼
[Single Deployable Process: Orders, Users, Billing, Inventory]
│
▼ (Single ACID Transaction)
[Shared Relational Database]
MICROSERVICES ARCHITECTURE:
[Web / Mobile Clients] ──► [API Gateway / BFF]
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
[Order Service] [Billing Service] [Inventory Service]
│ │ │
▼ ▼ ▼
[Order DB (SQL)] [Billing DB (NoSQL)] [Inventory DB (SQL)]
▲ ▲ ▲
└────────── [Kafka Event Bus] ──────────────────┘Explain the CAP Theorem. Why is it impossible for a distributed data store to guarantee Consistency, Availability, and Partition Tolerance simultaneously?
Formulated by Eric Brewer in 2000 and mathematically proven by Seth Gilbert and Nancy Lynch in 2002, the CAP theorem defines the fundamental boundary of distributed data stores:
- P (Partition Tolerance): The system continues to operate despite arbitrary network packet drops or communication partitions between server nodes. In real-world distributed networks, hardware cables get cut, switches fail, and cloud availability zones experience latency. Network partitions are inevitable; therefore, you MUST support Partition Tolerance (P).
- The Core Trade-off during a Partition:
Imagine Node 1 (US-East) and Node 2 (US-West) lose network connectivity to each other:
- A client writes
Balance = $100to Node 1. - Another client reads
Balancefrom Node 2. - If you choose Consistency (CP System): Node 2 cannot synchronize with Node 1 over the severed network. To prevent returning stale, incorrect data, Node 2 rejects the read or returns an error. You preserved Consistency, but sacrificed Availability!
- If you choose Availability (AP System): Node 2 answers the read immediately with its existing data (
Balance = $50). The system remains 100% available, but returns stale data. You preserved Availability, but sacrificed Consistency!
- A client writes
| System Type | Guarantees | Behavior During Partition | Real-World Technologies |
|---|---|---|---|
| CP (Consistency + Partition) | Linearizable Consistency | Rejects writes/reads on minority partition to prevent split-brain | Google Spanner, CockroachDB, ZooKeeper, etcd, Redis (with sync replication) |
| AP (Availability + Partition) | Eventual Consistency | Both sides accept writes; reconciles conflicts later (CRDTs, Last-Write-Wins) | Apache Cassandra, Amazon DynamoDB, Couchbase, Riak |
[ Client A ] [ Client B ]
│ │
POST Balance=$100 GET Balance=?
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ Node 1 │ x x x x x x x │ Node 2 │
│ (US-East)│ NETWORK SPLIT │ (US-West)│
└─────────┘ x x x x x x x └─────────┘
CP Choice: Node 2 returns 500 Server Error (Consistent, but Unvailable)
AP Choice: Node 2 returns $50 (Available, but Inconsistent Stale Read)Compare ACID and BASE consistency models. When do you transition from ACID to BASE in distributed systems?
Modern system architects choose between ACID and BASE based on business domain requirements:
- ACID (Traditional RDBMS):
- Atomicity: All operations in a transaction succeed together, or all roll back completely (All-or-Nothing).
- Consistency: Data transitions only between valid states enforcing all foreign keys and constraints.
- Isolation: Concurrent transactions execute without interfering with one another (Serializable, Snapshot, Read Committed).
- Durability: Once committed, data survives power outages and crashes via write-ahead logging (WAL).
- Best For: Core ledger accounting, bank balances, inventory checkout.
- BASE (Distributed NoSQL & Event-Driven Systems):
- Basically Available: The system guarantees response availability to every request, potentially degraded.
- Soft State: System state can change over time even without client input due to background data replication.
- Eventual Consistency: If no new updates are made, all replicas will eventually converge to the same value within seconds or milliseconds.
- Best For: Social media feeds, user profiles, video view counters, shopping cart drafts.
// 1. ACID: Strict Relational Transaction (Atomic & Isolated)
using var transaction = await _dbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable);
try
{
accountA.Balance -= 500;
accountB.Balance += 500;
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync(); // Both updated or neither!
}
catch
{
await transaction.RollbackAsync();
}
// 2. BASE: Eventual Consistency (Asynchronous Event-Driven)
public async Task LikeVideoAsync(Guid videoId, Guid userId)
{
// Write like to fast local database/cache immediately
await _redis.SetAddAsync($"video:{videoId}:likes", userId.ToString());
// Publish event to Kafka/RabbitMQ: View count increments asynchronously in background
await _eventBus.PublishAsync(new VideoLikedEvent(videoId, DateTime.UtcNow));
// User sees their like immediately (Optimistic UI), while total count updates in ~1-2 seconds.
}What is the difference between a Forward Proxy and a Reverse Proxy? What are their key architectural use cases?
Understanding proxy placement establishes perimeter security and network boundary topology:
- Forward Proxy (Client-Facing):
[Clients in Office LAN] ──► [Forward Proxy] ──► (Public Internet) ──► [External Web Servers]- Client Anonymity: Hides internal IP addresses of client machines.
- Access Control: Blocks employee access to unauthorized domains (e.g., corporate web filters).
- Egress Caching: Caches external assets (e.g., operating system software updates) across all office laptops.
- Reverse Proxy (Server-Facing):
(Public Internet Users) ──► [Reverse Proxy] ──► [Internal Application Cluster / Microservices]- Hiding Server Topology: Clients only know the reverse proxy IP; internal container IPs remain private.
- SSL/TLS Termination: Decrypts HTTPS traffic at the edge, relieving backend pods of cryptographic CPU overhead.
- Load Balancing: Distributes incoming traffic across backend pods.
- Static Caching & Compression: Serves HTML, JS, CSS, and compressed images directly from memory cache.
FORWARD PROXY:
• Protects / Represents: CLIENTS
• Hides: Client IP from public servers
• Direction: Inside-out (Private LAN -> Internet)
• Common Tools: Squid, Charles Proxy, Corporate Zscaler
REVERSE PROXY:
• Protects / Represents: SERVERS
• Hides: Backend servers and database topology from clients
• Direction: Outside-in (Internet -> Internal Microservices)
• Common Tools: Nginx, HAProxy, Envoy, Cloudflare, Traefik, YARPHow does DNS (Domain Name System) work? Explain Anycast DNS and GeoDNS for global traffic routing.
DNS is the first hop of every network request on the internet:
- The 4-Step Recursive DNS Resolution Process:
- Browser / OS Cache: Inspects local memory. If absent, queries the local ISP Recursive Resolver.
- Root Nameserver (
.): Directs the resolver to the Top-Level Domain (TLD) nameserver (e.g.,.com). - TLD Nameserver (
.com): Directs the resolver to the domain’s Authoritative Nameserver (e.g., Cloudflare / AWS Route 53). - Authoritative Nameserver: Returns the final IP address (A / AAAA record) with a Time-To-Live (TTL) cache duration.
- GeoDNS (DNS-Based Routing):
The nameserver inspects the client’s IP subnet (EDNS Client Subnet – ECS). A user in London is resolved to an AWS Ireland IP, while a user in Tokyo is resolved to an AWS Tokyo IP. Disadvantage: Susceptible to stale DNS caching; if a datacenter crashes, clients with unexpired TTLs still query the dead IP.
- Anycast DNS (Border Gateway Protocol – BGP):
The same IP address (e.g.,
1.1.1.1) is broadcast from over 300 CDN PoPs worldwide. Internet routers automatically route packets to the topologically closest edge node via BGP shortest-path routing. If a datacenter goes down, BGP withdraws the route in seconds with zero DNS TTL propagation lag.
# Trace entire recursive DNS delegation hierarchy from root to authoritative
dig +trace rtsall.com
# Query specific A records and check TTL (Time To Live in seconds)
dig A rtsall.com +noall +answer
# Sample Output:
# rtsall.com. 300 IN A 104.21.50.2
# rtsall.com. 300 IN A 172.67.182.115
# Notice TTL = 300 seconds (5 minutes cache expiration)What is the difference between Stateless and Stateful architectures? Why is statelessness essential for web scale?
State management dictates how easily a system can survive node crashes and scale out horizontally:
- Stateful Architecture Hazards:
- Sticky Sessions: Load balancers must inspect cookies and route a user back to Server 4. If Server 4 crashes, the user’s active session, unsaved shopping cart, and temporary form data are lost permanently.
- Uneven Load Distribution: A few power users pinned to Server 4 can cause 100% CPU utilization while Server 5 and 6 sit completely idle.
- Slow Autoscaling: Nodes cannot be decommissioned during quiet hours without terminating active user sessions.
- The Stateless Paradigm:
- State is completely externalized: Authentication tokens (JWT) live on the client; session cache lives in a distributed Redis cluster; persistent records live in PostgreSQL.
- Web servers are purely stateless compute engines. If traffic doubles, Kubernetes boots 50 new pods instantly. If traffic falls, 50 pods are killed with zero data loss.
// 1. STATEFUL ANTI-PATTERN (Stored in local server RAM):
// Disastrous for horizontal scaling behind a load balancer!
HttpContext.Session.SetString("CartItems", jsonCart);
// 2. STATELESS BEST PRACTICE (Externalized to Redis Distributed Cluster):
public class StatelessCartService
{
private readonly IDatributedCache _redis;
public StatelessCartService(IDistributedCache redis) => _redis = redis;
public async Task SaveCartAsync(string userId, CartDto cart)
{
// Saved to centralized Redis: Accessible by ANY node in the cluster!
await _redis.SetStringAsync($"cart:{userId}", JsonSerializer.Serialize(cart),
new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromHours(24) });
}
}What is a Single Point of Failure (SPOF)? How do you systematically identify and eliminate SPOFs in system architectures?
A reliable distributed architecture assumes that every component will eventually fail (hardware crashes, disk corruption, fiber cuts, power outages):
- Tier-by-Tier SPOF Identification & Remediation:
- DNS Tier:
- SPOF: Relying on a single DNS provider.
- Remediation: Dual-DNS setup (e.g., AWS Route 53 + Cloudflare as primary/secondary nameservers).
- Load Balancer Tier:
- SPOF: A single Nginx reverse proxy VM.
- Remediation: Active-Passive pair with Keepalived (VRRP – Virtual Router Redundancy Protocol) floating IP, or cloud managed multi-AZ load balancers (AWS ALB/NLB).
- Application Tier:
- SPOF: A single monolithic application server.
- Remediation: Minimum of 2–3 stateless instances spread across multiple distinct Availability Zones (AZs).
- Database Tier:
- SPOF: A single primary database server.
- Remediation: High Availability cluster: Primary writer with synchronous replication to an automatic failover standby node in another AZ (e.g., AWS RDS Multi-AZ, SQL Server Always On Availability Groups).
- Network Tier:
- SPOF: Single internet service provider (ISP) or single top-of-rack network switch.
- Remediation: Dual redundant network interfaces (NIC bonding) and multi-homed BGP connections.
- DNS Tier:
[ Dual Anycast DNS (Route53 + Cloudflare) ]
│
┌────────────────────┴────────────────────┐
▼ ▼
[ Multi-AZ Load Balancer A ] [ Multi-AZ Load Balancer B ]
│ │
┌──────────┴──────────┐ ┌──────────┴──────────┐
▼ ▼ ▼ ▼
[App Pod 1] [App Pod 2] [App Pod 3] [App Pod 4]
(AZ-1: us-east-1a) (AZ-1: us-east-1a) (AZ-2: us-east-1b) (AZ-2: us-east-1b)
│ │ │ │
└──────────┬──────────┴───────────────────┴──────────┬──────────┘
▼ ▼
[ Primary Database (Write) ] ──(Sync Replication)──► [ Standby Replica (Auto-Failover) ]
(AZ-1: us-east-1a) (AZ-2: us-east-1b)Explain the SOLID principles with concrete Low-Level Design violations and refactoring patterns.
In LLD and technical code reviews, interviewers test whether you can identify subtle SOLID violations that lead to brittle, coupled codebases:
- S — Single Responsibility Principle (SRP):
- Violation: An
OrderServiceclass that calculates invoice tax, saves order data to SQL Server, and sends an email via SMTP. - Refactor: Split into three classes:
OrderProcessingService,OrderRepository, andNotificationService.
- Violation: An
- O — Open/Closed Principle (OCP):
- Violation: A discount calculator using a giant
switch(customerType)statement. Every time a new tier (e.g., ‘VIP’) is added, the existing class must be modified, risking regression bugs. - Refactor: Use the Strategy Pattern: create an
IDiscountStrategyinterface with separate implementations (RegularDiscount,VipDiscount).
- Violation: A discount calculator using a giant
- L — Liskov Substitution Principle (LSP):
- Violation: The classic Square inheriting from Rectangle. Calling
SetWidth(5)unexpectedly changes the height, breaking client assumptions. - Rule: Derived classes must fulfill all contracts of the base type without throwing unexpected
NotImplementedExceptionor altering invariant state.
- Violation: The classic Square inheriting from Rectangle. Calling
- I — Interface Segregation Principle (ISP):
- Violation: A fat
IWorkerinterface containingWork(),Eat(),Sleep(). ARobotWorkerclass is forced to throw exceptions onEat(). - Refactor: Decompose into granular interfaces:
IWorkable,IFeedable.
- Violation: A fat
- D — Dependency Inversion Principle (DIP):
- Violation: High-level
PaymentProcessordirectly instantiatesnew PayPalClient()inside its constructor. - Refactor: Depend on
IPaymentGateway, injected via constructor dependency injection.
- Violation: High-level
// 1. Contract Open for Extension
public interface IDiscountStrategy
{
decimal ApplyDiscount(decimal orderTotal);
}
// 2. Concrete Extensible Strategies (No existing code modified to add new tiers!)
public class RegularCustomerDiscount : IDiscountStrategy
{
public decimal ApplyDiscount(decimal total) => total * 0.95m; // 5% discount
}
public class VipCustomerDiscount : IDiscountStrategy
{
public decimal ApplyDiscount(decimal total) => total * 0.80m; // 20% discount
}
// 3. High-Level Context Closed for Modification
public class CheckoutService
{
public decimal CalculateFinalPrice(decimal total, IDiscountStrategy discountStrategy)
{
return discountStrategy.ApplyDiscount(total);
}
}Compare Factory Method, Abstract Factory, and Singleton. How do you implement a thread-safe Singleton in modern C#?
Creational patterns abstract the object instantiation process, making systems independent of how their objects are created, composed, and represented:
- Factory Method vs Abstract Factory:
- Factory Method: Defines an interface for creating an object, but lets subclasses decide which class to instantiate (e.g.,
DocumentFactory.CreateDocument()→PdfDocument). - Abstract Factory: A super-factory that creates families of related objects without specifying their concrete classes (e.g.,
UIFactory.CreateButton()andUIFactory.CreateCheckbox()returning dark-themed or light-themed components).
- Factory Method: Defines an interface for creating an object, but lets subclasses decide which class to instantiate (e.g.,
- The Singleton Pattern:
Restricts a class to a single instance (common for thread pools, hardware drivers, memory caches). Naive implementations with
if (_instance == null) _instance = new Singleton();suffer from severe race conditions under multi-threading.In modern .NET,
Lazy<T>provides compile-time double-check locking, thread-safety, and true lazy initialization with zero boilerplate.
public sealed class DatabaseConnectionPool
{
// Lazy<T> ensures thread-safe, double-check locked, lazy initialization
private static readonly Lazy<DatabaseConnectionPool> _instance =
new Lazy<DatabaseConnectionPool>(() => new DatabaseConnectionPool(), LazyThreadSafetyMode.ExecutionAndPublication);
// Private constructor prevents external instantiation
private DatabaseConnectionPool()
{
// Initialize socket pools, connection strings, etc.
}
// Public accessor for the single global instance
public static DatabaseConnectionPool Instance => _instance.Value;
public void ExecuteQuery(string sql)
{
// Query logic
}
}Compare Decorator, Adapter, and Proxy design patterns with real-world enterprise examples.
While all three patterns wrap an underlying object, their architectural intent is fundamentally different:
| Pattern | Primary Intent | Interface Compatibility | Enterprise Example |
|---|---|---|---|
| Adapter | Bridges two incompatible interfaces | Changes interface from $A o B$ | Adapting a legacy 3rd-party XML SOAP client to fit your clean IPaymentGateway JSON interface. |
| Decorator | Adds dynamic behavior without inheritance | Maintains same interface | GZipStream wrapping a FileStream (both inherit from Stream), adding compression. |
| Proxy | Controls and mediates access to an object | Maintains same interface | EF Core Dynamic Proxies for Lazy Loading; Security Proxies checking user permissions before calling service. |
public interface IProductRepository
{
Task<Product?> GetByIdAsync(int id);
}
// Core Implementation
public class SqlProductRepository : IProductRepository
{
public async Task<Product?> GetByIdAsync(int id) => /* Query SQL Server */ null;
}
// Decorator: Adds Redis Caching without modifying SqlProductRepository!
public class CachedProductRepository : IProductRepository
{
private readonly IProductRepository _inner;
private readonly IDistributedCache _cache;
public CachedProductRepository(IProductRepository inner, IDistributedCache cache)
{
_inner = inner;
_cache = cache;
}
public async Task<Product?> GetByIdAsync(int id)
{
string cacheKey = $"product:{id}";
var cached = await _cache.GetStringAsync(cacheKey);
if (cached != null) return JsonSerializer.Deserialize<Product>(cached);
var product = await _inner.GetByIdAsync(id); // Delegate to inner repository
if (product != null)
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(product));
return product;
}
}Compare Strategy, State, and Observer patterns. How does the Observer pattern form the foundation of Event-Driven systems?
Behavioral patterns organize algorithms and the assignment of responsibilities between objects:
- Strategy vs State Pattern:
- Strategy: The client chooses which strategy to pass in (e.g.,
Sort(new QuickSortStrategy())). The strategy is generally independent and stateless. - State: The context object transitions automatically between state classes (e.g.,
OrderState: Draft → Submitted → Shipped → Delivered). Each state class knows how to transition to the next state.
- Strategy: The client chooses which strategy to pass in (e.g.,
- The Observer Pattern & Event-Driven Architecture:
The Observer pattern decouples the Subject (event publisher) from its Observers (subscribers). When an event occurs (e.g.,
OrderPlaced), the subject iterates through registered observers and callsUpdate().In distributed systems, the Observer pattern scales out from in-memory C# events into distributed message brokers: Apache Kafka, RabbitMQ, and AWS SNS/SQS act as the centralized message bus coordinating independent microservice observers.
public record StockPrice(string Symbol, decimal Price);
// Subject / Publisher
public class StockTicker : IObservable<StockPrice>
{
private readonly List<IObserver<StockPrice>> _observers = new();
public IDisposable Subscribe(IObserver<StockPrice> observer)
{
if (!_observers.Contains(observer))
_observers.Add(observer);
return new Unsubscriber(_observers, observer);
}
public void UpdatePrice(string symbol, decimal price)
{
var data = new StockPrice(symbol, price);
foreach (var observer in _observers)
observer.OnNext(data); // Notify all subscribers
}
private class Unsubscriber : IDisposable
{
private readonly List<IObserver<StockPrice>> _obs;
private readonly IObserver<StockPrice> _ob;
public Unsubscriber(List<IObserver<StockPrice>> obs, IObserver<StockPrice> ob) { _obs = obs; _ob = ob; }
public void Dispose() => _obs.Remove(_ob);
}
}What is the Repository and Unit of Work pattern? Why is building a Generic Repository on top of EF Core considered an anti-pattern?
The Repository and Unit of Work patterns were introduced by Martin Fowler in Patterns of Enterprise Application Architecture:
- The Core Intent: Insulate business domain logic from database concerns (SQL queries, stored procedures, raw ADO.NET connections) and facilitate unit testing via mock repositories.
- Why ‘GenericRepository<T>’ on EF Core is an Anti-Pattern:
- DbSet<T> IS a Repository: It already provides
Find(),Add(),Remove(), and LINQ querying. Wrapping it inGenericRepository<T>simply duplicates existing functionality without adding value. - DbContext IS a Unit of Work: It tracks changes across multiple entities and writes them to the database in a single atomic SQL transaction when
SaveChangesAsync()is called. - Loss of LINQ Optimization: Generic repositories often expose
IEnumerable<T>instead ofIQueryable<T>, pulling entire database tables into RAM and executing filtering in memory instead of on SQL Server! - Disables EF Core Features: Completely hides projection (
.Select(...)), batching, split queries, and explicit loading.
- DbSet<T> IS a Repository: It already provides
- The Correct Pattern — Specific Domain Repositories:
If you use repositories, write Specific Domain Repositories with rich business methods:
IOrderRepository.GetActiveOrdersForCustomerAsync(Guid customerId), encapsulating complex queries and keeping DbContext internal to the persistence layer.
// BEST PRACTICE: Specific Domain Repository with optimized projections
public interface IInvoiceRepository
{
Task<InvoiceSummaryDto?> GetSummaryAsync(Guid invoiceId, CancellationToken ct);
Task<IReadOnlyList<Invoice>> GetOverdueInvoicesAsync(int daysOverdue, CancellationToken ct);
}
public class InvoiceRepository : IInvoiceRepository
{
private readonly AppDbContext _db;
public InvoiceRepository(AppDbContext db) => _db = db;
public async Task<InvoiceSummaryDto?> GetSummaryAsync(Guid invoiceId, CancellationToken ct)
{
// Executes optimized SQL projection (.Select) directly in database:
return await _db.Invoices
.Where(i => i.Id == invoiceId)
.Select(i => new InvoiceSummaryDto(i.Id, i.Total, i.Customer.Name))
.FirstOrDefaultAsync(ct);
}
public async Task<IReadOnlyList<Invoice>> GetOverdueInvoicesAsync(int days, CancellationToken ct)
{
var cutoff = DateTime.UtcNow.AddDays(-days);
return await _db.Invoices
.Include(i => i.Items) // Explicitly eager load necessary navigation properties
.Where(i => i.DueDate < cutoff && !i.IsPaid)
.ToListAsync(ct);
}
}Design a Thread-Safe In-Memory LRU (Least Recently Used) Cache. Explain the data structures and concurrency synchronization.
Designing an LRU cache tests fundamental data structure synthesis, algorithm complexity, and multi-threading concurrency control:
- Why an Array or List Fails: Searching an item is $O(N)$, and shifting elements on removal is $O(N)$. An LRU cache requires $O(1)$ for both
Get(key)andPut(key, value). - The Dual Data Structure Architecture:
- Hash Table (
Dictionary<K, LinkedListNode<CacheItem>>): Provides instant $O(1)$ key-to-node pointer lookup. - Doubly Linked List: Maintains access order. The most recently used (MRU) item sits at the Head; the least recently used (LRU) item sits at the Tail. Removing or moving a node takes $O(1)$ pointer rewiring.
- Hash Table (
- Concurrency Synchronization:
A simple
lockstatement works, but blocks readers during concurrent reads. UsingReaderWriterLockSlimallows concurrent threads to read simultaneously (EnterReadLock), upgrading to an exclusive write lock (EnterWriteLock) only when updating node positions or inserting new items.
public class LruCache<TKey, TValue> where TKey : notnull
{
private class CacheItem
{
public TKey Key { get; }
public TValue Value { get; set; }
public CacheItem(TKey key, TValue value) { Key = key; Value = value; }
}
private readonly int _capacity;
private readonly Dictionary<TKey, LinkedListNode<CacheItem>> _map;
private readonly LinkedList<CacheItem> _list;
private readonly ReaderWriterLockSlim _lock = new();
public LruCache(int capacity)
{
_capacity = capacity > 0 ? capacity : throw new ArgumentException("Capacity must be positive");
_map = new Dictionary<TKey, LinkedListNode<CacheItem>>(capacity);
_list = new LinkedList<CacheItem>();
}
public bool TryGet(TKey key, out TValue value)
{
_lock.EnterWriteLock(); // Needs write lock because accessing moves item to head!
try
{
if (_map.TryGetValue(key, out var node))
{
_list.Remove(node);
_list.AddFirst(node); // Move to head (Most Recently Used)
value = node.Value.Value;
return true;
}
value = default!;
return false;
}
finally { _lock.ExitWriteLock(); }
}
public void Put(TKey key, TValue value)
{
_lock.EnterWriteLock();
try
{
if (_map.TryGetValue(key, out var node))
{
node.Value.Value = value;
_list.Remove(node);
_list.AddFirst(node);
}
else
{
if (_map.Count >= _capacity)
{
// Evict LRU item from Tail
var tail = _list.Last!;
_map.Remove(tail.Value.Key);
_list.RemoveLast();
}
var newItem = new CacheItem(key, value);
var newNode = _list.AddFirst(newItem);
_map[key] = newNode;
}
}
finally { _lock.ExitWriteLock(); }
}
}Design a Thread-Safe In-Memory Token Bucket Rate Limiter at the class level.
A naive rate limiter uses a background timer thread to add tokens every second. This wastes CPU during idle periods and creates synchronization overhead across thousands of buckets:
- The Lazy Replenishment Pattern:
Instead of running a background timer, compute tokens dynamically upon each incoming request:
elapsedSeconds = (now - lastRefillTime) tokensToAdd = elapsedSeconds * refillRatePerSecond currentTokens = Min(capacity, currentTokens + tokensToAdd) lastRefillTime = now - Thread Safety: A lock ensures that multiple concurrent threads calling
AllowRequest()cannot read stale token counts or over-consume the bucket.
public class TokenBucketRateLimiter
{
private readonly long _capacity;
private readonly double _refillTokensPerSecond;
private double _currentTokens;
private long _lastRefillTimestampTicks;
private readonly object _lock = new();
public TokenBucketRateLimiter(long capacity, double refillTokensPerSecond)
{
_capacity = capacity;
_refillTokensPerSecond = refillTokensPerSecond;
_currentTokens = capacity; // Start full
_lastRefillTimestampTicks = Stopwatch.GetTimestamp();
}
public bool AllowRequest(int tokens = 1)
{
lock (_lock)
{
Refill();
if (_currentTokens >= tokens)
{
_currentTokens -= tokens;
return true; // Request Allowed
}
return false; // Rate Limit Exceeded (HTTP 429)
}
}
private void Refill()
{
long now = Stopwatch.GetTimestamp();
double elapsedSeconds = (double)(now - _lastRefillTimestampTicks) / Stopwatch.Frequency;
if (elapsedSeconds > 0)
{
double tokensToAdd = elapsedSeconds * _refillTokensPerSecond;
_currentTokens = Math.Min(_capacity, _currentTokens + tokensToAdd);
_lastRefillTimestampTicks = now;
}
}
}Explain Dependency Injection lifetimes in ASP.NET Core. What is a Captive Dependency, and why is it dangerous?
Dependency Injection (DI) manages object creation and dependency graphs. Mixing lifetimes improperly causes severe production concurrency crashes:
- The 3 DI Lifetimes:
Transient:Lightweight, stateless services. Created each time requested (services.AddTransient<IEmailValidator, EmailValidator>()).Scoped:Bound to the lifespan of an HTTP request. Shared across all controllers and services involved in that request (e.g.,DbContext). Disposed when HTTP request completes.Singleton:Created once on initial request and lives for the entire lifetime of the process (e.g., memory cache, HttpClient).
- The Captive Dependency Trap:
Suppose you inject a
Scoped AppDbContextinto aSingleton OrderProcessorService:- The Singleton is created once at startup. It holds a permanent reference to that initial
AppDbContext. DbContextis not thread-safe! When 50 concurrent HTTP requests hit the Singleton, 50 threads execute queries concurrently on the exact sameDbContextinstance, throwing immediateInvalidOperationException: A second operation started on this context before a previous operation completed!- The DbContext change tracker never gets disposed, accumulating tracked entities in memory until the container crashes from OutOfMemoryException.
- The Singleton is created once at startup. It holds a permanent reference to that initial
// DANGEROUS CAPTIVE DEPENDENCY:
// Singleton holding a Scoped DbContext!
public class BadBackgroundQueue // Registered as Singleton
{
private readonly AppDbContext _db; // Scoped service trapped in Singleton!
public BadBackgroundQueue(AppDbContext db) => _db = db;
}
// SAFE ARCHITECTURAL SOLUTION:
// Inject IServiceScopeFactory into the Singleton to create short-lived scopes
public class SafeBackgroundQueue // Registered as Singleton
{
private readonly IServiceScopeFactory _scopeFactory;
public SafeBackgroundQueue(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task ProcessJobAsync()
{
// Creates an isolated Scoped container on demand
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Safely use DbContext in a dedicated thread
await db.Orders.ExecuteUpdateAsync(/*...*/);
} // DbContext is cleanly disposed here!
}What is Clean Architecture (Onion Architecture)? Compare Entities, Value Objects, and Aggregates in Domain-Driven Design (DDD).
Clean Architecture (Robert C. Martin) and Domain-Driven Design (Eric Evans) create software systems that survive framework churn and database technology migrations:
- The Concentric Layers of Clean Architecture:
- Domain Layer (Core): Contains business rules, Entities, Value Objects, Domain Exceptions. Zero external package dependencies (no EF Core, no ASP.NET)!
- Application Layer: Use Cases, Commands, Queries, DTOs, and Interfaces (e.g.,
IOrderRepository,IPaymentGateway). - Infrastructure Layer: Concrete implementations: EF Core DbContext, Redis caches, SendGrid email clients, AWS S3 storage.
- Presentation Layer: Web APIs, Controllers, Minimal API endpoints, CLI tools.
- DDD Core Tactical Patterns:
- Entity: Identity matters. Two
Customerobjects with different IDs are different, even if their names are identical. - Value Object: Identity does not matter; attributes determine equality. Examples:
Money(Amount, Currency),Address. Value objects are strictly immutable. TwoMoney(10, "USD")objects are identical. - Aggregate Root: The master Entity that controls access to child entities within its boundary (e.g.,
Orderis the Aggregate Root forOrderItem). External code can never update anOrderItemdirectly; all mutations flow through methods onOrderto enforce business invariants.
- Entity: Identity matters. Two
// 1. Immutable Value Object (Value-based equality via C# record)
public record Money(decimal Amount, string Currency)
{
public static Money Zero(string currency) => new(0, currency);
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Cannot add different currencies.");
return new Money(Amount + other.Amount, Currency);
}
}
// 2. Aggregate Root (Enforces invariants on child OrderItems)
public class Order
{
public Guid Id { get; private set; }
private readonly List<OrderItem> _items = new();
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
public OrderStatus Status { get; private set; }
public Order(Guid id)
{
Id = id;
Status = OrderStatus.Draft;
}
// Business method enforcing invariants
public void AddItem(Guid productId, Money price, int quantity)
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot add items to a finalized order.");
var existing = _items.FirstOrDefault(i => i.ProductId == productId);
if (existing != null)
existing.IncreaseQuantity(quantity);
else
_items.Add(new OrderItem(productId, price, quantity));
}
}Compare Optimistic Concurrency and Pessimistic Concurrency. What are Compare-And-Swap (CAS) atomic operations?
Managing concurrent updates to shared resources is a core requirement of both low-level multi-threading and distributed database design:
- Optimistic Concurrency Control (OCC):
- How it works: Every record has a
RowVersionorVersionNumber. When updating:UPDATE Products SET Stock = Stock - 1, Version = Version + 1 WHERE Id = 42 AND Version = 5; - If rows affected = 0, another user modified the item first. The transaction throws a
DbUpdateConcurrencyException. - Best For: Read-heavy systems with low write contention. High throughput with zero database locking stalls.
- How it works: Every record has a
- Pessimistic Concurrency Control (PCC):
- How it works: Acquires exclusive row locks (
SELECT ... WITH (UPDLOCK, ROWLOCK)in SQL Server orSELECT ... FOR UPDATEin Postgres). Other sessions are blocked until the transaction commits. - Best For: High write-contention systems (e.g., concert ticket reservation where 50,000 users attempt to reserve seat A1 simultaneously).
- How it works: Acquires exclusive row locks (
- Compare-And-Swap (CAS) in Memory:
In CPU architectures, CAS is an atomic hardware instruction (
Interlocked.CompareExchangein C#). It updates a memory variable only if it matches an expected value:Interlocked.CompareExchange(ref location, newVal, expectedVal). Enables lock-free algorithms with zero thread sleeping.
// 1. Optimistic Concurrency in EF Core with RowVersion
public class Product
{
public int Id { get; set; }
public int Stock { get; set; }
[Timestamp] // Maps to SQL Server rowversion byte[]
public byte[] RowVersion { get; set; } = default!;
}
// Handled gracefully in business logic:
try
{
product.Stock -= 1;
await _dbContext.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
// Reload fresh data and notify user or retry automatically
await ReloadAndRetryAsync();
}
// 2. Lock-Free Atomic Compare-And-Swap (CAS) in Memory:
public class LockFreeCounter
{
private int _value;
public void Increment()
{
int initial, computed;
do
{
initial = _value;
computed = initial + 1;
}
// Atomic CPU CAS: updates _value only if it still equals initial!
while (Interlocked.CompareExchange(ref _value, computed, initial) != initial);
}
}What is Consistent Hashing, how does it solve the rehashing problem when nodes scale, and why are Virtual Nodes essential?
In distributed caching and database partitioning, consistent hashing is fundamental to achieving linear horizontal scalability without catastrophic cluster re-shuffling:
- The Failure of Simple Modulo Hashing:
In naive sharding, a key is routed via
ServerIndex = Hash(Key) % N. If you have 10 nodes (N=10) and add 1 node to handle traffic spikes (N=11), virtually every single key’s modulo target changes. RoughlyN / (N + 1) = 10/11 ≈ 91%of cached items become cache misses simultaneously. This causes an instant cascading database stampede that crashes the underlying database. - The Consistent Hash Ring Mechanism:
Both server identifiers (e.g., IP address, hostname) and data keys are hashed using a uniform 32-bit or 64-bit hash function (like MurmurHash3 or MD5) onto a circular ring range
[0, 2^32 - 1].- To store or read a key, hash the key and traverse clockwise along the perimeter until you hit the first server node.
- Adding a Node: Only keys located between the new node and its counter-clockwise predecessor must be migrated to the new node. On average, only
K / Nkeys move. - Removing/Failing a Node: Only keys mapped to the failed node are reassigned to its immediate clockwise successor. All other nodes remain completely unaffected.
- Why Virtual Nodes (VNodes) are Critical:
In practice, with a small number of physical nodes (e.g., 5 servers), standard hashing creates non-uniform distribution (hot spots where one node is assigned 60% of the ring arc). Furthermore, physical machines may have unequal hardware capacities.
Solution: Assign each physical machine 100 to 256 virtual nodes across the ring (e.g.,
Server1#1,Server1#2, …,Server1#200). This statistically homogenizes key distribution across the ring and allows assigning more vnodes to higher-capacity servers.
public class ConsistentHashRing<TNode>
{
private readonly int _virtualNodeReplicas;
private readonly SortedDictionary<uint, TNode> _ring = new();
private readonly ReaderWriterLockSlim _lock = new();
public ConsistentHashRing(int virtualNodeReplicas = 150)
{
_virtualNodeReplicas = virtualNodeReplicas;
}
public void AddNode(TNode node)
{
_lock.EnterWriteLock();
try
{
for (int i = 0; i < _virtualNodeReplicas; i++)
{
string vNodeKey = $"{node.ToString()}#VN{i}";
uint hash = ComputeMurmur3(vNodeKey);
_ring[hash] = node;
}
}
finally { _lock.ExitWriteLock(); }
}
public void RemoveNode(TNode node)
{
_lock.EnterWriteLock();
try
{
for (int i = 0; i < _virtualNodeReplicas; i++)
{
string vNodeKey = $"{node.ToString()}#VN{i}";
uint hash = ComputeMurmur3(vNodeKey);
_ring.Remove(hash);
}
}
finally { _lock.ExitWriteLock(); }
}
public TNode GetNode(string dataKey)
{
_lock.EnterReadLock();
try
{
if (_ring.Count == 0) throw new InvalidOperationException("Hash ring is empty.");
uint keyHash = ComputeMurmur3(dataKey);
// Binary search for the first node with hash >= keyHash (clockwise traversal)
foreach (var kvp in _ring)
{
if (kvp.Key >= keyHash) return kvp.Value;
}
// Wrap around to the first node on the ring
return _ring.First().Value;
}
finally { _lock.ExitReadLock(); }
}
private static uint ComputeMurmur3(string key)
{
byte[] bytes = Encoding.UTF8.GetBytes(key);
// Standard 32-bit MurmurHash3 algorithm implementation
return Murmur3Hash.Hash(bytes);
}
}How does Database Sharding work? Compare Range-based, Hash-based, and Directory-based sharding with re-sharding strategies.
When a database exceeds the write IOPS, RAM, or connection capacity of a single bare-metal server, sharding becomes mandatory. However, choosing the right sharding strategy is one of the most critical decisions in system architecture:
| Strategy | How It Works | Advantages | Disadvantages & Trade-offs |
|---|---|---|---|
| Range-Based | Partitions data by key ranges (e.g. A–F, G–M, or timestamps Jan 2026, Feb 2026). | Simple; range queries within the same shard are extremely fast. | Severe Hotspots: If partitioning by timestamp, 100% of current writes hit the newest shard while older shards sit idle. |
| Hash-Based | Shard = Hash(ShardKey) % TotalShards. Uniformly scatters records across all shards. | Uniform distribution; prevents single-node write bottlenecks. | Cross-Shard Range Scans: Querying WHERE CreatedDate BETWEEN X AND Y requires fan-out scatter-gather queries across all shards. |
| Directory-Based | A dynamic lookup service / table stores the exact shard mapping for each entity ID. | Extremely flexible; shards can be moved or split individually without rehashing. | Extra network hop to query the directory service; directory becomes a Single Point of Failure (requires Redis caching). |
The Re-Sharding Blueprint (Zero-Downtime Migration):
- Deploy new shard topology (e.g., doubling from 16 to 32 shards).
- Dual-Write Phase: Update application shard router to write to both old and new shards, with error handling ignoring errors on new shards.
- Backfill / Historical Migration: Run an asynchronous ETL background worker migrating historical data from old shards to new shards.
- Catch-up & Verification: Compare checksums between old and new shards.
- Flip Reads: Route read queries to new shards.
- Retire Old Shards: Disable dual-writing and decommission old hardware.
public interface IShardRouter
{
string ResolveShardConnectionString(string customerId);
}
public class HashShardRouter : IShardRouter
{
private readonly IReadOnlyList<string> _shardConnectionStrings;
public HashShardRouter(IConfiguration config)
{
_shardConnectionStrings = config.GetSection("DatabaseShards")
.Get<List<string>>() ?? throw new ArgumentException("Shards not configured");
}
public string ResolveShardConnectionString(string customerId)
{
if (string.IsNullOrWhiteSpace(customerId))
throw new ArgumentNullException(nameof(customerId));
// Use MurmurHash3 or FNV-1a for uniform integer distribution
byte[] bytes = Encoding.UTF8.GetBytes(customerId);
uint hash = Murmur3Hash.Hash(bytes);
// Map uniformly to shard pool
int shardIndex = (int)(hash % (uint)_shardConnectionStrings.Count);
return _shardConnectionStrings[shardIndex];
}
}Compare Database Replication Models: Synchronous vs Asynchronous vs Semi-Synchronous. How do you handle Replication Lag in production?
Replication is used to provide high availability, failover protection, and read scaling across distributed datacenters:
- Synchronous Replication:
The primary replica waits for all secondary nodes to write the transaction to disk/WAL before committing.
Trade-off: High durability (RPO = 0), but write throughput is bottlenecked by the slowest network link or node in the cluster. If one replica freezes, all writes halt.
- Asynchronous Replication (Default in MySQL / PostgreSQL / SQL Server AGs):
The primary commits locally and immediately returns HTTP 200 OK. An async background thread streams write-ahead logs to replicas.
Trade-off: Ultra-fast writes, but replicas suffer from Replication Lag. If the primary crashes before WAL transmits, uncommitted data is lost permanently (failover introduces RPO > 0).
- Semi-Synchronous Replication:
The primary waits for at least ONE secondary replica to acknowledge receiving and logging the event into its relay log before returning success to the client. Strikes a balanced trade-off between durability and latency.
- Production Strategies for Mitigating Replication Lag:
- Read-Your-Own-Writes Consistency: When a user updates their profile, set a short-lived cookie or session token:
last_write_timestamp = DateTime.UtcNow. For the next 10 seconds, route all read requests from that specific user to the Primary/Leader database. Other users continue reading from read replicas. - Replication Watermark / LSN Tracking: When writing, the primary returns the commit Log Sequence Number (LSN). The client sends this LSN with subsequent reads; the read router only dispatches the query to replicas whose current applied LSN is >= client LSN.
- Critical Domain Routing: Financial transactions, balance checks, and password changes ALWAYS read from the primary; product catalogs and review feeds read from replicas.
- Read-Your-Own-Writes Consistency: When a user updates their profile, set a short-lived cookie or session token:
public interface IDbConnectionResolver
{
IDbConnection GetReadConnection(HttpContext context);
IDbConnection GetWriteConnection();
}
public class ReplicationLagAwareDbResolver : IDbConnectionResolver
{
private readonly string _primaryConnStr;
private readonly IReadOnlyList<string> _replicaConnStrings;
public ReplicationLagAwareDbResolver(IConfiguration config)
{
_primaryConnStr = config.GetConnectionString("PrimaryDb")!;
_replicaConnStrings = config.GetSection("ReplicaDbs").Get<List<string>>()!;
}
public IDbConnection GetWriteConnection() => new SqlConnection(_primaryConnStr);
public IDbConnection GetReadConnection(HttpContext context)
{
// If user performed a write within the last 5 seconds, read from Primary to prevent phantom reads
if (context.Session.TryGetValue("LastUserWriteTicks", out byte[]? val) && val != null)
{
long lastWriteTicks = BitConverter.ToInt64(val);
var elapsed = DateTime.UtcNow - new DateTime(lastWriteTicks, DateTimeKind.Utc);
if (elapsed < TimeSpan.FromSeconds(5))
{
return new SqlConnection(_primaryConnStr);
}
}
// Round-robin / random load balance across healthy read replicas
int index = Random.Shared.Next(_replicaConnStrings.Count);
return new SqlConnection(_replicaConnStrings[index]);
}
}Contrast Caching Strategies: Cache-Aside vs Write-Through vs Write-Behind. How do you solve Cache Stampede (Thundering Herd)?
Caching is the most effective weapon against database overload, but incorrect invalidation or eviction strategies create severe production vulnerabilities:
- Cache-Aside (Lazy Loading):
The application coordinates both cache and DB. On read: read cache -> on miss, read DB -> write to cache. On write: write to DB -> invalidate (delete) cache key.
Rule: Always delete the cache key on write, rather than updating it. Updating creates race conditions between concurrent writes.
- Write-Through:
The application treats the cache as the main data store. The cache automatically updates the database synchronously before returning success. Guarantees consistency, but write latency includes DB latency.
- Write-Behind (Write-Back):
The application writes to cache; the cache returns immediately and buffers changes in an async queue to batch-write to disk. Ultra-fast writes, but risks data loss if the cache server crashes before flushing to disk.
- Cache Stampede (Thundering Herd) Solutions:
- Mutex Locking (Single Flight Pattern): When a cache miss occurs, the first thread acquires a lock (via Redis
SET resource_lock my_val NX EX 10) to query the DB and repopulate the cache. All other threads wait or return stale data, ensuring exactly 1 query hits the database. - Probabilistic Early Expiration (XFetch Algorithm): Instead of waiting for hard TTL expiration, background threads recompute and refresh the cache key early if
currentTime - (beta * delta * ln(random())) > expiry. Hot keys are refreshed seamlessly before they ever expire!
- Mutex Locking (Single Flight Pattern): When a cache miss occurs, the first thread acquires a lock (via Redis
public class StampedeProtectedCache
{
private readonly IDistributedCache _cache;
private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new();
public StampedeProtectedCache(IDistributedCache cache) => _cache = cache;
public async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, TimeSpan ttl)
{
// 1. Fast Path: Check distributed cache
var cached = await _cache.GetStringAsync(key);
if (cached != null)
return JsonSerializer.Deserialize<T>(cached)!;
// 2. Lock locally per key so only 1 thread fetches from DB
var keyLock = _locks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
await keyLock.WaitAsync();
try
{
// Double-check cache after acquiring lock
cached = await _cache.GetStringAsync(key);
if (cached != null)
return JsonSerializer.Deserialize<T>(cached)!;
// 3. Only 1 request executes expensive DB query
T freshData = await factory();
var options = new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = ttl };
await _cache.SetStringAsync(key, JsonSerializer.Serialize(freshData), options);
return freshData;
}
finally
{
keyLock.Release();
}
}
}Deep dive: Apache Kafka vs RabbitMQ. What are their core architectural differences, delivery guarantees, and use cases?
Architects must evaluate the fundamental architectural models of RabbitMQ and Kafka rather than treating them as interchangeable message queues:
| Dimension | Apache Kafka | RabbitMQ |
|---|---|---|
| Core Model | Distributed, partitioned Append-Only Commit Log. | AMQP Message Broker with Exchanges, Bindings, and Queues. |
| Data Flow | Pull Model: Consumers pull batches at their own pace based on offset. | Push Model: Broker pushes messages to active consumers based on prefetch limit. |
| Message Retention | Messages are immutable and retained on disk for days/weeks/forever, regardless of consumption. | Messages are transient; deleted from queue immediately upon consumer ACK. |
| Replayability | Full Replay: Any consumer can rewind its offset to re-process historical events. | No replay. Once ACKed, the message is gone forever. |
| Throughput | Ultra-high (1M+ msgs/sec per cluster via sequential disk I/O and zero-copy OS page cache). | Moderate to high (20k–50k msgs/sec per node; CPU/RAM intensive routing). |
| Routing Capabilities | Basic: Partition keys direct messages to specific partition numbers. | Advanced: Topic exchanges, Direct, Fanout, Headers, Dead-Letter Exchanges, Priority queues. |
Delivery Guarantees in Distributed Messaging:
- At-Most-Once: Commit offset / ACK before processing. Messages can be lost if consumer crashes during processing, but never duplicated.
- At-Least-Once (Standard): Process message, then commit offset / ACK. Messages are never lost, but if consumer crashes before ACK, message will be redelivered. Requires idempotent consumers.
- Exactly-Once Processing (EOS): Achieved in Kafka via transactional producer APIs and atomic two-phase commit with Kafka offsets and sink state.
public class IdempotentOrderConsumer
{
private readonly AppDbContext _dbContext;
private readonly ILogger<IdempotentOrderConsumer> _logger;
public IdempotentOrderConsumer(AppDbContext dbContext, ILogger<IdempotentOrderConsumer> logger)
{
_dbContext = dbContext;
_logger = logger;
}
public async Task ProcessMessageAsync(ConsumeResult<string, string> result, CancellationToken ct)
{
var orderEvent = JsonSerializer.Deserialize<OrderCreatedEvent>(result.Message.Value)!;
// Atomic deduplication: Check if MessageId / EventId was already processed
await using var tx = await _dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, ct);
try
{
bool alreadyProcessed = await _dbContext.ProcessedEvents
.AnyAsync(e => e.EventId == orderEvent.EventId, ct);
if (alreadyProcessed)
{
_logger.LogWarning("Duplicate event detected: {EventId}. Skipping.", orderEvent.EventId);
return; // Discard safely without re-executing business logic
}
// Execute core business mutation
var order = new Order { Id = orderEvent.OrderId, Amount = orderEvent.Amount };
_dbContext.Orders.Add(order);
// Record processed event identifier in the same ACID transaction
_dbContext.ProcessedEvents.Add(new ProcessedEventRecord
{
EventId = orderEvent.EventId,
ProcessedAt = DateTime.UtcNow
});
await _dbContext.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
}
catch (Exception ex)
{
await tx.RollbackAsync(ct);
_logger.LogError(ex, "Failed processing event {EventId}", orderEvent.EventId);
throw; // Trigger retry or DLQ
}
}
}How do Content Delivery Networks (CDNs) and Edge Caching work? Explain Cache-Control headers, Stale-While-Revalidate, and Invalidation.
Modern applications rely on CDNs (Cloudflare, AWS CloudFront, Fastly) not just for static images, but for dynamic API caching, DDoS mitigation, and edge compute execution:
- How Anycast DNS Routes Traffic:
A single IP address is announced simultaneously from hundreds of CDN datacenters globally using BGP (Border Gateway Protocol). Internet routers automatically direct a client’s packet to the topologically closest CDN Edge PoP in under 10 milliseconds.
- Key HTTP Caching Headers:
Cache-Control: public, max-age=31536000, immutable: Used for asset-hashed bundles (e.g.app.b8c2f1.js). Browsers and CDNs cache the file for 1 full year without ever revalidating with the origin server.Cache-Control: s-maxage=3600, max-age=60:s-maxageapplies specifically to shared public caches (CDNs), telling the edge to cache for 1 hour, while private user browsers only cache for 60 seconds.stale-while-revalidate=86400: If the cache entry has expired, the CDN immediately serves the stale copy to the client (sub-10ms response), while firing an asynchronous background fetch to the origin to update the edge cache.ETag & If-None-Match: Client sends previous ETag hash; if content has not changed, origin returns304 Not Modifiedwith zero response body bytes.
- Cache Invalidation with Surrogate-Keys (Cache-Tags):
Purging individual URLs via CDN APIs is slow and fragile. Instead, the origin attaches a
Cache-Tag: product-1234, category-electronicsheader on responses. When product 1234 is edited, the backend sends a single API call to the CDN:PurgeByTag("product-1234"). The CDN instantly evicts all pages, widgets, and API responses containing that tag globally in <150ms.
public class EdgeCacheHeaderMiddleware
{
private readonly RequestDelegate _next;
public EdgeCacheHeaderMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
context.Response.OnStarting(() =>
{
if (context.Response.StatusCode == StatusCodes.Status200OK &&
HttpMethods.IsGet(context.Request.Method))
{
// Serve stale content instantly while revalidating in background for 24 hours
context.Response.Headers["Cache-Control"] =
"public, max-age=300, s-maxage=3600, stale-while-revalidate=86400";
// Attach Surrogate-Keys / Cache-Tags for Cloudflare / Fastly instant tag-based purging
if (context.Items.TryGetValue("CacheTag", out var tag))
{
context.Response.Headers["Cache-Tag"] = tag?.ToString();
context.Response.Headers["Surrogate-Key"] = tag?.ToString();
}
}
return Task.CompletedTask;
});
await _next(context);
}
}What is a Bloom Filter? How does it mathematically guarantee zero False Negatives, and where is it applied in distributed databases?
In massive distributed datasets, searching disk or memory for keys that don’t exist wastes tremendous IOPS and CPU. Bloom filters solve this problem with incredible space efficiency:
- Mathematical Working Mechanism:
- Initialize a bit array of
mbits, all set to0. - Configure
kindependent, uniform cryptographic or non-cryptographic hash functions (e.g., MurmurHash3, CityHash). - Adding an Element: Feed the key to each of the
khash functions to producekarray positions. Set the bit at each of those positions to1. - Querying an Element: Hash the key with the same
kfunctions.- If ANY of the bits at those positions is
0, the element is DEFINITIVELY NOT in the set. (Zero False Negatives). - If ALL bits are
1, the element MIGHT be in the set. (Small, mathematically tunable False Positive probabilityp).
- If ANY of the bits at those positions is
- Initialize a bit array of
- Optimal Sizing Formulas:
For
nitems and desired false positive probabilityp:- Bit array size:
m = - (n * ln(p)) / (ln(2)^2)(roughly 10 bits per item for a 1% false positive rate). - Optimal number of hash functions:
k = (m / n) * ln(2) ≈ 0.7 * (m / n)(roughly 7 hash functions for 1% error).
- Bit array size:
- Real-World Distributed Systems Applications:
- LSM Storage Engines (Cassandra, RocksDB, Bigtable): Each SSTable on disk has an in-memory Bloom filter. If the Bloom filter returns false, the engine skips reading the SSTable from NVMe entirely!
- Web Crawlers (Googlebot): Checks if a discovered URL has already been crawled before enqueueing, storing 1 billion URLs in just ~1.2 GB of RAM.
- Medium / CDN Caching: Avoids caching “one-hit wonders” (items requested only once).
public class BloomFilter<T>
{
private readonly BitArray _bitArray;
private readonly int _bitSize;
private readonly int _hashFunctionCount;
public BloomFilter(int expectedElements, double falsePositiveRate = 0.01)
{
// Optimal size calculations
_bitSize = (int)Math.Ceiling(-1 * expectedElements * Math.Log(falsePositiveRate) / Math.Pow(Math.Log(2), 2));
_hashFunctionCount = (int)Math.Ceiling((_bitSize / (double)expectedElements) * Math.Log(2));
_bitArray = new BitArray(_bitSize);
}
public void Add(T item)
{
var (hash1, hash2) = ComputeDoubleHash(item);
for (int i = 0; i < _hashFunctionCount; i++)
{
// Kirsch-Mitzenmacher optimization: Gi(x) = H1(x) + i * H2(x)
int combinedHash = Math.Abs((int)((hash1 + i * hash2) % _bitSize));
_bitArray.Set(combinedHash, true);
}
}
public bool MightContain(T item)
{
var (hash1, hash2) = ComputeDoubleHash(item);
for (int i = 0; i < _hashFunctionCount; i++)
{
int combinedHash = Math.Abs((int)((hash1 + i * hash2) % _bitSize));
if (!_bitArray.Get(combinedHash))
return false; // 100% guarantee: Item is NOT present!
}
return true; // Probable match (may be false positive)
}
private (uint Hash1, uint Hash2) ComputeDoubleHash(T item)
{
byte[] bytes = Encoding.UTF8.GetBytes(item?.ToString() ?? string.Empty);
uint h1 = Murmur3Hash.Hash(bytes, seed: 0);
uint h2 = Murmur3Hash.Hash(bytes, seed: h1);
return (h1, h2);
}
}Compare B+ Trees and LSM Trees (Log-Structured Merge-Trees). Why do Cassandra and RocksDB use LSM Trees while SQL Server and Postgres use B+ Trees?
Storage engine architecture determines the fundamental I/O performance characteristics of modern databases:
| Dimension | B+ Tree (SQL Server, PostgreSQL, MySQL InnoDB) | LSM Tree (RocksDB, Cassandra, ScyllaDB, LevelDB) |
|---|---|---|
| Write Mechanism | In-Place Updates: Overwrites specific 8KB/16KB disk pages. Requires random disk writes. | Append-Only: Writes to in-memory MemTable (SkipList) + sequential Write-Ahead Log (WAL). Zero random disk writes. |
| Disk Flush Flow | Dirty pages flushed via buffer pool checkpointing. | Full MemTable flushed sequentially as immutable SSTable (Sorted String Table). |
| Background Maintenance | Page split handling and index defragmentation. | Compaction: Background threads merge and deduplicate multiple SSTables, purging deleted tombstones. |
| Write Performance | Moderate. Bottlenecked by random I/O and page locking. | Extremely Fast: Sequential write speeds match raw NVMe bus capacity. |
| Read Performance | Fast: O(log_B N). Exactly one leaf page contains the target record. | Slower: Must check MemTable and potentially multiple SSTables on disk (mitigated by Bloom filters). |
| Storage Efficiency | Lower. Pages have internal fragmentation (30–40% empty space after splits). | High: SSTables are immutable and sequentially sorted, allowing aggressive compression (Snappy/ZSTD). |
LSM Tree Architecture Flow:
[Incoming Write] ──> [Write-Ahead Log (Disk Sequential)]
│
└───> [MemTable (RAM SkipList)] ──(When Full)──> [Flush to L0 SSTable]
│
[Background Compaction]
│
[L1 SSTables] ──> [L2 SSTables]
Workload Type Recommended Engine Primary Justification
--------------------------------------------------------------------------------------
Read-Heavy OLTP (90% Reads) B+ Tree (Postgres/SQL) Fast point lookups; single-page reads
Write-Heavy Ingest / Time- LSM Tree (Cassandra/ Append-only sequential disk writes;
Series (80% Writes) RocksDB) zero random write stalls
Range Scans on Clustered Key B+ Tree Leaves linked sequentially on disk
High Compression Required LSM Tree Immutable SSTables compress up to 70%How do Real-Time Communication Protocols compare: WebSockets vs Server-Sent Events (SSE) vs HTTP Long-Polling vs gRPC Streaming?
Selecting the wrong real-time protocol creates catastrophic connection overhead, firewall blocking, and mobile battery drain:
- HTTP Long-Polling:
Client sends an HTTP request; the server holds it open until data is available. Once returned, the client immediately initiates a new request.
Drawbacks: Severe HTTP header overhead on every message, high connection churn, and race conditions during reconnect.
- WebSockets (RFC 6455):
Begins with an HTTP/1.1 Upgrade handshake, then switches to a persistent, bidirectional binary/text TCP framing protocol.
Best For: Interactive 2-way real-time collaboration (Figma, Discord voice/text, online multiplayer games, trading desks).
Gotchas: Does not automatically reconnect; does not support HTTP/2 multiplexing natively; requires custom load balancer sticky sessions and heartbeat ping/pongs.
- Server-Sent Events (SSE):
Standard HTTP connection with
Content-Type: text/event-stream. The server pushes text messages to the client indefinitely.Best For: Unidirectional server-to-client notifications, live financial ticker updates, and AI chat token streaming (like ChatGPT).
Benefits: Built-in automatic reconnection, event IDs, works natively over HTTP/2 (multiplexing 100 streams over a single TCP connection), and effortlessly bypasses corporate firewalls.
- gRPC Streaming:
Runs exclusively on HTTP/2 with binary Protocol Buffers serialization. Supports Client Streaming, Server Streaming, and Bidirectional Streaming.
Best For: Low-latency internal microservice communication where type safety and strict schema contracts are required.
[ApiController]
[Route("api/v1/[controller]")]
public class StreamController : ControllerBase
{
private readonly ILiveFeedService _feedService;
public StreamController(ILiveFeedService feedService) => _feedService = feedService;
[HttpGet("ticker")]
public async Task GetLiveTickerStream(CancellationToken ct)
{
Response.Headers.Append("Content-Type", "text/event-stream");
Response.Headers.Append("Cache-Control", "no-cache");
Response.Headers.Append("Connection", "keep-alive");
// Stream updates over standard HTTP connection
await foreach (var tick in _feedService.SubscribeAsync(ct))
{
string payload = JsonSerializer.Serialize(tick);
string sseMessage = $"id: {tick.Sequence}\nevent: priceUpdate\ndata: {payload}\n\n";
await Response.WriteAsync(sseMessage, Encoding.UTF8, ct);
await Response.Body.FlushAsync(ct);
}
}
}What is the Gossip Protocol, and how do distributed clusters (Cassandra, Consul, Redis Cluster) use it for Failure Detection and Membership?
In clusters containing hundreds or thousands of nodes, centralized heartbeats to a master node create bandwidth bottlenecks and single points of failure. The Gossip Protocol decentralizes cluster coordination:
- Epidemic Information Dissemination:
Every
Tmilliseconds (e.g., 1000ms), each node randomly selectskpeer nodes (e.g., 3 peers) from its membership list and transmits its known cluster state (heartbeat generation numbers and node health).Information spreads exponentially like an infection: within
O(log N)gossip rounds, 100% of nodes in a 1,000-node cluster learn about any state change or new joiner. - The SWIM Failure Detector (Structured Weakly-Consistent Infection-Style):
Traditional heartbeats struggle with transient network blips. SWIM separates failure detection from dissemination:
- Direct Ping: Node A sends a
Pingto Node B. If B replies withAckwithin timeout, B is healthy. - Indirect Ping: If Node B does not respond, Node A sends
Ping-Req(B)tokauxiliary peer nodes. These peers attempt to ping B. If any peer gets an Ack, B is marked alive (avoiding false alarms due to routing issues between A and B). - Suspect State: If indirect pings fail, Node B is not immediately marked dead; it is marked Suspect. A gossip broadcast is emitted: ‘Node B is Suspect’.
- Refutation: If Node B is alive (e.g., it was briefly paused during garbage collection), it increments its incarnation number and gossips: ‘I am Alive’, refuting the suspicion.
- Declaration of Death: If no refutation arrives before the suspicion timeout expires, the cluster transitions Node B to Dead and re-routes traffic.
- Direct Ping: Node A sends a
public class GossipNode
{
public string NodeId { get; }
private readonly ConcurrentDictionary<string, NodeState> _membership = new();
private long _heartbeatSequence;
public GossipNode(string nodeId)
{
NodeId = nodeId;
_membership[NodeId] = new NodeState(NodeId, _heartbeatSequence, DateTime.UtcNow, NodeStatus.Alive);
}
// Runs on a background timer every 1 second
public void ExecuteGossipRound(IReadOnlyList<GossipNode> allKnownPeers)
{
Interlocked.Increment(ref _heartbeatSequence);
_membership[NodeId] = new NodeState(NodeId, _heartbeatSequence, DateTime.UtcNow, NodeStatus.Alive);
// Pick 3 random peers
var randomPeers = allKnownPeers
.Where(p => p.NodeId != NodeId)
.OrderBy(_ => Random.Shared.Next())
.Take(3)
.ToList();
foreach (var peer in randomPeers)
{
peer.ReceiveGossipDigest(NodeId, _membership.Values.ToList());
}
}
public void ReceiveGossipDigest(string fromNode, IReadOnlyList<NodeState> incomingStates)
{
foreach (var remote in incomingStates)
{
_membership.AddOrUpdate(remote.NodeId, remote, (key, local) =>
{
// Accept higher heartbeat sequence numbers
if (remote.HeartbeatSequence > local.HeartbeatSequence)
{
return new NodeState(remote.NodeId, remote.HeartbeatSequence, DateTime.UtcNow, remote.Status);
}
return local;
});
}
}
}
public record NodeState(string NodeId, long HeartbeatSequence, DateTime LastSeen, NodeStatus Status);
public enum NodeStatus { Alive, Suspect, Dead }Explain Distributed Transactions: Why does Two-Phase Commit (2PC) fail at scale, and how does the Saga Pattern solve it?
Maintaining data consistency across isolated microservice databases without crippling system throughput is a classic distributed systems challenge:
- Why Two-Phase Commit (2PC) Fails at Scale:
- Phase 1 (Prepare): Coordinator sends
CanCommit?to all databases. Every database executes up to the commit point and holds database locks. - Phase 2 (Commit): If all vote Yes, coordinator issues
DoCommit; otherwise,Abort. - Fatal Flaw (Blocking Coordinator Failure): If the coordinator node crashes after Phase 1, participating databases cannot decide on their own whether to abort or commit. They must hold database locks indefinitely, stalling subsequent transactions and crashing the entire platform.
- Throughput Degradation: Lock duration is bound by the slowest network link across datacenters.
- Phase 1 (Prepare): Coordinator sends
- The Saga Pattern:
A Saga is a sequence of local transactions:
T1, T2, ..., Tn. Each local transaction updates the database of a single service and publishes a domain event. If transactionTifails, the Saga executes compensating transactionsCi, C(i-1), ..., C1in reverse order to undo changes. - Choreography vs Orchestration:
- Choreography: Decentralized. Services listen to events and decide the next step (e.g., PaymentService listens to
OrderCreated, emitsPaymentProcessed). Pros: Loose coupling. Cons: Hard to visualize; high risk of cyclic dependencies. - Orchestration: Centralized. A dedicated orchestrator state machine directs services what to do via command messages. Pros: Single point of truth for workflow state; easy to handle compensations. Cons: Additional service to manage.
- Choreography: Decentralized. Services listen to events and decide the next step (e.g., PaymentService listens to
public class OrderCheckoutSagaOrchestrator
{
private readonly IInventoryService _inventory;
private readonly IPaymentService _payment;
private readonly IShippingService _shipping;
private readonly ILogger<OrderCheckoutSagaOrchestrator> _logger;
public OrderCheckoutSagaOrchestrator(
IInventoryService inventory,
IPaymentService payment,
IShippingService shipping,
ILogger<OrderCheckoutSagaOrchestrator> logger)
{
_inventory = inventory;
_payment = payment;
_shipping = shipping;
_logger = logger;
}
public async Task<bool> ExecuteSagaAsync(OrderCheckoutContext context, CancellationToken ct)
{
// Step 1: Reserve Inventory
bool inventoryReserved = await _inventory.ReserveStockAsync(context.OrderId, context.Items, ct);
if (!inventoryReserved)
{
_logger.LogWarning("Saga aborted at Step 1: Out of stock.");
return false;
}
// Step 2: Authorize Payment
bool paymentSucceeded = await _payment.ChargeAsync(context.OrderId, context.Amount, ct);
if (!paymentSucceeded)
{
_logger.LogError("Step 2 failed. Initiating compensating transaction: Release Stock.");
await _inventory.CompensateReleaseStockAsync(context.OrderId, ct);
return false;
}
// Step 3: Create Shipping Label
bool shippingCreated = await _shipping.CreateConsignmentAsync(context.OrderId, ct);
if (!shippingCreated)
{
_logger.LogError("Step 3 failed. Executing compensations in reverse order: Refund -> Release Stock.");
await _payment.CompensateRefundAsync(context.OrderId, ct);
await _inventory.CompensateReleaseStockAsync(context.OrderId, ct);
return false;
}
_logger.LogInformation("Order Checkout Saga completed successfully: {OrderId}", context.OrderId);
return true;
}
}What is Event Sourcing and CQRS? When should you adopt them, and when are they an architectural antipattern?
Modern microservices often suffer from conflating read access patterns with transactional write models. Event Sourcing and CQRS decouple these responsibilities:
- Traditional State Mutation vs Event Sourcing:
- Traditional:
UPDATE BankAccounts SET Balance = 1500 WHERE Id = 42. Historical context is permanently erased. You only know the current state, not how or why it happened. - Event Sourcing: Append events to an EventStore:
AccountOpened($1000)->MoneyDeposited($700)->ATMWithdrawn($200). Current balance is derived by replaying these events from genesis or from a recent snapshot.
- Traditional:
- CQRS Architecture:
Commands (mutations) mutate state by validating domain invariants and appending events to the Write Model. Projections (event handlers) asynchronously consume these events and project them into specialized Read Stores (Elasticsearch for full-text search, Redis for key-value lookups, Postgres read views for reporting).
- When Event Sourcing + CQRS is an Antipattern:
- Simple CRUD applications with low business logic complexity.
- Applications requiring immediate read-after-write consistency across every user interaction (projections have eventual consistency lag).
- Teams unprepared for event schema versioning, upcasting, and complex projection rebuild pipelines.
public abstract class AggregateRoot
{
private readonly List<object> _uncommittedEvents = new();
public Guid Id { get; protected set; }
public int Version { get; protected set; }
public IReadOnlyList<object> GetUncommittedEvents() => _uncommittedEvents.AsReadOnly();
public void ClearUncommittedEvents() => _uncommittedEvents.Clear();
protected void RaiseEvent(object @event)
{
ApplyChange(@event);
_uncommittedEvents.Add(@event);
}
public void LoadFromHistory(IEnumerable<object> history)
{
foreach (var @event in history)
{
ApplyChange(@event);
Version++;
}
}
protected abstract void ApplyChange(object @event);
}
public class BankAccountAggregate : AggregateRoot
{
public decimal Balance { get; private set; }
public void Deposit(decimal amount, string transactionRef)
{
if (amount <= 0) throw new ArgumentException("Deposit must be positive");
RaiseEvent(new MoneyDepositedEvent(Id, amount, transactionRef, DateTime.UtcNow));
}
protected override void ApplyChange(object @event)
{
switch (@event)
{
case MoneyDepositedEvent e:
Balance += e.Amount;
break;
case MoneyWithdrawnEvent w:
Balance -= w.Amount;
break;
}
}
}How does Distributed Locking work? Compare Redis Redlock vs ZooKeeper/etcd Leases. What is a Fencing Token?
Distributed locking is deceptively difficult because time and process scheduling cannot be assumed to be synchronized in distributed systems:
- The Danger of Naive Distributed Locks (Redis Single-Master):
A client executes
SET lock_key client_uuid NX PX 10000. If the client experiences a 15-second Stop-The-World (STW) Garbage Collection pause, its lock TTL expires in Redis while the thread is asleep. Another client acquires the lock. When the first thread wakes up, it proceeds to write to shared storage, causing catastrophic silent data corruption! - Martin Kleppmann’s Critique & The Fencing Token:
You cannot rely on lock expiration timers for mutual exclusion. Every lock grant must issue a Fencing Token (monotonically increasing integer: 31, 32, 33…).
The target storage service (e.g. database or file store) inspects the token: if incoming write token < highest token previously processed, the write is rejected!
- ZooKeeper / etcd Consensus Leases vs Redlock:
- etcd / ZooKeeper: Backed by Raft / ZAB consensus algorithms. Strong consistency (CP). When the lock holder dies, its ephemeral node or lease heartbeats stop, cleanly releasing the lock without split-brain anomalies.
- Redis Redlock: Relies on wall-clock time synchrony across N masters. Vulnerable to clock skew, NTP jumps, and asymmetric network partitions. Recommended for advisory locks where occasional duplicates cause low harm, but NOT for financial balance reconciliation.
public class SharedFileStorageEngine
{
private long _highestSeenFencingToken = 0;
private readonly object _stateLock = new();
public async Task WriteDataWithFencingAsync(long fencingToken, string data, CancellationToken ct)
{
lock (_stateLock)
{
// Reject stale writes from zombie/delayed clients
if (fencingToken <= _highestSeenFencingToken)
{
throw new ConcurrencyLockException(
$"Stale write rejected! Incoming Token: {fencingToken}, Storage Token: {_highestSeenFencingToken}. " +
"Client lock lease likely expired during network or GC pause.");
}
// Accept and advance fencing watermark
_highestSeenFencingToken = fencingToken;
}
// Safely execute file / database disk write
await File.WriteAllTextAsync("critical_state.dat", data, ct);
}
}Explain Distributed Consensus: Compare Raft and Paxos. How does Raft handle Leader Election, Log Replication, and Split-Brain?
Modern distributed systems (etcd, Kubernetes, Kafka KRaft, CockroachDB, Consul) rely on Raft for distributed coordination:
- Raft Node Roles & States:
Every node is in one of three states: Follower, Candidate, or Leader.
- Leader Election with Randomized Timeouts:
- Followers expect periodic heartbeats from the Leader.
- If election timeout (randomized between 150ms–300ms) expires without a heartbeat, the follower transitions to Candidate, increments its Term counter, votes for itself, and sends
RequestVoteRPCs to all peers. - Randomizing timeouts ensures one candidate starts its election before others, preventing split-vote ties.
- The candidate becomes Leader if it receives votes from a majority quorum
(N / 2 + 1).
- Log Replication & Commit Quorum:
The Leader receives client writes, appends them to its local log, and broadcasts
AppendEntriesRPCs. Once the entry is persisted on a majority of nodes, the Leader commits it and applies it to its state machine. - Split-Brain Resolution:
Consider a 5-node cluster partitioned into two subnets:
{A, B}and{C, D, E}.- The minority partition
{A, B}cannot form a quorum (2 < 3). Any writes sent to A or B cannot commit and are rejected. - The majority partition
{C, D, E}easily forms a quorum (3 >= 3) and elects a new Leader with a higher Term number. - When the partition heals, nodes A and B see the higher Term number, step down, and overwrite their uncommitted logs with the majority log.
- The minority partition
Cluster Topology (5 Nodes) -> Majority Quorum Required = 3 Nodes
Network Partition Occurs:
Partition 1 (Minority: 2 Nodes) Partition 2 (Majority: 3 Nodes)
[Node A] [Node B] [Node C] [Node D] [Node E]
Can Form Quorum? NO (2 < 3) Can Form Quorum? YES (3 >= 3)
Writes Accepted: 0 (Rejected/Blocked) Elects Leader with Term 2; Commits Writes!
When Network Heals:
Nodes A & B receive AppendEntries with Term 2 -> Overwrite uncommitted tail -> Cluster in 100% sync.How do you conduct Back-of-the-Envelope Capacity Estimations in System Design interviews? Provide the standard formulas.
Back-of-the-envelope calculations demonstrate your ability to convert business requirements into physical distributed infrastructure:
Interactive Resource: For complex scenarios, practice using the live System Design Back-of-the-Envelope Estimator to verify your math.
| Metric | Standard Estimation Formula | Worked Example (500M DAU, 10 reads/day, 1 write/day) |
|---|---|---|
| Read QPS | Total Reads / 100,000 seconds | (500M × 10) / 100,000 = 50,000 Read QPS |
| Write QPS | Total Writes / 100,000 seconds | (500M × 1) / 100,000 = 5,000 Write QPS |
| Peak QPS | Average QPS × 2 (or 3x) | 55,000 total QPS × 2 = 110,000 Peak QPS |
| Daily Storage | Daily Writes × Payload Size | 500M writes × 2 KB = 1,000 GB = 1 TB / day |
| 5-Year Storage | Daily Storage × 365 × 5 × Replication(3) | 1 TB × 1825 days × 3 = 5.47 Petabytes |
| RAM Cache (80/20) | Daily Read Volume × 20% | (5B reads × 2 KB) × 0.20 = 10 TB × 0.20 = 2 TB RAM (16 × 128GB Redis nodes) |
Powers of Two & Latency Numbers Every Architect Must Memorize:
1 Day = 86,400 seconds ≈ 100,000 seconds(simplifies mental math by 15%).1 Million requests/day ≈ 12 QPS;100 Million requests/day ≈ 1,200 QPS.- L1 cache ref: 0.5–1 ns | L2 cache ref: 4 ns | RAM access: 100 ns
- NVMe SSD read: 10–50 µs | Rotational disk seek: 2–10 ms
- Datacenter round-trip: 0.5 ms | Cross-country (US-East to US-West): 60 ms | Transatlantic: 100 ms
public record CapacityMetrics(
long DailyActiveUsers,
int ReadsPerUserPerDay,
int WritesPerUserPerDay,
int AveragePayloadBytes);
public static class CapacityCalculator
{
public static void CalculateSystemDimensions(CapacityMetrics input)
{
const int SecondsPerDay = 100_000; // Standard interview approximation
long totalReadsPerDay = input.DailyActiveUsers * input.ReadsPerUserPerDay;
long totalWritesPerDay = input.DailyActiveUsers * input.WritesPerUserPerDay;
double readQps = (double)totalReadsPerDay / SecondsPerDay;
double writeQps = (double)totalWritesPerDay / SecondsPerDay;
double peakQps = (readQps + writeQps) * 2.0;
double dailyStorageGb = (totalWritesPerDay * input.AveragePayloadBytes) / (1024.0 * 1024 * 1024);
double fiveYearStoragePb = (dailyStorageGb * 365 * 5 * 3) / (1024.0 * 1024); // 3x replication
double cacheRamTb = (totalReadsPerDay * input.AveragePayloadBytes * 0.20) / (1024.0 * 1024 * 1024 * 1024);
Console.WriteLine($"Average Read QPS: {readQps:N0} QPS");
Console.WriteLine($"Average Write QPS: {writeQps:N0} QPS");
Console.WriteLine($"Peak Combined QPS: {peakQps:N0} QPS");
Console.WriteLine($"Daily Raw Storage: {dailyStorageGb:N2} GB/day");
Console.WriteLine($"5-Year Storage (3x Repl): {fiveYearStoragePb:N2} PB");
Console.WriteLine($"Redis Cache RAM (80/20): {cacheRamTb:N2} TB");
}
}What is an API Gateway versus a Service Mesh? How do Envoy, Istio, and mTLS handle East-West versus North-South traffic?
Modern containerized microservices split network architecture into two distinct planes:
- North-South Traffic (Client to API Gateway):
- Role: Acts as the public front door. Protects internal microservices from direct internet exposure.
- Responsibilities: Public rate limiting, IP whitelisting/WAF, API version routing, request/response payload transformation, API monetization, and SSL termination.
- Examples: Kong, AWS API Gateway, Ocelot, Azure API Management, NGINX.
- East-West Traffic (Microservice to Microservice):
- Role: Manages communication within the private Kubernetes cluster.
- Responsibilities:
- Mutual TLS (mTLS): Zero-Trust security where every pod proves its cryptographic identity via X.509 certificates rotated automatically every few hours.
- Distributed Tracing: Injects W3C TraceContext headers (
traceparent) to trace a transaction across 20 downstream microservices. - Traffic Shifting & Canary Deployments: Routes 5% of internal RPCs to a canary v2 deployment and 95% to v1.
- Examples: Istio, Linkerd, Consul Connect with Envoy sidecar proxies.
- The Sidecar Architecture Pattern:
In a service mesh, the Envoy proxy is injected into the same Kubernetes Pod alongside your application container. All inbound and outbound localhost network traffic is transparently intercepted via Linux
iptablesrules. Developers write plain HTTP/gRPC calls; the sidecar handles TLS encryption, retries, and metrics transparently.
public class MtlsValidationService
{
private readonly HashSet<string> _authorizedSpiffeIds;
public MtlsValidationService(IConfiguration config)
{
// SPIFFE ID identifies internal microservice identity (e.g. spiffe://cluster.local/ns/prod/sa/order-service)
_authorizedSpiffeIds = config.GetSection("AuthorizedSpiffeIdentities")
.Get<HashSet<string>>() ?? new();
}
public bool ValidateClientCertificate(X509Certificate2 clientCertificate)
{
// 1. Verify certificate chain against internal mesh CA
if (!clientCertificate.Verify()) return false;
// 2. Extract SPIFFE identity from SAN (Subject Alternative Name) extension
var sanExtension = clientCertificate.Extensions["2.5.29.17"] as X509SubjectAlternativeNameExtension;
if (sanExtension == null) return false;
foreach (var uri in sanExtension.EnumerateUris())
{
if (_authorizedSpiffeIds.Contains(uri))
return true; // Authorized internal peer service
}
return false;
}
}Explain Data Tiering: Hot, Warm, and Cold Storage. How do you implement Partition Switching and TTL without locking production tables?
Relational databases slow down as tables grow into hundreds of millions of rows because indexes exceed available RAM. Data tiering keeps OLTP tables lean:
- The Storage Hierarchy:
- Hot Tier: High-performance NVMe SSDs, in-memory Redis. Sub-millisecond latency. 100% of OLTP transactions. High cost per gigabyte ($0.10–$0.30/GB/month).
- Warm Tier: Standard cloud managed disks, read replicas, or columnstore partitions. Used for monthly reporting and business dashboards. Moderate cost ($0.04/GB/month).
- Cold / Archive Tier: Cloud Object Storage (S3 Glacier Deep Archive, compressed Parquet/ORC files). Retrieval time: minutes to hours. Lowest cost ($0.00099/GB/month — a 99% cost reduction).
- Why `DELETE FROM Table WHERE Date < X` Destroys Production:
A mass delete query acquires an exclusive lock (causing lock escalation to table level), generates gigabytes of Write-Ahead Log (WAL) records, causes replication lag across replicas, and leaves index fragmentation.
- Zero-Lock Partition Switching Mechanism:
In SQL Server or PostgreSQL table partitioning, tables are partitioned by date range (e.g. monthly). When month
2024-01becomes cold:- Create an empty staging table with identical schema.
- Execute
ALTER TABLE Orders SWITCH PARTITION 1 TO Orders_Archive_2024_01. - This is a metadata-only pointer swap that completes in 2 milliseconds, regardless of whether the partition contains 50,000,000 rows!
- Export the standalone archive table to parquet files in S3 and drop the table with zero impact on live traffic.
-- Step 1: Create empty staging table on the exact same filegroup
CREATE TABLE dbo.Orders_Staging_2024_01 (
OrderId BIGINT NOT NULL,
CustomerId INT NOT NULL,
OrderDate DATETIME2 NOT NULL,
TotalAmount DECIMAL(18,2) NOT NULL,
CONSTRAINT PK_Orders_Staging PRIMARY KEY (OrderId, OrderDate)
) ON [PRIMARY];
-- Step 2: Instant O(1) metadata pointer swap (Takes < 5 milliseconds!)
ALTER TABLE dbo.Orders
SWITCH PARTITION 1 TO dbo.Orders_Staging_2024_01;
-- Step 3: Now export dbo.Orders_Staging_2024_01 to AWS S3 / Azure Parquet in the background
-- Step 4: Drop the staging table safely with zero lock contention on dbo.Orders
DROP TABLE dbo.Orders_Staging_2024_01;How do you solve the Celebrity / Hotspot Problem (Fan-out on Read vs Fan-out on Write) in High-Scale Social Media Feeds?
The timeline feed problem (Twitter, Instagram, LinkedIn) highlights the tension between read latency and write amplification:
- Model 1: Fan-out on Write (Push / Eager Model):
- Flow: User writes a tweet -> background workers query follower list -> append tweet ID to every follower’s Redis timeline list.
- Pros: Reading timeline is instant:
LRANGE timeline:user_123 0 20takes < 2ms. - The Celebrity Disaster: When a celebrity with 100M followers tweets, the system must execute 100,000,000 Redis writes! Message queues backlog for minutes, causing severe fan-out lag.
- Model 2: Fan-out on Read (Pull / Lazy Model):
- Flow: When User writes, write to their own tweet table only (1 write). When a follower opens their app, query all users they follow, fetch recent tweets, and merge-sort in memory.
- Cons: High read latency. If a user follows 2,000 people, the query must fetch and sort 2,000 lists on the fly.
- Model 3: The Hybrid Feed Architecture (Industry Standard):
- Categorize users by follower count threshold (e.g. 25,000 followers).
- Non-Celebrity Posts (<25k): Use Fan-out on Write. Pushed into followers’ Redis home timelines.
- Celebrity Posts (>25k): No fan-out. Saved only to the celebrity’s post stream.
- When an active user opens their timeline:
- Fetch pre-computed timeline from Redis (instant).
- Check which celebrities the user follows.
- Fetch recent posts from those specific celebrities.
- Merge and sort the two lists in memory before returning to client (takes < 20ms).
public class HybridTimelineService
{
private readonly IDistributedCache _cache;
private readonly ITweetRepository _tweetRepo;
private readonly IFollowerRepository _followerRepo;
private const int CelebrityThreshold = 25_000;
public HybridTimelineService(IDistributedCache cache, ITweetRepository tweetRepo, IFollowerRepository followerRepo)
{
_cache = cache;
_tweetRepo = tweetRepo;
_followerRepo = followerRepo;
}
public async Task PublishTweetAsync(long authorId, string text, CancellationToken ct)
{
var tweet = await _tweetRepo.CreateTweetAsync(authorId, text, ct);
int followerCount = await _followerRepo.GetFollowerCountAsync(authorId, ct);
if (followerCount < CelebrityThreshold)
{
// Fan-out on Write: Enqueue async fanout task to push to all followers' Redis timeline
await EnqueueFanoutOnWriteAsync(tweet.Id, authorId);
}
else
{
// Celebrity Tweet: Skip fan-out! Save only to celebrity's personal post list
await _cache.SetStringAsync($"celebrity:{authorId}:latest", tweet.Id.ToString());
}
}
public async Task<List<TweetDto>> GetUserHomeTimelineAsync(long userId, CancellationToken ct)
{
// 1. Read pre-computed push timeline from Redis cache (O(1) lookup)
var normalTimelineIds = await GetCachedTimelineIdsAsync(userId);
// 2. Query which celebrities this user follows
var followedCelebrities = await _followerRepo.GetFollowedCelebritiesAsync(userId, ct);
// 3. Pull latest tweets from followed celebrities
var celebrityTweetIds = await GetCelebrityTweetIdsAsync(followedCelebrities);
// 4. Merge-sort both lists by timestamp descending and take top 20
var combinedIds = normalTimelineIds.Union(celebrityTweetIds).Take(20).ToList();
return await _tweetRepo.HydrateTweetsAsync(combinedIds, ct);
}
}Disaster Recovery in Distributed Systems: Differentiate RPO vs RTO and explain Multi-Region Active-Active vs Active-Passive.
Designing for catastrophic regional cloud outages requires precise service level commitments:
- RPO vs RTO Metrics:
- RPO (Data Loss Target):
Disaster Time - Last Valid Backup/Replicated Log. If your database replicates asynchronously every 60 seconds and a datacenter loses power, RPO is 60 seconds (60 seconds of transactions are permanently lost). - RTO (Downtime Target):
Service Restored Time - Disaster Time. The duration needed to detect failure, update DNS records, promote replica to master, and warm up caches.
- RPO (Data Loss Target):
- Active-Passive (Warm Standby / Pilot Light):
- Topology: Primary datacenter (Region A) serves 100% of traffic. Secondary (Region B) receives database log replication.
- Pros: Zero write conflict; simple architecture; lower software complexity.
- Cons: High failover RTO (DNS TTL propagation takes minutes; cold caches cause initial performance degradation); paid standby hardware sits mostly idle.
- Multi-Region Active-Active:
- Topology: Users in Europe write to
eu-central; users in America write tous-east. Global Anycast / Geo-DNS directs clients to the closest operational region. - Challenges: Speed-of-light network latency (~100ms between EU and US) makes synchronous cross-region commits too slow. Requires multi-master replication with Conflict-Free Replicated Data Types (CRDTs), Last-Write-Wins (LWW), or strict geographic sharding by User Home Region.
- RTO: Effectively 0 seconds. If Region A goes dark, global health checks re-route traffic to Region B instantaneously.
- Topology: Users in Europe write to
Architecture Pattern RPO Target RTO Target Cost Overhead Engineering Complexity
--------------------------------------------------------------------------------------------
Backup & Restore (Cold) 24 Hours 12–24 Hours Low Minimal
Warm Standby (Passive) 1–5 Minutes 15–30 Minutes Moderate Medium
Hot Standby (Failover) < 1 Minute < 2 Minutes High High
Active-Active (Multi-Reg) Near 0 Near 0 Very High Extreme (CRDTs/Multi-Master)How do you engineer Resiliency in Microservices? Detail Circuit Breakers, Bulkheads, and Retry with Exponential Backoff + Jitter.
In a microservice mesh with 50 interconnected services, if each service boasts 99.9% availability, total system availability is 0.999^50 ≈ 95.1% (over 42 hours of downtime per year!). Resiliency patterns prevent cascading collapse:
- The Circuit Breaker Pattern:
Wraps fragile external network calls and tracks failure rates across three states:
- Closed: Normal operation. Calls pass through. If failure percentage exceeds threshold (e.g. 50% over 10 seconds), trips to Open.
- Open: Fast-fails immediately with a cached or default fallback without making a network call, giving the downstream service time to recover.
- Half-Open: After a cooldown duration (e.g. 30s), lets a small percentage of trial requests through. If successful, resets to Closed; if failing, returns to Open.
- The Bulkhead Pattern (Ship Hull Isolation):
Isolates memory, thread pools, and database connection pools by dependency. If the
RecommendationServicehangs on a slow database query, its dedicated thread pool fills up, but thePaymentServiceandOrderServicethread pools remain completely uninhibited. - Retry with Exponential Backoff + Full Jitter:
Retrying immediately upon failure creates a Retry Storm that keeps the struggling dependency permanently crashed. Adding Full Jitter decorrelates retries across thousands of concurrent clients:
SleepDuration = Random(0, Min(MaxBackoff, BaseDelay * 2^AttemptNumber))
public static class HttpClientResilienceExtensions
{
public static IServiceCollection AddResilientDownstreamClient(this IServiceCollection services)
{
services.AddHttpClient("PaymentGateway", client =>
{
client.BaseAddress = new Uri("https://api.paymentgateway.internal/");
client.Timeout = TimeSpan.FromSeconds(3); // Strict socket timeout
})
.AddResilienceHandler("custom-pipeline", builder =>
{
// 1. Bulkhead: Restrict concurrent calls to 20 so slow requests don't exhaust threadpool
builder.AddConcurrencyLimiter(new ConcurrencyLimiterOptions
{
PermitLimit = 20,
QueueLimit = 10
});
// 2. Retry with Exponential Backoff and Full Jitter
builder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true, // Mandatory to prevent thundering herd retry storms
Delay = TimeSpan.FromMilliseconds(200)
});
// 3. Circuit Breaker: Trip open if 50% of requests fail over 10 seconds
builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
SamplingDuration = TimeSpan.FromSeconds(10),
FailureRatio = 0.5,
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(30)
});
});
return services;
}
}Design a Scalable URL Shortener (TinyURL / Bitly) handling 100M new URLs per month with sub-10ms redirect latency.
Designing TinyURL is the quintessential system design blueprint. Interviewers use it to evaluate your understanding of hashing, collision handling, encoding, caching, and redirect status codes:
- 1. Capacity & Sizing Math:
- Writes: 100M new URLs/month ≈ 100M / (30 × 100,000) ≈ 35 writes/sec. Peak writes ≈ 100 writes/sec.
- Reads (100:1 Read-to-Write Ratio): 3,500 reads/sec. Peak reads ≈ 10,000 QPS.
- Storage (5-Year Horizon): 100M/month × 12 × 5 = 6 Billion records. At 500 bytes per record (Original URL + Short URL + User ID + Timestamp), total storage ≈ 3 Terabytes (easily fits in a single sharded database cluster).
- 2. Encoding & Token Generation Strategy:
- Why MD5/SHA256 Hashing Fails: Hashing the long URL produces a 128-bit hash. Taking the first 7 characters causes hash collisions that require expensive database lookups and salt re-hashing loops.
- The Key Generation Service (KGS) / Token Range Blueprint: A lightweight coordinator (backed by ZooKeeper or Redis) allocates distinct ID ranges to application server instances (e.g. Server 1 gets
1,000,000–1,999,999; Server 2 gets2,000,000–2,999,999). Each server increments locally in memory and encodes the 64-bit integer into Base62. Zero collisions, zero lock contention!
- 3. HTTP 301 vs HTTP 302 Redirect Trade-off:
- HTTP 301 (Permanent Redirect): Browsers cache the redirect locally. Subsequent clicks bypass your servers entirely. Pros: Lowest server load. Cons: You cannot track click analytics or geo-metrics!
- HTTP 302 (Found / Temporary Redirect): Every click hits your server/CDN first before redirecting. Pros: 100% accurate click analytics, click fraud detection, and monetization tracking. Industry choice for commercial URL shorteners.
[Client Browser / Mobile App]
│
▼ (Anycast DNS / CDN Edge)
[API Gateway / Load Balancer]
│
┌───────┴────────────────────────┐
▼ (Write: /api/shorten) ▼ (Read: /{shortCode})
[URL Creation Service] [Redirect Resolution Service]
│ │
├─> [KGS Token Range (Zookeeper)]│ (Check Redis Cache First)
│ ├─> [Redis Cache Cluster (LRU)]
│ │ │ (On Cache Miss)
▼ ▼ ▼
[NoSQL Database Shards (DynamoDB / MongoDB: Key: short_hash, Value: long_url)]
│
▼ (Async Click Telemetry via Kafka)
[Click Analytics Pipeline (Kafka -> Flink -> ClickHouse DB)]Design a Distributed Rate Limiter capable of enforcing tier limits across 50,000 requests/sec with minimal latency overhead.
Rate limiting protects APIs from denial-of-service attacks, brute force, and downstream resource exhaustion:
- Algorithm Comparison:
- Token Bucket: Tokens refill at constant rate. Allows bursts up to bucket capacity. Widely used (AWS, Stripe).
- Leaky Bucket: Requests process at a strictly constant rate via a FIFO queue. Smooths traffic bursts, but delays processing of urgent requests.
- Fixed Window Counter: Counts requests per fixed minute. Flaw: Boundary Burst (double traffic can arrive across the window seam: 100 requests at 11:59:59 and 100 requests at 12:00:01).
- Sliding Window Counter: Approximates previous window overlap:
Count = CurrentWindowCount + PreviousWindowCount * ((WindowSize - Offset) / WindowSize). Zero boundary bursts, negligible memory overhead.
- Distributed Synchronization & Race Conditions:
In a cluster of 50 API nodes, checking a counter and incrementing it (Read-Modify-Write) in Redis without atomic locks creates classic race conditions.
Solution: Execute the entire rate limit calculation inside a single Redis Lua Script. Redis guarantees Lua script execution is single-threaded and atomic, preventing race conditions without distributed locks.
- Handling Redis Outages (Fail-Open vs Fail-Closed):
If Redis cluster crashes, what should the rate limiter do? For public web APIs, Fail-Open (allow requests through while emitting high-priority alerts to PagerDuty) is standard to prevent rate limiter failure from taking down the entire business.
public class RedisSlidingWindowRateLimiter
{
private readonly IConnectionMultiplexer _redis;
private const string RateLimitLuaScript = @"
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local clearBefore = now - window
-- Remove timestamps older than the sliding window
redis.call('ZREMRANGEBYSCORE', key, '-inf', clearBefore)
-- Count requests in the current sliding window
local currentRequests = redis.call('ZCARD', key)
if currentRequests < limit then
-- Add current request timestamp to sorted set
redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, math.ceil(window / 1000))
return 1 -- Allowed
else
return 0 -- Rate Limited
end
";
public RedisSlidingWindowRateLimiter(IConnectionMultiplexer redis) => _redis = redis;
public async Task<bool> IsRequestAllowedAsync(string clientId, int maxRequests, TimeSpan window)
{
var db = _redis.GetDatabase();
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
long windowMs = (long)window.TotalMilliseconds;
try
{
var result = (int)await db.ScriptEvaluateAsync(
RateLimitLuaScript,
keys: new RedisKey[] { $"ratelimit:{clientId}" },
values: new RedisValue[] { nowMs, windowMs, maxRequests });
return result == 1;
}
catch (RedisException)
{
// Fail-Open strategy: Allow traffic to flow during Redis blips
return true;
}
}
}Design a Globally Unique 64-Bit ID Generator (Twitter Snowflake) without centralized database auto-increment bottlenecks.
Relational database auto-incrementing integers create a severe write bottleneck and single point of failure. UUIDv4 strings are 128 bits, non-sequential, and destroy B+ Tree index page locality. Snowflake solves both problems:
- 64-Bit Bitwise Layout Breakdown:
Bit Range Bits Purpose & Capacity Bit 63 1 bit Sign bit (always 0to keep IDs positive in signed 64-bit integers).Bits 62–22 41 bits Milliseconds elapsed since custom epoch. 2^41 / (1000 × 86400 × 365) ≈ 69.7 years.Bits 21–12 10 bits Worker Machine ID (or 5 bits Datacenter ID + 5 bits Worker ID). Supports 2^10 = 1,024nodes.Bits 11–0 12 bits Sequence counter. Increments for requests within the same millisecond. Supports 2^12 = 4,096IDs/ms. - Handling Clock Drift & NTP Backward Leaps:
If the server’s local clock synchronizes via NTP and jumps backward by 10ms, generating IDs based on the earlier timestamp could produce duplicate IDs!
Solution: The generator tracks
_lastTimestamp. IfcurrentTimestamp < _lastTimestamp, the generator refuses to issue IDs: it either spins and waits until the clock catches up (for small drifts < 5ms) or throws aClockBackwardsExceptionand alerts the ops team.
public class SnowflakeIdGenerator
{
private const long CustomEpoch = 1704067200000L; // 2024-01-01 00:00:00 UTC
private const int WorkerIdBits = 10;
private const int SequenceBits = 12;
private const long MaxWorkerId = -1L ^ (-1L << WorkerIdBits); // 1023
private const long MaxSequence = -1L ^ (-1L << SequenceBits); // 4095
private const int WorkerIdShift = SequenceBits;
private const int TimestampLeftShift = SequenceBits + WorkerIdBits;
private readonly long _workerId;
private long _sequence = 0L;
private long _lastTimestamp = -1L;
private readonly object _lock = new();
public SnowflakeIdGenerator(long workerId)
{
if (workerId < 0 || workerId > MaxWorkerId)
throw new ArgumentException($"Worker ID must be between 0 and {MaxWorkerId}");
_workerId = workerId;
}
public long NextId()
{
lock (_lock)
{
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
// Clock drift check
if (timestamp < _lastTimestamp)
{
long drift = _lastTimestamp - timestamp;
if (drift < 5) // Wait for minor NTP drift
{
Thread.Sleep((int)drift);
timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
else
{
throw new InvalidOperationException($"Clock moved backwards! Refusing to generate ID for {drift}ms");
}
}
if (_lastTimestamp == timestamp)
{
// Same millisecond: increment sequence counter
_sequence = (_sequence + 1) & MaxSequence;
if (_sequence == 0)
{
// Sequence overflow: spin until next millisecond
while (timestamp <= _lastTimestamp)
{
timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
}
}
else
{
_sequence = 0L;
}
_lastTimestamp = timestamp;
return ((timestamp - CustomEpoch) << TimestampLeftShift) |
(_workerId << WorkerIdShift) |
_sequence;
}
}
}Design a Real-Time Scalable Chat Application (WhatsApp / Slack / Discord) supporting 1-on-1 and Group chats with online presence.
Designing a messaging platform (WhatsApp, Slack, Discord) requires orchestrating real-time socket connections with massive append-only message persistence:
- 1. WebSocket Gateway & Connection Management:
Clients maintain long-lived WebSocket connections. Because 10M active users cannot connect to a single machine (a single Linux box handles ~50k–100k open TCP sockets), traffic is load-balanced across a cluster of 200 Gateway servers.
Session Registry: A Redis cluster maintains a key-value mapping:
user_123:active_gateway -> "gateway-node-42". - 2. Message Routing Flow (1-on-1 Chat):
- User A sends message to User B via its active WebSocket.
- Gateway A receives message, generates Snowflake ID, and publishes to Kafka topic
chat-messages. - Message Persister service writes message to Cassandra database asynchronously.
- Routing Service queries Redis Session Registry for User B:
- If User B is Online: Dispatches message to Gateway Node 42, which pushes it down User B’s active WebSocket.
- If User B is Offline: Enqueues a notification to the Push Notification Service (Apple APNs / Google FCM).
- 3. Group Chat Fan-Out Architecture:
- Small Groups (<100 members): Fan-out on write. Message is cloned to each member’s personal inbox channel.
- Large Channels (Slack / Discord with 50,000 members): Fan-out on read/subscribe. The channel is represented as a single Kafka topic or Redis Pub/Sub channel; active connected members subscribe to the single channel stream.
- 4. Cassandra / ScyllaDB Data Model:
CREATE TABLE messages ( conversation_id UUID, message_id BIGINT, // Snowflake ID (Time-ordered) sender_id UUID, content TEXT, created_at TIMESTAMP, PRIMARY KEY (conversation_id, message_id) ) WITH CLUSTERING ORDER BY (message_id DESC);Partitioning by
conversation_idkeeps all messages of a chat co-located on the same physical disk node; clustering bymessage_id DESCmakes fetching the latest 50 messages an instant sequential disk scan.
[Client A (Mobile/Web)]
│ (Active WebSocket)
▼
[WebSocket Gateway Node 1] ───(Publish Message)───> [Kafka Broker: chat-messages]
│ │
│ ┌─────────────────┴─────────────────┐
▼ ▼ ▼
[Redis Session Registry] [Message Persistence Worker] [Chat Message Router]
(Maps User -> Gateway Node) │ │
▼ ▼
[Cassandra Database Cluster] (Check User B State)
(Partition: conversation_id) │
┌───────────────┴───────────────┐
▼ (Online) ▼ (Offline)
[WebSocket Gateway Node 42] [Push Notification (APNs/FCM)]
│
▼ (Active WebSocket)
[Client B (Recipient)]Design a Distributed, Fault-Tolerant Key-Value Store (Amazon DynamoDB / Apache Cassandra style).
The Amazon Dynamo paper (2007) is the blueprint for modern masterless NoSQL databases (Cassandra, ScyllaDB, DynamoDB). Here is the complete architectural anatomy:
- 1. Data Partitioning & Replication (Ring Topology):
Nodes form a consistent hash ring. A key is hashed to a position on the ring. The primary replica is the first node clockwise; the key is replicated across the next
N - 1consecutive physical nodes along the ring (Replication FactorN = 3). - 2. Tunable Consistency via Quorums (N, W, R):
N:Number of replicas.W:Write Quorum (number of replicas that must acknowledge a write before success).R:Read Quorum (number of replicas that must respond to a read query).- Strong Consistency Condition:
R + W > N. The read set and write set must mathematically overlap by at least one node containing the latest write. - Fast Writes / Eventual Consistency:
W = 1, R = 1(Lowest latency, maximum availability, risk of stale reads).
- 3. Conflict Resolution with Vector Clocks:
In a masterless system where any node accepts writes, concurrent writes can occur. Vector Clocks track causal history:
[NodeA: 1, NodeB: 2]. If two versions have conflicting clocks that are neither ancestors nor descendants, the conflict is returned to the client application or resolved via Last-Write-Wins (LWW) using NTP timestamps. - 4. Fault Tolerance Mechanisms:
- Sloppy Quorum & Hinted Handoff: If Node A is temporarily down, Node B accepts the write on Node A’s behalf and stores a “hint”. Once Node A recovers, Node B hands off the data.
- Read Repair: When a client queries with
R = 2and the two replicas return different versions, the coordinator returns the latest version to the client and asynchronously writes the latest version to the out-of-date replica. - Anti-Entropy with Merkle Trees: In the background, nodes exchange Merkle Trees (binary hash trees) to identify and synchronize divergent data ranges without streaming gigabytes of raw records across the network.
[Hash Ring: 0 to 2^32 - 1]
(Node A: vnode1)
/ \
/ \
(Node C: vnode3) (Node B: vnode2)
\ /
\ /
(Node D: vnode4)
Write Key 'user_882' -> Hashes to Arc between A and B
Replication Factor N = 3 -> Replicated to: Node B, Node D, Node C
Write Quorum W = 2 -> Acknowledged by Node B & Node D -> Return HTTP 200 OK!
Anti-Entropy Engine:
Node B and Node D periodically compare Merkle Trees:
Root Hash Match? -> 100% in sync! Done in 1 round-trip.
Root Hash Diff? -> Traverse tree branches to pinpoint exact divergent key range.Design a Scalable Distributed Web Crawler (Googlebot / Bingbot) indexing 1 Billion web pages per month.
Web crawling requires balancing crawling speed with ethical politeness while avoiding infinite traps:
- 1. Scale & Capacity Math (1 Billion Pages / Month):
- Crawling Throughput: 1,000,000,000 / (30 × 86,400) ≈ 400 pages/second. Peak rate ≈ 1,200 pages/second.
- Storage: Average web page HTML = 100 KB. 1 Billion pages × 100 KB = 100 Terabytes / month. Stored in distributed object storage (AWS S3 / GCP Cloud Storage).
- 2. The URL Frontier Architecture (Politeness & Priority):
The URL Frontier prevents overloading external web servers while prioritizing high-value domains:
- Priority Queues (F1): Classifies URLs into priority tiers based on PageRank and update frequency (e.g. CNN homepage crawled hourly; personal blogs crawled monthly).
- Politeness Queues (F2): Each target hostname (e.g.
wikipedia.org) has its own dedicated FIFO queue. A Politeness Worker enforces a mandatory delay (e.g. 1,000ms) between consecutive requests to the same IP, respectingrobots.txtcrawl-delays.
- 3. Avoiding Infinite Spider Traps & Duplicates:
- URL Deduplication: Before adding a newly discovered link to the Frontier, query a Bloom Filter in memory. If seen, discard. (1 Billion URLs at 10 bits/URL requires only 1.2 GB of RAM!).
- Content Deduplication (SimHash): Many URLs point to identical or mirrored content. Compute a 64-bit SimHash fingerprint of the parsed text. If Hamming distance <= 3, discard as duplicate.
- Spider Traps (Infinite Dynamic URLs): Restrict URL path depth (e.g. max 10 subdirectories) and discard URLs with repeating query patterns (e.g.
/calendar?date=2026-09-28...).
[Seed URLs] ──> [URL Frontier (Priority & Politeness Queues)]
│
▼
[DNS Caching Subsystem]
│
▼
[Asynchronous Worker Fetchers]
│
(Fetch HTML via HTTP/2)
▼
[Robots.txt Parser]
│
▼
[HTML Document Parser]
│
┌──────────────────┴──────────────────┐
▼ (Extracted Content) ▼ (Extracted Outbound URLs)
[SimHash Content Deduplication] [Bloom Filter URL Deduplication]
│ │
▼ (If Unique) ▼ (If Not Crawled Yet)
[Object Storage: S3 Raw HTML] [Enqueue to URL Frontier]
│
▼
[Search Inverted Index Engine (Bigtable / Elasticsearch)]Design a Video Streaming Platform (YouTube / Netflix) handling video ingestion, transcoding, and adaptive delivery.
Video streaming involves two completely decoupled pipelines: a heavy asynchronous write pipeline (transcoding) and an ultra-scalable read pipeline (CDN delivery):
- 1. Ingestion & Pre-Signed Upload:
Clients never upload 20GB raw video files through API web servers. The API server generates an AWS S3 / Cloud Storage Pre-Signed Multipart URL. The client uploads chunks directly to Object Storage in parallel with resumable upload capabilities.
- 2. Distributed Transcoding DAG Pipeline:
- An S3
ObjectCreatedevent fires, publishing a message to a Kafka topic. - Video Chunker: Splits the raw MP4 into small 4-second video segments at GOP (Group of Pictures) keyframe boundaries.
- Transcoding Cluster (Kubernetes GPU Workers): Transcodes each 4-second chunk in parallel into multiple target profiles:
- Resolutions: 2160p (4K), 1080p, 720p, 480p, 360p.
- Codecs: H.264 (universal compatibility), VP9/AV1 (high compression efficiency).
- Manifest Generator: Compiles the Master Manifest file (
master.m3u8) linking resolution streams and chunk segment URLs.
- An S3
- 3. Adaptive Bitrate Streaming (ABR):
The video player client (e.g. HLS.js or native iOS/Android player) downloads the master manifest. Every 4 seconds, the player dynamically measures current network bandwidth:
- If user is on fast fiber Wi-Fi: player downloads 4K chunks.
- If user enters a tunnel and bandwidth drops to 1 Mbps: player automatically steps down to 480p chunks on the very next 4-second segment without buffering or stuttering!
- 4. Edge Caching & Byte-Range Optimization:
99% of video streaming bandwidth is absorbed by CDNs. Popular video chunks reside in edge PoP SSDs. CDNs support HTTP
Range: bytes=0-1048575headers, allowing users to scrub forward immediately without downloading the entire file.
[Client App] ──(1. Get Pre-Signed URL)──> [API Gateway]
│
└──(2. Direct Multipart Upload)───> [Raw Video Storage: S3 Bucket]
│
(3. S3 Event Notification)
▼
[Kafka: video-uploaded]
│
▼
[Distributed Transcoding Engine]
(DAG Pipeline / GPU Spot Pods)
│
┌──────────────────────┴──────────────────────┐
▼ ▼
[Split into 4s Chunks] [Parallel Transcoding Tasks]
│ (4K, 1080p, 720p, 480p)
▼ │
[Generate Master Manifest (.m3u8)] │
│ │
└──────────────────────┬──────────────────────┘
▼
[Transcoded Video Chunks (S3)]
│
▼
[Global CDN Edge PoPs]
│
(Adaptive Bitrate Stream: HLS/DASH)
▼
[End-User Video Player]Design a High-Concurrency Flash Sale / Ticket Reservation System (Hotstar / Ticketmaster / Amazon Prime Day).
Directly executing UPDATE Inventory SET Count = Count - 1 WHERE ProductId = 42 against a relational database under 100,000 concurrent requests creates catastrophic row-lock deadlocks, CPU saturation, and complete database freeze. The multi-tiered blueprint prevents this:
- Tier 1: Edge Gate & Virtual Waiting Room (Traffic Shaping):
CDN and API Gateways enforce aggressive rate limiting and bot detection (Cloudflare Turnstile). When traffic spikes 50x, non-reserved users are routed to a Virtual Waiting Room (FIFO queue), releasing users in controlled micro-batches (e.g. 500 users/second) into the checkout flow.
- Tier 2: In-Memory Atomic Reservation via Redis Lua:
Inventory counts are pre-loaded into Redis RAM. When a user clicks ‘Reserve’:
- An atomic Redis Lua script checks if available stock > 0.
- If yes: decrements available stock, creates an ephemeral reservation key with a 10-minute TTL:
reservation:user_123 -> {productId: 42, qty: 1}, and returns Success. - If no: returns ‘Sold Out’ in 1 millisecond without touching the database!
- Tier 3: Asynchronous Order Creation Pipeline:
Users with valid reservations are pushed to a Kafka topic
order-checkout. Downstream worker pools consume orders sequentially at the database’s maximum sustained write capacity (e.g., 2,000 writes/sec), eliminating database spikes. - Tier 4: Payment Expiry & Inventory Rollback:
Users have 10 minutes to complete payment. If payment fails or times out, a background worker consumes the expired reservation event, increments the Redis inventory counter, and makes the seat/item available to the next waiting customer.
public class FlashSaleInventoryService
{
private readonly IConnectionMultiplexer _redis;
private const string ReservationScript = @"
local inventoryKey = KEYS[1]
local reservationKey = KEYS[2]
local userId = ARGV[1]
local qty = tonumber(ARGV[2])
local ttlSeconds = tonumber(ARGV[3])
-- 1. Check if user already has an active reservation
if redis.call('EXISTS', reservationKey) == 1 then
return -1 -- Error: Already reserved
end
-- 2. Check stock balance
local currentStock = tonumber(redis.call('GET', inventoryKey) or '0')
if currentStock >= qty then
-- Atomically decrement stock
redis.call('DECRBY', inventoryKey, qty)
-- Record reservation with TTL (10 minutes to pay)
redis.call('SETEX', reservationKey, ttlSeconds, userId)
return 1 -- Success: Reserved!
else
return 0 -- Failed: Sold Out
end
";
public FlashSaleInventoryService(IConnectionMultiplexer redis) => _redis = redis;
public async Task<ReservationResult> TryReserveItemAsync(string productId, string userId, int qty = 1)
{
var db = _redis.GetDatabase();
var keys = new RedisKey[] { $"stock:{productId}", $"reservation:{productId}:{userId}" };
var args = new RedisValue[] { userId, qty, 600 }; // 600s = 10 minutes TTL
var result = (int)await db.ScriptEvaluateAsync(ReservationScript, keys, args);
return result switch
{
1 => ReservationResult.Success,
0 => ReservationResult.SoldOut,
-1 => ReservationResult.AlreadyReserved,
_ => ReservationResult.Error
};
}
}
public enum ReservationResult { Success, SoldOut, AlreadyReserved, Error }Design a Proximity / Location-Based Service (Yelp / Uber / Google Maps Nearby) with sub-second spatial queries.
Searching geographic spaces efficiently requires converting two-dimensional coordinates (Latitude, Longitude) into one-dimensional indexes suitable for B-Trees and Hash Tables:
- Spatial Indexing Options Compared:
- Geohash (Industry Standard for Yelp / Foursquare):
- Divides the world into recursive bounding boxes. Interleaves bits of lat and lon into a Base32 string (e.g.
9q8yyfor San Francisco). - Prefix Property: Locations sharing a common prefix are close to each other. A 6-character Geohash covers ~1.2 km × 0.6 km.
- Querying: To find places near coordinate X, calculate its Geohash and query the 8 surrounding neighbor bounding boxes (to avoid edge boundary misses). A simple SQL or Redis query:
WHERE geohash LIKE '9q8yy%'runs in milliseconds!
- Divides the world into recursive bounding boxes. Interleaves bits of lat and lon into a Base32 string (e.g.
- Quadtree:
A tree data structure where each internal node has exactly 4 children (NW, NE, SW, SE). Nodes recursively split when the number of points in a quadrant exceeds a threshold (e.g. 100 places). Fast in-memory spatial search, but difficult to balance and persist across distributed clusters.
- Google S2 (Used by Uber / Pokemon GO):
Projects the 3D spherical Earth onto the 6 faces of a cube, applying a Hilbert space-filling curve to map 2D coordinates to 64-bit integers. Preserves spatial locality better than Geohash and avoids distortion at the Earth’s poles.
- Geohash (Industry Standard for Yelp / Foursquare):
- Handling Static Places (Yelp) vs Dynamic Moving Objects (Uber):
- Yelp (Static POIs): High read-to-write ratio (1000:1). Restaurants rarely move. Pre-compute geohashes and cache nearby lists in Redis clusters with long TTLs.
- Uber (Dynamic Drivers): High write ratio. 1,000,000 drivers transmit GPS coordinates every 4 seconds (250,000 writes/sec!). Store driver locations in an in-memory spatial store like Redis Geospatial (GEOADD / GEORADIUS) which uses sorted sets backed by 52-bit Geohash integers.
public class ProximitySearchService
{
private readonly IPlaceRepository _placeRepo;
public ProximitySearchService(IPlaceRepository placeRepo) => _placeRepo = placeRepo;
public async Task<List<PlaceDto>> FindNearbyPlacesAsync(double lat, double lon, double radiusKm)
{
// 1. Calculate Geohash for user coordinates (Precision 6 = ~1.2km box)
int precision = GetPrecisionForRadius(radiusKm);
string centerHash = GeohashEncoder.Encode(lat, lon, precision);
// 2. Compute the 8 adjacent neighbor geohashes to avoid boundary edge misses
List<string> searchGeohashes = GeohashEncoder.GetNeighbors(centerHash);
searchGeohashes.Add(centerHash);
// 3. Query indexed storage: SELECT * FROM Places WHERE geohash IN (...)
var candidatePlaces = await _placeRepo.GetPlacesByGeohashPrefixesAsync(searchGeohashes);
// 4. Exact Distance Filter: Apply Haversine formula to eliminate corner false positives
return candidatePlaces
.Where(p => Haversine.DistanceKm(lat, lon, p.Latitude, p.Longitude) <= radiusKm)
.OrderBy(p => Haversine.DistanceKm(lat, lon, p.Latitude, p.Longitude))
.ToList();
}
private static int GetPrecisionForRadius(double radiusKm) => radiusKm switch
{
<= 1.0 => 6, // ~1.2 km x 0.6 km box
<= 5.0 => 5, // ~4.9 km x 4.9 km box
_ => 4 // ~39 km x 19 km box
};
}Design a Highly Scalable Distributed Notification Service (Apple Push, Firebase FCM, SMS Twilio, Email SendGrid).
Modern notification systems must guarantee delivery of critical transactional messages (OTP codes) within 2 seconds while handling marketing blasts of 100M messages without delay or duplicate spam:
- 1. Multi-Priority Queues:
Never mix transactional OTP codes with promotional marketing campaigns in the same queue! A bulk promotional blast of 50M emails will backlog the queue, causing users waiting for password reset OTPs to time out.
Solution: Separate Kafka topics by channel and priority:
sms.high-priority.otp,push.normal.social,email.low-priority.marketing. - 2. Deduplication & Idempotency:
Network timeouts between your service and Twilio/SendGrid can cause duplicate sends, leading to customer complaints and double SMS billing charges.
Solution: Clients provide an
Idempotency-Key(or the server hashesHash(UserId + EventType + EntityId)). The key is stored in Redis with a 24-hour TTL. Duplicate attempts within the window are rejected instantly. - 3. User Preference & Do-Not-Disturb (DND) Service:
Before dispatching, workers verify: (a) Has user opted out of marketing emails? (b) Is the recipient in a DND time window (e.g. 10:00 PM to 7:00 AM in their local timezone)? If DND is active and message is non-critical, delay message until morning.
- 4. Third-Party Gateway Failover & Circuit Breakers:
Third-party vendors (Twilio, SendGrid, APNs) experience frequent outages or rate limit throttles. The notification worker wraps each provider in a Circuit Breaker. If Twilio fails, the worker automatically fails over to a secondary SMS vendor (e.g. MessageBird / AWS SNS).
[Client Microservices]
│ (POST /api/v1/notifications with Idempotency-Key)
▼
[Notification API Gateway] ──(Check Idempotency)──> [Redis Deduplication Store]
│
▼
[User Preference & Rate Limit Validator] ──(Check Preferences)──> [User Profile DB]
│
├──────────────────────────┬──────────────────────────┐
▼ ▼ ▼
[Kafka: Critical OTP] [Kafka: Social Push] [Kafka: Marketing Email]
│ │ │
▼ ▼ ▼
[SMS Dispatch Workers] [Push Dispatch Workers] [Email Dispatch Workers]
(With Fallback Circuit) (Apple APNs / Google FCM) (SendGrid / AWS SES)
│ │ │
┌──────┴────────┐ │ │
▼ (Primary) ▼ (Fallback) │ │
[Twilio API] [MessageBird API] │ │
│ │
(On Repeated Error / 3x Failure) │
└───────────┬──────────────┘
▼
[Dead Letter Queue (DLQ)]
│
▼
[Alerting & Manual Replay]Top 6 Mistakes Candidates Make in System Design Interviews
- Jumping Directly into Architecture Diagrams Without Clarifying Requirements: Immediately sketching databases and load balancers without clarifying DAU, read/write ratios, latency SLAs, or data consistency constraints. Always spend the first 5 minutes agreeing on functional and non-functional requirements.
- Hand-Waving Database Choices (“I’ll just use NoSQL”): Proposing MongoDB, Cassandra, or PostgreSQL without defending why. You must justify your choice based on access patterns (e.g., heavy random reads vs sequential append-only writes, ACID transactions vs flexible schemaless scaling).
- Ignoring Single Points of Failure (SPOFs): Drawing a single load balancer, master database, or token coordinator. Every component in a distributed system fails eventually; always explain your redundancy, active-passive failover, or leader election strategy.
- Overlooking Celebrity Hotspots & Fan-Out Disasters: Designing social feeds or message queues assuming uniform user activity. If Elon Musk tweets to 180M followers, a naive push fan-out will collapse your message brokers and Redis clusters. Always implement hybrid push/pull models.
- Assuming Clocks and Time are Synchronized: Relying on wall-clock NTP timestamps across servers for distributed ordering or locking. Network latency and leap seconds cause clock drift; always use Lamport Timestamps, Vector Clocks, or Raft terms for causal ordering.
- Failing to Perform Back-of-the-Envelope Math: Designing a 100-node Kafka and Cassandra cluster for a service that receives only 5 requests per second, or designing a single MySQL server for a platform ingesting 100,000 writes/sec. Back-of-the-envelope math anchors your architecture in reality.
The 4-Step Technical Interview Framework for System Design
Structure your 45-minute whiteboard or virtual system design interview using this proven 4-step framework:
Distinguish Functional requirements (what the system does) from Non-Functional requirements (High Availability, Low Latency, Eventual vs Strong Consistency).
Calculate Average & Peak QPS, Daily Storage growth, 5-Year Storage with 3x replication, network bandwidth, and the 20% Redis cache RAM footprint.
Draw the end-to-end data flow: Client -> CDN -> API Gateway / Load Balancer -> Stateless App Services -> Distributed Cache -> Sharded Database.
Tackle hard trade-offs: Consistent Hashing, DB Sharding, replication lag, Thundering Herd caching fixes, Saga compensations, and disaster recovery.
24-Hour Final Revision Checklist
Quickly verify you have these high-frequency distributed systems concepts locked down before entering your interview:
- [ ] Can explain why Consistent Hashing with Virtual Nodes minimizes remapping to K/N during auto-scaling.
- [ ] Know the difference between Layer 4 (TCP/UDP IP routing) and Layer 7 (HTTP path/header content routing) load balancing.
- [ ] Differentiate CP vs AP systems in the CAP Theorem and explain why CA systems cannot exist across physical networks.
- [ ] Can explain why 2-Phase Commit (2PC) blocks and why the Saga Pattern (Orchestration vs Choreography) is preferred in microservices.
- [ ] Understand why B+ Trees optimize for point and range reads while LSM Trees maximize write throughput and compression.
- [ ] Know how Bloom Filters guarantee zero false negatives to avoid useless disk I/O in Cassandra/RocksDB.
- [ ] Understand Fencing Tokens and why lock TTLs alone cannot prevent data corruption in distributed locking.
- [ ] Can sketch the 64-bit layout of a Twitter Snowflake ID generator (1 sign, 41 timestamp, 10 worker, 12 sequence).
- [ ] Can explain the Hybrid Feed model (Fan-out on Write for regular users, Fan-out on Read for celebrities > 25k followers).
- [ ] Know the quorum equation (R + W > N) for tunable consistency and why odd cluster sizes (3, 5, 7) are standard in Raft/Paxos.
Related Developer Interview Tracks & Interactive Tools
System Design Calculator
Interactive back-of-the-envelope estimator for API QPS, network bandwidth, storage, and Redis cache sizing.
Web API / REST API Interview Guide
50 Master questions covering HTTP semantics, ASP.NET Core, JWT auth, idempotency keys, and microservices.
SQL Server Interview Questions
50 In-depth database questions covering B-Tree indexes, execution plans, RCSI, and live DBA tuning scenarios.
C# Interview Questions & Answers
42 Master questions covering .NET 8/9, CLR internals, Garbage Collection, async/await, and Span<T>.
DSA Coding Interview Questions
38 Master data structures and algorithm problems with code solutions, two-pointers, sliding window, and DP.
ASP.NET MVC & Core Interview Guide
100 Questions covering MVC architecture, middleware pipelines, EF Core, and enterprise patterns.
.NET to AI Engineer Roadmap
Complete 24-week curriculum transitioning backend .NET engineers to production AI and LLM agents.
Master Interview Hub
Browse all 8 engineering interview tracks with 415+ verified technical questions, filters, and cheat sheets.
Frequently Asked Questions (Candidate FAQ)
1. What is the single most common reason candidates fail System Design interviews?
Designing in a vacuum without clarifying requirements or constraints. Candidates who immediately jump to the whiteboard sketching databases and load balancers fail because they make unverified assumptions about traffic scale, consistency requirements, and latency SLAs. Top candidates spend the first 5 to 7 minutes clarifying functional and non-functional requirements and validating assumptions with back-of-the-envelope math.
2. How deep should I go into database internals during High-Level Design?
Go deep into the access pattern justification. You don’t need to recite C++ storage engine source code, but you must explain why your choice fits the workload: for example, choosing an LSM-tree store (like Cassandra or RocksDB) for high-frequency append-only write telemetry, or choosing a B+ tree relational database (PostgreSQL/SQL Server) for ACID transactions with clustered primary key range scans.
3. What is the difference between Low-Level Design (LLD) and High-Level Design (HLD)?
High-Level Design (HLD) focuses on macro distributed architecture: load balancers, caching layers, microservices, message queues, database sharding, and network protocols. Low-Level Design (LLD) focuses on micro object-oriented design within a single service: class diagrams, interfaces, design patterns (Strategy, Factory, Observer), thread safety, memory concurrency primitives, and clean code principles (SOLID).
4. Is Strong Consistency always better than Eventual Consistency?
No. Strong consistency (such as Two-Phase Commit or Spanner TrueTime) imposes high latency overhead and degrades availability during network partitions (per the CAP theorem). Eventual consistency allows systems to achieve massive throughput and 99.999% availability by accepting asynchronous replication lag, which is completely acceptable for social feeds, product reviews, and video view counts.
5. How do I practice back-of-the-envelope math quickly under pressure?
Use the standard mental approximations: treat 1 day as 100,000 seconds (instead of 86,400); remember that 1 Million requests/day ≈ 12 QPS and 100 Million requests/day ≈ 1,200 QPS; and apply the Pareto 80/20 rule to cache 20% of daily read volume in RAM. Verify your sizing models using RTSALL’s free System Design Calculator.
Leave a comment