Foundations: computer science & system design basics
Two parts. 1.1β1.11, computer science basics: what runs on a CPU, how work is shared and locked, what waiting costs, how memory, storage and the OS behave, the data structures behind real systems, and what happens when you type a URL. 1.12β1.19, system design basics: the building blocks, requirements and quality attributes, scaling and partitioning, data, communication patterns, reliability, coordination, and the security and observability baseline. Each is a primer that links to the deep-dive page.
Sharing is fast, but two people grabbing the same knife = race condition. That's why we need locks.
#SystemDesign #SoftwareEngineering #Backend
1.2 Concurrency vs Parallelism Added
Concurrency means juggling many tasks by switching between them. Parallelism means doing many tasks at the same instant on multiple cores.
Visual
"Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once." (Rob Pike)
You can have concurrency without parallelism (a single-core event loop) and parallelism without much concurrency (a GPU running the same math on many items).
I/O-bound work (waiting on network or DB) gains most from concurrency and async.
CPU-bound work (encryption, image processing) gains from parallelism across cores.
Amdahl's law: the part of the job that can't run in parallel limits the total speed-up.
.NET 10 How to implement it
Library / API
Package
Use it for
Task + async/await
built-inSystem.Threading.Tasks
Concurrency for I/O: many calls in flight, few threads. Task.WhenAll to fan out
Parallel.ForEachAsync
built-inSystem.Threading.Tasks
Process a list concurrently with a cap (MaxDegreeOfParallelism)
PLINQ / Parallel.For
built-inSystem.Linq (AsParallel)
True parallelism for CPU-bound work across all cores
Channels
built-inSystem.Threading.Channels
Producer/consumer queues between tasks, with back-pressure (bounded channels)
TPL Dataflow
System.Threading.Tasks.Dataflow
Multi-step pipelines where each block has its own parallelism and buffer
// Concurrency (I/O-bound): up to 8 calls in flight at once
await Parallel.ForEachAsync(accountIds,
new ParallelOptions { MaxDegreeOfParallelism = 8, CancellationToken = ct },
async (id, token) => await client.RefreshBalanceAsync(id, token));
// Parallelism (CPU-bound): spread the work across cores
var totals = transactions.AsParallel()
.GroupBy(t => t.AccountId)
.Select(g => (Account: g.Key, Total: g.Sum(t => t.Amount)))
.ToList();
Don't wrap I/O calls in Task.Run or PLINQ to "make them parallel". Async I/O is already concurrent; extra threads only add overhead.
1.3 Sync vs Async
Synchronous: the caller waits for the answer before moving on. Asynchronous: the caller moves on and gets the result later.
Two levels, same idea
In code
Blocking I/O: the thread sits idle while waiting for the DB.
Non-blocking / async-await: the thread is released while waiting, then picks up again when data arrives.
Result: one thread can serve many requests.
Between systems
Sync: HTTP request/response. The client waits for 200 OK.
Async: send a message to a queue or event bus, return immediately. A consumer processes it later.
Result: services are decoupled in time.
Visual
Async request-reply pattern (for long jobs)
POST /reports
β
202 AcceptedLocation: /reports/42/status
β
Poll GET /statusor receive webhook
β
200 + result URL
Synchronous
Asynchronous
Coupling
Tight. Both must be up at the same time
Loose. The consumer can be down for a while
Simplicity
Easy to code, debug and trace
Harder: correlation IDs, retries, ordering
Consistency
Immediate result
Eventual consistency
Load spikes
Caller feels every slowdown
The queue absorbs spikes (load levelling)
Best for
Queries, validation, "need answer now"
Long jobs, notifications, fan-out, integration
Async doesn't make one request faster. It improves throughput, resilience and decoupling.
Always carry a correlation ID so you can trace a message end to end.
Async consumers must be idempotent because messages can arrive twice.
Calling sync services in a chain multiplies latency and failure: AβBβC means each call adds risk.
Interview answer: "Use sync when the user needs the answer to continue. Use async when the work can finish later or fan out."
.NET 10 How to implement it
Level
Library / API
Use it for
In code
built-inasync / await, IAsyncEnumerable<T>
Release the thread while waiting for I/O; stream results as they arrive
Background work
built-inBackgroundService / IHostedService
Long-running workers inside the same app
Async HTTP API
built-inTypedResults.Accepted
Return 202 + a status URL, then do the work later
In-process queue
built-inSystem.Threading.Channels
Hand work from the request to the background worker (lost if the process dies)
Between systems
Azure.Messaging.ServiceBus
Durable queue/topic, so work survives restarts and other services can consume it
builder.Services.AddSingleton(Channel.CreateBounded<ReportJob>(100));
builder.Services.AddHostedService<ReportWorker>();
app.MapPost("/reports", async (ReportRequest req, Channel<ReportJob> queue) =>
{
var job = new ReportJob(Guid.NewGuid(), req);
await queue.Writer.WriteAsync(job);
return TypedResults.Accepted($"/reports/{job.Id}", new { job.Id }); // 202 + where to poll
});
public sealed class ReportWorker(Channel<ReportJob> queue) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
await foreach (var job in queue.Reader.ReadAllAsync(ct))
await BuildReportAsync(job, ct);
}
}
Never block on async code with .Result or .Wait(). It ties up thread-pool threads and can starve the whole server under load.
Sync vs Async explained with coffee β
Sync: you stand at the counter until your coffee is ready.
Async: you get a buzzer (202 Accepted), sit down, and it buzzes when ready (webhook).
Latency = how long one request takes. Throughput = how many requests finish per second. Bandwidth = the maximum the pipe can carry.
β± Latency
Time for one trip, in ms. Like how long one car takes on the highway.
π Throughput
Work done per unit of time (req/s, TPS, MB/s). Like cars arriving per hour.
π£ Bandwidth
The theoretical max capacity. Like the number of lanes.
Report percentiles, not averages: p50 (typical), p95, p99 (tail). One slow dependency hurts p99.
Little's Law: L = Ξ» Γ W. Requests in the system = arrival rate Γ time each spends. Example: 200 req/s Γ 0.5 s = 100 requests in flight, so you need about 100 concurrent workers.
Batching raises throughput but can add latency. That trade-off shows up everywhere.
.NET 10 How to implement it
Library / API
Package
Use it for
Stopwatch
built-inSystem.Diagnostics
Time one operation: Stopwatch.GetTimestamp() + Stopwatch.GetElapsedTime(start) (no allocation)
Histogram / Counter
built-inSystem.Diagnostics.Metrics
Record latency (histogram, for p95/p99) and throughput (counter)
http.server.request.duration
built-in ASP.NET Core metric
Request latency and request rate for every endpoint, for free
dotnet-counters
.NET global tool
Watch request rate, latency, GC and thread-pool queue live on a running process
Response compression
built-inAddResponseCompression
Send fewer bytes when bandwidth is the bottleneck
public sealed class PaymentMetrics(IMeterFactory meterFactory)
{
private readonly Histogram<double> _duration = meterFactory.Create("Bank.Payments")
.CreateHistogram<double>("payments.duration", unit: "s");
public async Task<T> TimeAsync<T>(Func<Task<T>> work)
{
var start = Stopwatch.GetTimestamp();
try { return await work(); }
finally { _duration.Record(Stopwatch.GetElapsedTime(start).TotalSeconds); }
}
}
1.5 Back-of-the-envelope numbers Added
Quick estimates of traffic, storage and latency let you size a design in an interview, or sanity-check one at work.
10M users Γ 5 calls/day = 50M/day Γ· 86,400 β 580 req/s avg, about 1,700 req/s peak. 1 KB per record Γ 50M/day β 50 GB/day β 18 TB/year.
Memory is thousands of times faster than a network hop (~100 ns vs ~0.5 ms), and a hop inside a data centre is hundreds of times faster than going across the world (~0.5 ms vs ~150 ms). That's why caching and data locality matter.
Round your numbers. Interviewers care about orders of magnitude, not decimals.
.NET 10 How to implement it
Library / tool
Package
Use it for
BenchmarkDotNet
BenchmarkDotNet
Measure the real cost of one operation (time and allocations) to replace a guess
NBomber
NBombercommercial licence
Load tests written in C#. Free for personal use only; an organisation needs a paid licence
k6 / Azure Load Testing
Not .NET (JavaScript / JMeter / Locust)
Free or managed alternatives to drive req/s at your API and check the estimate
dotnet-counters
.NET global tool
Watch CPU, GC, thread pool and request rate during the test, to see which resource runs out first
1.6 Race conditions, locks and deadlocks Added
A race condition is a bug where the result depends on which thread gets there first. Locks make a critical section run one at a time; deadlock is when locked threads wait for each other forever.
Visual: the lost update
How it works
Critical section: code that touches shared state and must not interleave. Atomic = it happens all at once or not at all.
Two classic races: read-modify-write (count++ is read, add, write) and check-then-act (if (!exists) add).
Atomic operations use one CPU instruction. Compare-and-swap (CAS): "set to X only if it is still Y", else re-read and retry. Lock-free, best for one variable.
Basics of thread safety were in 1.1. Across machines you need a distributed lock or a fencing token.
Primitive
Lets in
Use for
Mutex / lock
1 thread
Protect a small critical section
Semaphore
N at once
Cap concurrency (e.g. 10 DB calls)
Reader-writer lock
Many readers or 1 writer
Read-mostly data, like a cache
Atomic / CAS
No lock
Counters, flags, swapping a reference
Optimistic vs pessimistic concurrency
Optimistic: check at write
Read a row version or ETag, then UPDATE … WHERE version = @v.
0 rows changed = someone won: reload and retry, or return 409 / 412.
Best when conflicts are rare. No locks held.
Pessimistic: lock at read
SELECT … FOR UPDATE (SQL Server: UPDLOCK) inside a transaction.
Others wait until you commit.
Best when conflicts are frequent and retries are costly. Keep it short.
See transactions in 6.2 and ETag + If-Match on APIs in 8.2.
Deadlock: four conditions, break one
Coffman condition
Means
Break it with
Mutual exclusion
Only one holder
Immutable data, lock-free ops
Hold and wait
Hold one, wait for more
Take all locks at once, or smaller lock scope
No preemption
Locks can't be taken away
Timeouts, release and retry
Circular wait
A waits B waits A
Global lock order (e.g. by account ID)
Livelock
Threads keep backing off for each other and retrying, so nothing finishes. Fix: random jitter on retries.
Starvation
One thread never gets the lock, e.g. a writer behind endless readers. Fix: fair queues, short hold times.
Avoid locks
Immutable objects need no lock. Thread-safe collections do the locking for you.
When to use what
Situation
Choose
Avoid
One counter or flag
Atomic / CAS
A full lock
Small in-memory section
lock
Holding it during I/O
Async code, or cap of N
Semaphore
lock around await
DB row, rare conflicts
Optimistic version
Long DB locks
DB row, hot contention
Pessimistic, short tx
Retry storms
Several app instances
DB constraint or distributed lock
In-process locks
count++ is not atomic: it is read, add, write.
Keep locks small and short. Never do I/O or call unknown code while holding one.
Prevent deadlock with a fixed lock order plus timeouts. Databases detect deadlocks and kill a victim: retry it.
Optimistic for rare conflicts, pessimistic for hot rows.
An in-process lock does nothing across two servers. Use the database or a distributed lock.
.NET 10 How to implement it
Library / API
Package
Use it for
Lock + lock
built-inSystem.Threading (.NET 9+)
Mutual exclusion; TryEnter(timeout) to avoid waiting forever
Interlocked
built-inSystem.Threading
Increment, Add, CompareExchange (CAS)
SemaphoreSlim
built-inSystem.Threading
Async mutual exclusion (WaitAsync) or a cap of N
ReaderWriterLockSlim
built-inSystem.Threading
Many readers, one writer; sync code only (thread-affine)
Share read-only data with no locks; swap the whole reference to update
EF Core concurrency token
Microsoft.EntityFrameworkCore 10.0
[Timestamp] / IsRowVersion(); stale writes throw DbUpdateConcurrencyException
public sealed class Account
{
public Guid Id { get; set; }
public decimal Balance { get; set; }
[Timestamp] public byte[] RowVersion { get; set; } = []; // concurrency token
}
// Optimistic: EF sends UPDATE ... WHERE Id = @id AND RowVersion = @original
try
{
account.Balance -= amount;
await db.SaveChangesAsync(ct);
}
catch (DbUpdateConcurrencyException)
{
// Someone wrote first: reload, re-check the rule, then retry or return 409
}
// Lock-free counter and CAS
Interlocked.Increment(ref _requests);
Interlocked.CompareExchange(ref _config, newConfig, oldConfig); // swap only if unchanged
ConcurrentDictionary.GetOrAdd can run the value factory more than once under a race. Keep the factory cheap and side-effect free, or store a Lazy<T>.
Two deposits. One balance. One of them vanished. πΈ
π§΅ Thread A reads 100, Thread B reads 100.
β A writes 150, B writes 120. A's deposit is gone: a race condition.
π Fix in memory: a lock, an atomic op or compare-and-swap.
ποΈ Fix in the database: a row version (optimistic) or SELECT FOR UPDATE (pessimistic).
π Deadlock? Take locks in one fixed order, add timeouts, keep locks small.
Which do you reach for first: optimistic or pessimistic?
A concurrency model is the rule for how concurrent work shares state: shared memory with locks, one event loop, actors passing messages, channels between tasks, or data split across cores.
Event loop: async I/O lets one thread juggle thousands of waits (1.3). Node.js moves CPU work to worker_threads. .NET async uses the same idea on a thread pool, so it gets both.
Actors: no shared memory and no locks. Erlang and Akka.NET add supervisors ("let it crash"). Orleans adds virtual actors that the runtime places and activates for you (23.10).
CSP: "Don't communicate by sharing memory; share memory by communicating" (Go proverb).
Data parallelism is parallelism, not concurrency (1.2). Amdahl's law still caps the speed-up.
How to answer "which model?"
CPU or I/O?name the work
β
I/O: asyncevent loop / tasks
β
Stages? channelbounded
β
Many stateful entities? actors
β
CPU on big data? parallel
Most real systems mix models: async I/O at the edge, channels inside, locks for a few hot fields.
Never block an event loop or an actor: it stalls everything queued behind it.
Every queue needs a bound (back-pressure), or memory grows until the process dies.
Actors and channels remove locks, not ordering, retry and overload problems.
Shared memory within one process; messages between processes and machines.
.NET 10 How to implement it
Model
Package
Use it for
Tasks / async
built-inSystem.Threading.Tasks
Async I/O on the thread pool; Task.WhenAll to fan out
Channels (CSP)
built-inSystem.Threading.Channels
Bounded producer/consumer queues between tasks
TPL Dataflow
System.Threading.Tasks.Dataflow 10.0 (NuGet, MIT)
Linked blocks, each with its own buffer and parallelism
Virtual actors
Microsoft.Orleans.Server, .Client, .Sdk 10.x
Grains across a cluster; single-threaded per grain by default
Spread CPU work across cores; SIMD inside one core
// An actor in miniature: one mailbox, one reader, private state, no locks
public sealed class AccountActor
{
private readonly Channel<decimal> _mailbox = Channel.CreateBounded<decimal>(
new BoundedChannelOptions(1_000) { SingleReader = true, FullMode = BoundedChannelFullMode.Wait });
private decimal _balance; // only RunAsync touches it
public ValueTask PostAsync(decimal amount, CancellationToken ct) =>
_mailbox.Writer.WriteAsync(amount, ct); // waits when full: back-pressure
public async Task RunAsync(CancellationToken ct) // start once, e.g. from a BackgroundService
{
await foreach (var amount in _mailbox.Reader.ReadAllAsync(ct))
_balance += amount; // one message at a time
}
}
Orleans grains are non-reentrant by default: if grain A awaits B and B calls back into A, they deadlock until the request times out. Avoid call cycles, or allow reentrancy on purpose (RequestContext.AllowCallChainReentrancy(), [AlwaysInterleave] or [Reentrant]).
Five ways to run work at the same time, and how each one breaks. π§
π Threads + locks: fast, until races and deadlocks.
π Event loop: one thread, thousands of I/O waits. One CPU-heavy call freezes everyone.
π¬ Actors: own your state, one message at a time. Watch for full mailboxes and call cycles.
π° Channels: share a pipe, not memory. Always bound the pipe.
β‘ Data parallel: split the data across cores. Amdahl still wins.
Real systems mix them. Which one runs your hot path?
The kernel owns the CPU, memory, files and sockets. Your code asks for them through system calls. OOM kills, port exhaustion and async scaling all come down to how it hands them out.
Your codeuser space
β
System callread Β· write Β· epoll_wait
β
Kernelprivileged mode
β
Disk Β· NICdevice driver
How it works
Concept
What it means
Interview angle
User vs kernel space
Apps run unprivileged; a syscall jumps into the kernel and back
Crossings cost: batch I/O
Virtual memory
Private address space per process, mapped to RAM in pages (often 4 KB)
Swap one thread's registers for another's: microseconds, plus cold caches
10k threads = switching, not working
Processes, threads and stacks are in 1.1. The caller's view of waiting is in 1.3, and the event loop as a programming model in 1.7.
I/O models: why async scales
Model
Thread while waiting
Where you see it
Blocking
Asleep, held
Thread-per-request servers, scripts
Non-blocking
Free, but must keep asking (EAGAIN)
Building block, rarely used alone
Multiplexing (readiness)
Free; one waiter for many sockets
epoll (Linux), kqueue (BSD, macOS)
Completion
Free; kernel does the I/O
IOCP (Windows), io_uring (Linux 5.1+)
Async / event loop wins
Many concurrent, mostly idle connections: APIs, gateways, WebSockets.
Memory and thread count stay flat as connections grow.
Threads are fine when
Few connections, or CPU-bound work (async doesn't add cores).
Simplicity matters more than scale: batch jobs, CLIs.
137 / OOMKilled = memory limit. Compare it with working set; hunt unbounded caches and queues.
"Too many open files" = fd limit. SocketException under load = port exhaustion. Both say: reuse connections.
Async scales because a waiting request holds a kernel registration, not a thread.
Low CPU + high latency = threads blocked, not busy. See 23.4.
.NET 10 How to implement it
Library / tool
Package
Use it for
Sockets, SocketsHttpHandler, Kestrel
built-inSystem.Net.Sockets
Async socket calls sit on epoll (Linux), kqueue (macOS) and IOCP (Windows); completions resume on the thread pool
Container-aware GC
built-in runtime config
With a container memory limit, the GC heap defaults to 75% of it. Tune with DOTNET_GCHeapHardLimitPercent
dotnet-counters
dotnet-counters (.NET global tool)
Live GC heap, working set, thread-pool queue, HTTP connections
dotnet-trace
dotnet-trace (.NET global tool)
CPU sampling and runtime events; open in Visual Studio or PerfView
dotnet-dump
dotnet-dump (.NET global tool)
Capture and analyse a dump: heap by type, thread stacks, gcroot
# install once (or copy the single-file builds into the container)
dotnet tool install --global dotnet-counters
dotnet tool install --global dotnet-trace
dotnet tool install --global dotnet-dump
# memory climbing toward the limit? watch it live
dotnet-counters monitor -n PaymentsApi --counters System.Runtime,System.Net.Http
# where is the CPU going? (Ctrl+C to stop)
dotnet-trace collect -p 4242
# what is holding the memory? dump before the OOM kill, then inspect
dotnet-dump collect -p 4242
dotnet-dump analyze <dump-file> # then: dumpheap -stat, gcroot <address>
An OOM kill is SIGKILL: no exception, no crash dump. Alert on working set near the limit and capture a dump while it climbs.
"Our pod keeps restarting with exit code 137." Here's what the OS is telling you π§
π₯ 137 = SIGKILL from the kernel: the container hit its memory limit. No exception, no log line
π "Too many open files" or SocketException under load = file descriptors or ephemeral ports ran out. Reuse connections
π 10,000 threads mostly means 10,000 context switches and stacks
β‘ epoll, kqueue, IOCP and io_uring let a few threads wait on thousands of sockets. That's why async servers scale
Know these four and half the "it's slow / it crashed" incidents explain themselves. Which one bit you last?
Storage is a ladder: each step down is bigger, cheaper per GB and much slower. Fast systems keep hot data high on the ladder and turn random access into sequential access.
The numbers to quote are in the 1.4 and 1.5 latency ladder. Rule of thumb: each step is roughly 10β1,000Γ slower than the one above.
Locality: why arrays beat linked lists
How it works
Temporal locality: data used now is likely used again soon (so cache it). Spatial locality: its neighbours are likely next (so read whole lines and pages).
Sequential beats random, on every device. That's why append-only logs (WAL, Kafka) and LSM trees (RocksDB, Cassandra) exist: they turn random writes into sequential ones.
Page cache: a write() lands in OS memory and reaches the disk later. Reads of hot files are served from RAM.
Durable means it survives a power cut: only after fsync (or write-through) returns. Databases fsync the WAL on commit and batch commits to share one fsync (group commit).
Row vs columnar: rows keep a record together; columns keep each field together, so scans read less and compress better.
Write
β
WAL append+ memtable in RAM
β
Flushsorted file (SSTable)
β
Compactionmerge in background
Trade-offs
Choice
Wins at
Pays with
B-tree (SQL Server, PostgreSQL)
Reads, range scans
Random in-place writes
LSM tree
High write rates
Reads check several files; compaction I/O
Row store (OLTP)
Fetch or update one record
Scanning a few columns of many rows
Columnar (Parquet, columnstore)
Analytics scans, compression
Single-row updates, small writes
Index internals: 6.3 Indexing. Block, file and object storage: 6.8. Parquet in a lakehouse: 6.13.
Caches exist to hide the gap between rungs. Hit rate decides speed.
Arrays beat linked lists because of cache lines, not big-O.
write() returning is not durable. fsync is; in a cluster, durable usually also means "on N replicas".
Append, batch, then compact: sequential I/O is the cheapest I/O.
OLTP on rows, analytics on columns. Don't run big reports on the OLTP primary.
An array of structs is contiguous; an array of classes is an array of pointers
FileStream + FileStreamOptions
built-inSystem.IO
BufferSize, FileOptions.SequentialScan, FileOptions.WriteThrough; Flush(true) asks the OS to flush to disk
Parquet.Net
Parquet.Net 6.1 (MIT, .NET 8+)
Read and write columnar Parquet files
// Append-only journal: sequential writes, durable on return
var opts = new FileStreamOptions
{
Mode = FileMode.Append, Access = FileAccess.Write, Share = FileShare.Read,
BufferSize = 64 * 1024, Options = FileOptions.Asynchronous
};
await using var journal = new FileStream("payments.journal", opts);
await journal.WriteAsync(record, ct); // in .NET's buffer / OS page cache
journal.Flush(flushToDisk: true); // fsync: now it survives a power cut
// Cache-friendly: contiguous structs and a pooled buffer
public readonly record struct Tick(long Time, decimal Price);
byte[] buffer = ArrayPool<byte>.Shared.Rent(81_920);
try { /* read into buffer.AsSpan() */ }
finally { ArrayPool<byte>.Shared.Return(buffer); }
// Columnar export for analytics
await ParquetSerializer.SerializeAsync(transactions, "tx-2026-10.parquet");
Flush() and FlushAsync() only move .NET's buffer into the OS page cache. For durability use Flush(true) or FileOptions.WriteThrough, and batch writes so you don't fsync per record.
Why is an array faster than a linked list when both are O(n)? π§
π The CPU reads memory in 64-byte cache lines. Arrays use every byte; linked lists jump around and miss
πΎ Disks love sequential I/O. That's why Kafka, WALs and LSM trees append instead of updating in place
β οΈ write() returning isn't durable. The data sits in the OS page cache until fsync
π OLTP wants rows, analytics wants columns. That's the whole case for Parquet
Big-O tells you the shape. The storage ladder tells you the real cost. What's your favourite locality win?
#SystemDesign #Performance #Databases #dotnet
1.10 Data structures behind real systems Added
Every database, cache and search engine is a few classic data structures underneath. Know which one, its cost and its trade-off, and you can justify any design choice.
A bounded buffer gives back-pressure: block or drop.
.NET 10 How to implement it
Library / API
Package
Use it for
Dictionary / HashSet
built-inSystem.Collections.Generic
Hash tables: O(1) average lookups, "seen before?"
SortedDictionary / SortedSet
built-inSystem.Collections.Generic
Self-balancing (red-black) tree: ordered keys, min/max, ranges via SortedSet.GetViewBetween
PriorityQueue<TElement,TPriority>
built-inSystem.Collections.Generic
Array-backed quaternary min-heap: top-K, schedulers. Equal priorities are not FIFO
FrozenDictionary / FrozenSet
built-inSystem.Collections.Frozen
Read-only hash lookups built once at startup, fastest reads
Bounded Channel<T>
built-inSystem.Threading.Channels
Bounded buffer with back-pressure; FullMode: Wait, DropOldest, DropNewest, DropWrite
StackExchange.Redis
StackExchange.Redis
Sorted sets (SortedSetAddAsync, rank, range) and GEO commands
Lucene.NET
Lucene.Netpreview
In-process inverted index (4.8.0 is still beta). Most teams use a search service
// Top-K largest payments: keep a min-heap of size K, O(n log K)
static List<Payment> TopK(IEnumerable<Payment> all, int k)
{
var heap = new PriorityQueue<Payment, decimal>(k);
foreach (var p in all)
{
if (heap.Count < k) heap.Enqueue(p, p.Amount);
else if (p.Amount > heap.Peek().Amount) heap.EnqueueDequeue(p, p.Amount);
}
return heap.UnorderedItems.Select(x => x.Element)
.OrderByDescending(p => p.Amount).ToList();
}
// Ring-buffer style log queue: never grows, drops the oldest when full
var logs = Channel.CreateBounded<string>(new BoundedChannelOptions(10_000)
{ FullMode = BoundedChannelFullMode.DropOldest, SingleReader = true });
Every "big system" is a handful of data structures wearing a trench coat π§₯
The classic opener: walk one request from the address bar to DNS, TCP/QUIC, TLS, the edge, your backend and back to pixels on screen.
How it works, step by step
HSTS: a host on the HSTS list is upgraded to https before anything is sent.
Caches first: a fresh HTTP cache entry skips the network; a cached DNS answer skips the lookup.
DNS: the recursive resolver walks root β TLD β authoritative. The answer is often a CNAME to a CDN, cached for the TTL.
Connect: TCP handshake, then TLS 1.3 (SNI, ALPN, certificate, keys). QUIC does both in one round trip.
Response: compressed (br/gzip) with Cache-Control and ETag, so the next visit is cheaper.
Render: sub-resources reuse the connection; render-blocking JS delays first paint.
Trade-offs you tune at each layer
Layer
Knob
Trade-off
DNS
TTL
Long = fewer lookups, slow failover
TLS
Cert lifetime
Short = safer, must automate renewal
CDN
Cache TTL
Higher hit ratio vs stale content
Load balancer
Sticky sessions
Simple state vs uneven load
Response
Compression
Fewer bytes vs CPU
π€ How to answer in an interview
1. Breadth first: walk the whole path in about 60-90 s, one line per layer. 2. Ask: "Which layer shall I go deep on?" 3. Go deep with numbers: RTTs, TTLs, cache hit ratio. 4. Close with failures and how you'd observe them (traces, p99).
β οΈ Common slips
Skipping caches, HSTS or the certificate check. Stopping at the server and forgetting rendering.
Breadth first, then depth: let the interviewer choose the layer.
DNS: resolver β root β TLD β authoritative, cached for the TTL.
Client-server: clients call a central server that owns the data. Easy to secure, govern and update; the server must scale. P2P: every node is both client and server (BitTorrent, blockchain, WebRTC media). No central bottleneck, scales with users, but hard to trust, secure and keep consistent. Banking and most business systems are client-server.
GeoDNS and anycast
GeoDNS: the DNS server answers with a different IP based on the resolver's location (latency or geo policy). Failover is limited by TTL caching. Anycast: many sites announce the same IP over BGP; the network delivers you to the nearest one. Used by CDNs, public DNS and global load balancers; fails over in seconds, no DNS change.
Forward proxy
Reverse proxy
Sits in front of
Clients
Servers
Hides
Who the client is
Which server answers
Used for
Egress control, URL filtering, caching
TLS offload, routing, LB, caching, WAF
Examples
Corporate web proxy, Squid
NGINX, Envoy, YARP, API gateway
π― Interview angle: "Draw a high-level design"
1. Clarify requirements and scale first (1.13, 1.5). 2. Draw the simple path: client β API β service β DB. 3. Add blocks by requirement and say why: "reads are 100:1, so a cache". 4. Walk one request end to end through the boxes. 5. Name failure points and how you'd see them (metrics, traces).
β οΈ Common slips
Drawing Kafka, Redis and a service mesh in minute one with no requirement behind them. No observability.
Start simple; every extra box needs a requirement behind it.
YARP is a library, not a product: you host it in ASP.NET Core and add auth, rate limiting and telemetry with normal middleware. For a managed gateway with developer portal and subscriptions, use an APIM product (9.1).
Every system design interview uses the same dozen boxes. The skill is knowing when to draw each one π§±
π DNS + CDN: get users to the nearest edge.
βοΈ Load balancer + stateless apps: scale out by adding copies.
Functional requirements say what the system does; non-functional ones (the "-ilities") say how well, and only count when they have a number.
Functional (FR)
Features and behaviour: "a customer can transfer money", "an admin can reverse a payment". Write as user stories or use cases; they shape the APIs and data model.
Non-functional (NFR)
Qualities and constraints: speed, uptime, security, cost. They shape the architecture: replicas, caches, regions, queues. Full catalogue: 12.9; trade-offs: 13.2.
Read:write ratio drives the design: 100:1 β cache + replicas; write-heavy β partitioning, append-only logs, queues.
Access pattern: key lookup, range scan, full-text, aggregate, or "latest N"? It picks the database and index.
Shape of traffic: steady, daily peak, or spikes (pay day, flash sale)? Spikes call for queues and autoscaling.
Turn the numbers into storage and QPS with 1.5 estimation.
π― Interview angle
Classic: "Design X" (the first 5 min), "What NFRs matter here?", "How would you prove p99?" 1. Split FR and NFR out loud; write them on the board. 2. Put a number on each NFR, or state your assumption. 3. Rank the top 2-3 qualities; name what you trade away. 4. Tie each design choice back to an NFR. Framework: 20.2.
β οΈ Common slips
"It must be fast and highly available" with no numbers. Using average latency. Asking 20 questions instead of assuming and moving on.
FR = what, NFR = how well; NFRs drive the architecture.
An NFR without a number is a wish.
Latency in p95/p99; availability in nines (99.99% β 52.6 min/year).
RPO = data you can lose; RTO = time to recover.
Read:write ratio and access pattern pick the cache and database.
Rank qualities: you can't max them all, so say what you trade.
.NET 10 How to implement it
Library / API
Package
Use it for
Meter + Histogram<T>
built-inSystem.Diagnostics.Metrics
Record durations; the backend computes p95/p99 from buckets
http.server.request.duration
built-in ASP.NET Core meter
Request latency histogram, emitted by the framework
OpenTelemetry hosting
OpenTelemetry.Extensions.Hosting
Wire up metrics and traces
OTLP exporter
OpenTelemetry.Exporter.OpenTelemetryProtocol
Send to any OTLP backend (Prometheus, Grafana, Azure Monitor)
k6
Grafana k6, open source CLI
Load test with thresholds, e.g. p(95)<200
NBomber
NBombercommercial licence
Load tests in C#; free only for personal use
// Custom histogram for one business operation
public sealed class PaymentMetrics
{
public static readonly Meter Meter = new("Bank.Payments");
public static readonly Histogram<double> Duration =
Meter.CreateHistogram<double>("payments.submit.duration", unit: "s");
}
builder.Services.AddOpenTelemetry()
.WithMetrics(m => m
.AddMeter("Microsoft.AspNetCore.Hosting") // http.server.request.duration
.AddMeter("Bank.Payments")
.AddOtlpExporter());
var sw = Stopwatch.StartNew();
await payments.SubmitAsync(cmd);
PaymentMetrics.Duration.Record(sw.Elapsed.TotalSeconds,
new KeyValuePair<string, object?>("channel", "mobile"));
Prove an NFR twice: a load test before release (k6 threshold fails the build) and a p99 SLO alert in production from the same histogram.
"It must be fast, secure and highly available." That's not a requirement. That's a wish π―
β±οΈ Fast β p95 < 200 ms, p99 < 500 ms on the payment path.
π’ Available β 99.95%, about 4.4 hours of downtime a year.
πΎ Recoverable β RPO 0, RTO 15 minutes.
π Scalable β 10k req/s at peak, 10Γ in two years.
Numbers turn arguments into design choices. 99.9% and 99.99% are different architectures, and different budgets.
In a design interview, the first 5 minutes are for questions like these. Which one do you always ask first?
Scale the cheap, reversible way first (caches, replicas, queues). Partition (shard) only when one database can't take the writes or hold the data, because every query then needs a shard key.
The scaling toolkit (primer)
Move
In one line
Deep
Vertical vs horizontal
Bigger box (simple, has a ceiling) vs more boxes (no ceiling, needs stateless apps)
Real systems do both: each shard is a small replicated cluster.
Sharding strategies
Strategy
Wins
Pays
Range (date, A-H)
Range scans, simple
Hotspots on the newest range
Hash (hash(key))
Even spread
Ranges fan out to all shards
Directory (lookup table)
Move any key or tenant freely
Extra hop; the directory must be HA and cached
Geo / tenant
Data residency, isolation
Uneven sizes ("whale" tenants)
A good shard key has high cardinality, spreads load evenly and is in most queries. Ladder and examples: 6.7 Database scaling.
Hot partitions, rebalancing and the bill
Hot key fixes:cache hot reads; salt or write-shard hot writes (N sub-keys, sum on read); split the hot partition; give a whale tenant its own shard.
Rebalancing:hash % N moves almost every key when N changes. Consistent hashing moves about 1/N (3.4). Or pre-split into many fixed slots and move slots, not keys.
Cross-shard cost: queries become scatter-gather; transactions need a saga or 2PC (4.6); IDs must be globally unique (6.4).
Interview angle: "How would you scale this to 10x?"
Also: "Replicas or shards?", "Pick a shard key", "One customer is hot. Now what?"
Measure: which resource saturates first (CPU, DB reads, DB writes, storage)?
Cache hot reads; push static content to a CDN.
Read replicas for read-heavy load; route read-your-own-writes to the primary.
Async: move slow or spiky work to a queue.
Partition last: name the shard key, the hot-key plan and the cross-shard cost.
Replicas scale reads; shards scale writes and size.
The shard key is the decision you can't easily undo. Pick it from the main query.
Range = scans but hotspots. Hash = even but no ranges.
A hot key beats any strategy: salt, split or cache it.
Consistent hashing or fixed slots make rebalancing cheap.
.NET 10 How to implement it
Library / API
Package
Use it for
Azure Cosmos DB SDK
Microsoft.Azure.Cosmos 3.63
Partition keys (up to 3 levels with hierarchical keys); a logical partition holds up to 20 GB and 10,000 RU/s
EF Core SQL Server
Microsoft.EntityFrameworkCore.SqlServer 10.0
One DbContext per shard database
DbContextOptionsBuilder
built-in EF Core
Build options per tenant from a directory lookup (shard routing)
// Directory sharding: look up the tenant's shard, then open a context on it
public sealed class ShardedContextFactory(ITenantDirectory directory)
{
public async Task<PaymentsContext> CreateAsync(string tenantId, CancellationToken ct)
{
string conn = await directory.GetConnectionStringAsync(tenantId, ct); // cache this
var options = new DbContextOptionsBuilder<PaymentsContext>()
.UseSqlServer(conn).Options;
return new PaymentsContext(options);
}
}
// Cosmos DB: hierarchical partition key, tenant then account
Container payments = await database.CreateContainerIfNotExistsAsync(
new ContainerProperties("payments", ["/tenantId", "/accountId"]));
PartitionKey pk = new PartitionKeyBuilder().Add(p.TenantId).Add(p.AccountId).Build();
await payments.CreateItemAsync(p, pk); // single-partition write
var read = await payments.ReadItemAsync<Payment>(p.Id, pk); // cheapest read: id + key
// Hot counter: write-shard into 8 sub-keys, sum them on read
string subKey = $"{merchantId}#{Random.Shared.Next(8)}";
A Cosmos DB transaction (TransactionalBatch) only spans one logical partition. Queries without the partition key fan out across partitions and cost more RUs.
Pick storage from the access pattern: how you read, write and query the data, how fresh it must be, and whether you need answers now (OLTP, stream) or over everything (OLAP, batch).
Primer
Concept
In one line
Deep
SQL vs NoSQL
Schema + joins + transactions vs models built for one access pattern at scale
How data moves between parts of a system: who starts the exchange (push or pull), when (sync or async) and how much at a time (pages, streams, fan-out).
In-process queue; a full bounded channel makes writers wait (backpressure)
Keyset paging
Microsoft.EntityFrameworkCore 10
Where on the last key + OrderBy + Take
// Push: a worker drains a bounded channel and broadcasts via SignalR
builder.Services.AddSignalR();
builder.Services.AddSingleton(Channel.CreateBounded<PriceTick>(
new BoundedChannelOptions(1_000) { FullMode = BoundedChannelFullMode.Wait }));
builder.Services.AddHostedService<PricePusher>();
app.MapHub<PricesHub>("/hubs/prices");
public sealed class PricePusher(Channel<PriceTick> ch, IHubContext<PricesHub> hub) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
await foreach (var t in ch.Reader.ReadAllAsync(ct))
await hub.Clients.Group(t.Pair).SendAsync("tick", t, ct);
}
}
// Keyset pagination: index (AccountId, BookedAt, Id)
var page = await db.Transactions
.Where(t => t.AccountId == accountId &&
(t.BookedAt < after.BookedAt ||
(t.BookedAt == after.BookedAt && t.Id < after.Id)))
.OrderByDescending(t => t.BookedAt).ThenByDescending(t => t.Id)
.Take(50)
.ToListAsync(ct);
Self-hosted SignalR on more than one instance needs a Redis backplane and sticky sessions (unless every client is WebSocket-only). Azure SignalR Service removes both.
"How will clients get updates?" is a trade-off question, not a trivia question π‘
π Polling: simple, but every empty call is waste.
π SSE: one HTTP stream, server to client. Great for feeds and AI token streams.
π WebSocket: two-way and fastest, but stateful. Plan the backplane.
π° News feed? Fan-out on write for most users, fan-out on read for celebrities.
π Big lists? Cursor pagination. Page 10,000 costs the same as page 1.
Start with freshness, direction and client count. The protocol choice follows.
#SystemDesign #RealTime #APIs #InterviewPrep
1.17 Reliability fundamentals Added
A reliable system keeps doing the right thing when parts fail: no single point of failure, fast detection, bounded retries and a planned way to degrade.
Split brain: after a partition, two nodes both act as primary and data diverges.
Prevent: majority quorum (or a witness), expiring leases, fence the old node.
Fencing token: each lease carries an increasing number; storage rejects older tokens, so a paused ex-leader can't write (4.5). Leader election and failure detection: 1.18.
Checksums and end-to-end integrity
Corruption is silent: bit flips, bad disks, truncated files.
CRC catches accidents cheaply; SHA-256 also catches tampering; HMAC proves the sender.
End to end: checksum at the producer, verify at the final consumer, not per hop.
Money and batch: control totals, reconciliation.
π€ "What happens when X fails?"
1. Draw the path, list components. 2. For each: detection, blast radius, automatic response. 3. Data: RPO, duplicates, split brain. 4. Slow, not down: timeouts, shedding. 5. Close: RTO/RPO and how you test it (15.16).
β οΈ Common slips
Retries at every layer. DB checks in liveness. Unrehearsed failover.
Find SPOFs by asking "if only this dies?" along the whole path.
Cold β warm β hot β active-active: RTO falls, cost rises.
Liveness restarts; readiness removes from traffic.
Retries need timeouts, backoff + jitter, a budget and idempotency.
Under overload, shed early and degrade by business priority.
Polly has no built-in retry budget. Keep MaxRetryAttempts low, let the circuit breaker stop retry storms, and retry at one layer only.
"What happens when the database fails?" is the reliability question every design review asks π₯
π Find SPOFs: walk the path and ask "if only this dies?" Don't forget DNS and certificates.
π Liveness restarts a process; readiness takes it out of traffic. Mix them up and one DB blip restarts everything.
β±οΈ Retries need timeouts, backoff, jitter and a budget, or they turn an outage into a storm.
πͺ« Overloaded? Shed load early, then switch off the nice-to-haves before the must-haves.
π§ Two primaries after a network split? Quorum and fencing tokens.
Slow is often worse than down. Design for both.
#SystemDesign #Reliability #SRE #Resilience
1.18 Distributed coordination primitives Added
The building blocks that let many nodes agree (consensus, quorum), find each other (discovery, gossip), detect failure (heartbeats) and repair drift (Merkle trees).
Primer: the core primitives
Primitive
In one line
Deep dive
Replication
Copies on several nodes: leader-follower, multi-leader or leaderless
How: each second, every node swaps state (members, heartbeats, versions) with a few random peers. Newer wins.
Converges in ~O(log N) rounds (1,000 nodes ≈ 10). No central server.
Used by:Cassandra (cluster state), Consul / Serf (SWIM-style membership), Redis Cluster (cluster bus), Amazon's Dynamo design.
Trade-off:eventually consistent; fine for membership, not for "exactly one leader".
Merkle trees and anti-entropy repair
Merkle tree: leaves hash key ranges; each parent hashes its children. Equal roots = equal data.
Anti-entropy: a background job compares trees and copies only the differing ranges (e.g. Cassandra nodetool repair).
Same idea in Git trees and blockchain blocks.
Failure detection, leader election, discovery
Failure detection
Heartbeat + timeout: no beat in T ⇒ suspect.
You can't tell slow from dead (GC pause, partition), so a detector only suspects.
Phi accrual: learns the beat-interval distribution and outputs a suspicion level φ; act above a threshold (Cassandra, Akka).
Short timeout = false alarms; long = slow failover.
Leader election
Bully / ring: highest ID wins. Textbook; unsafe under partitions.
Lease via a coordinator: etcd, ZooKeeper, a Kubernetes Lease or a blob lease. Holder renews; expiry = new election.
Consensus log: Raft elects a leader per term.
Always add a fencing token: two leaders can overlap briefly.
Service discovery
Registry (Consul, etcd, Eureka, Kubernetes) lists healthy instances; TTL or health checks expire them.
Client-side: client asks the registry and load-balances itself.
Server-side: client calls a stable name (DNS, load balancer, K8s Service) that routes for it.
Interview angle: "how do nodes agree / find each other / detect failure?"
Split the question: membership, failure detection, agreement, repair.
Membership: registry for services; gossip for large peer clusters.
Failure: heartbeats + timeout (or phi accrual); say "suspect, not dead".
Agreement: need one leader or strict order? Use Raft / etcd plus fencing. Else quorum + anti-entropy.
Close with the cost: majority-write latency vs eventual convergence.
Gossip spreads state in ~O(log N) rounds with no central node; eventually consistent.
Merkle trees find differing ranges in O(log n) comparisons; anti-entropy syncs only those.
A failure detector can only suspect: slow and dead look the same.
Leader election = lease or consensus, plus a fencing token.
Discovery: client-side (smart client) vs server-side (LB/DNS) over a health-checked registry.
.NET 10 How to implement it
Library / API
Package
Use it for
Orleans membership
Microsoft.Orleans.Server 10.x + a clustering provider, e.g. Microsoft.Orleans.Clustering.AzureStorage
Silos probe each other and vote dead silos out via a membership table. See 23.10
DistributedLock
DistributedLock 2.8 (meta-package; or one provider, e.g. DistributedLock.SqlServer, DistributedLock.Azure)
Locks / leader-only work over SQL Server, Postgres, Redis, Azure blob leases, ZooKeeper; HandleLostToken tells you the lock was lost
BlobLeaseClient
Azure.Storage.Blobs 12.x
Lease-based leader (15–60 s, renewable). Example in 4.4
Service discovery
Microsoft.Extensions.ServiceDiscovery 10.x
Client-side: https+http://accounts resolves from config, Aspire or Kubernetes DNS
SHA256.HashData
built-inSystem.Security.Cryptography
Merkle leaf and node hashes
// Leader-only job with DistributedLock (SQL Server provider)
var leaderLock = new SqlDistributedLock("nightly-settlement", connectionString);
await using var handle = await leaderLock.TryAcquireAsync(TimeSpan.FromSeconds(5), ct);
if (handle is null) return; // another node is leader
using var lost = CancellationTokenSource.CreateLinkedTokenSource(ct, handle.HandleLostToken);
await RunSettlementAsync(lost.Token); // stop if the lock is lost
// Merkle root over key ranges: compare roots first, then children
static byte[] MerkleRoot(IReadOnlyList<byte[]> ranges)
{
var level = ranges.Select(r => SHA256.HashData(r)).ToList();
while (level.Count > 1)
{
var next = new List<byte[]>();
for (int i = 0; i < level.Count; i += 2)
{
var right = i + 1 < level.Count ? level[i + 1] : level[i]; // odd count: repeat last
byte[] pair = [.. level[i], .. right];
next.Add(SHA256.HashData(pair));
}
level = next;
}
return level[0];
}
bool inSync = MerkleRoot(replicaA).AsSpan().SequenceEqual(MerkleRoot(replicaB));
A lock or lease is not a guarantee of exclusivity after a long GC pause. Pass a fencing token (or row version) to the resource you write.
How does a 1,000-node cluster learn that one node died, with no central server? π£οΈ
π¦ Gossip: each node tells a few random peers every second. Everyone knows in about 10 rounds (log N).
π Heartbeats + timeouts only suspect failure. Slow and dead look the same.
π³ Merkle trees: compare one root hash, then drill down. Repair only the ranges that differ.
π Need exactly one leader? Use a lease or Raft, and always add a fencing token.
π§ Services find each other through a registry: client-side or server-side discovery.
1.19 Security and observability in every design Added
Every design answer needs a security baseline (who can do what, data protected in transit and at rest) and an observability baseline (logs, metrics, traces, SLOs). Mention both in 60 seconds, then move on.
Automatic spans and metrics for incoming requests, HttpClient and SQL
Exporters
OpenTelemetry.Exporter.OpenTelemetryProtocol 1.x, or Azure.Monitor.OpenTelemetry.AspNetCore 1.x
Send to any OTLP backend, or to Application Insights
var builder = WebApplication.CreateBuilder(args);
// Secrets from Key Vault via managed identity (no secrets in appsettings)
builder.Configuration.AddAzureKeyVault(
new Uri("https://payments-kv.vault.azure.net/"), new DefaultAzureCredential());
// AuthN + AuthZ
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o => { o.Authority = builder.Configuration["Auth:Authority"]; o.Audience = "api://payments"; });
builder.Services.AddAuthorizationBuilder()
.AddPolicy("payments.write", p => p.RequireRole("Payments.Writer"));
// Traces, metrics and logs; W3C traceparent is propagated automatically
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService("payments-api"))
.WithTracing(t => t.AddAspNetCoreInstrumentation().AddHttpClientInstrumentation().AddOtlpExporter())
.WithMetrics(m => m.AddAspNetCoreInstrumentation().AddHttpClientInstrumentation().AddOtlpExporter());
builder.Logging.AddOpenTelemetry(o => o.AddOtlpExporter());
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapPost("/payments", CreatePayment).RequireAuthorization("payments.write");
app.Run();
In production, prefer a specific credential such as ManagedIdentityCredential over DefaultAzureCredential, and never log tokens, secrets or full card numbers.
Your system design was great. Then the interviewer asked: "How is it secured, and how would you know it's broken?" π€
πͺͺ AuthN at the gateway, AuthZ in the service. Least privilege everywhere.
π TLS in transit, encryption at rest, secrets in a vault via managed identity.
π Classify the data, mask PII, keep an audit log.
π Logs, metrics and traces, joined by one correlation ID.
π¨ Alert on SLO burn that users feel, not on CPU.
Ten points, 60 seconds, then back to the design. What is on your checklist?
They share the parent's memory and resources, so creating and switching them needs no new address space.
Is async always better?
No. It adds complexity (ordering, retries, eventual consistency). Use it when decoupling, spike absorption or long-running work justify it.
Why does p99 latency matter more than average?
If one page calls 10 services, most users hit at least one tail-latency call. The average hides the pain.
How do you prevent deadlocks?
Break one Coffman condition: take locks in one global order (no circular wait), use timeouts and release on failure, keep lock scope small, and avoid locks with immutable data or atomic operations. Databases detect deadlocks and kill a victim, so retry it.
Optimistic or pessimistic concurrency?
Optimistic (row version / ETag, retry on conflict) when conflicts are rare; pessimistic (SELECT ... FOR UPDATE in a short transaction) when the row is hot and retries are costly.
Actors or shared memory with locks?
Actors when you have many independent stateful entities (accounts, devices, sessions): each owns its state and handles one message at a time, so no locks. Shared memory with locks for a few hot fields inside one process, where message passing would only add overhead.
What breaks an event loop?
Any CPU-heavy or blocking call on the loop thread: every other request waits behind it. Move CPU work to worker threads and keep I/O async.
Why did our container get OOM-killed when the app never threw OutOfMemoryException?
The container hit its cgroup memory limit, so the kernel killed the process with SIGKILL (exit code 137, OOMKilled). Nothing in the app gets a chance to run. Compare the limit with working set, look for unbounded caches or queues, and capture a dump with dotnet-dump while memory climbs.
Why can an async server handle 10,000 connections on a handful of threads?
Waiting sockets are registered with the kernel (epoll, kqueue, IOCP, io_uring) instead of each holding a blocked thread. The kernel reports which ones are ready or done, and a few threads run just those. Threads only cost something when there's work to do.
When is a write actually durable?
When it's on stable storage, not just in the OS page cache: after fsync (or a write-through write) returns. Databases fsync the write-ahead log on commit and group commits to share one fsync. In a replicated system, durable usually also means acknowledged by N replicas.
Why do analytics systems store data by column?
Reports scan a few columns over millions of rows. Columnar formats like Parquet read only those columns, compress similar values well, and skip row groups using min/max stats. Row stores suit OLTP, where you fetch or update whole records.
Why do write-heavy stores like Cassandra use an LSM tree instead of a B+tree?
An LSM tree turns writes into sequential appends (WAL + memtable, flushed to SSTables), so ingest is fast. The cost is reads across several files and background compaction; Bloom filters keep those reads cheap.
Which structure would you use for a real-time leaderboard?
A Redis sorted set (a skip list plus a hash): O(log n) score updates, rank lookup and top-N range queries.
What happens when you type a URL? (the short version)
URL parse + HSTS, caches, DNS (resolver β root β TLD β authoritative), TCP + TLS 1.3 (or QUIC), HTTP request through CDN/WAF, load balancer, gateway, app, cache, DB, then a compressed, cacheable response the browser turns into DOM + CSSOM β layout β paint. Go breadth first, then ask which layer to go deep on.
Why is HTTP/3 faster to connect than HTTP/2?
HTTP/2 needs a TCP handshake and then a TLS 1.3 handshake (2 round trips). HTTP/3 runs over QUIC, which combines transport and TLS into 1 round trip, or 0 on a resumed connection.
Forward proxy vs reverse proxy?
A forward proxy sits in front of clients and hides them (egress control, filtering). A reverse proxy sits in front of servers and hides them (TLS offload, routing, load balancing, WAF). NGINX, Envoy and YARP are reverse proxies.
GeoDNS vs anycast?
GeoDNS returns a different IP per user location, so failover waits for the DNS TTL. Anycast announces the same IP from many sites over BGP and the network routes to the nearest one, so failover takes seconds.
Why p99 instead of average latency?
Averages hide the slow tail. p99 is the latency 1 in 100 requests exceed; with many calls per page, most users hit the tail, so SLOs use p95/p99.
How much downtime does 99.99% allow?
About 52.6 minutes a year (about 4.3 minutes a month). 99.9% allows 8.76 hours a year; 99.999% about 5.26 minutes.
Read replicas or sharding: which fixes a write bottleneck?
Sharding. Replicas copy the data, so every write still goes through one primary. Partitioning splits the rows, so each shard takes only its slice of the writes.
How do you handle a hot partition?
Cache hot reads. Salt or write-shard hot writes into N sub-keys and merge on read. Split the partition, or move a whale tenant to its own shard.
Why do read-heavy systems denormalise?
A denormalised view answers a screen in one read with no joins. You pay with write fan-out and some staleness, so keep a normalised source of truth and build read models from it.
Batch or stream processing?
Batch for bounded data where completeness matters more than speed (statements, reconciliation). Stream for unbounded data that needs answers in seconds (fraud alerts), using windows and watermarks for late events.
Why does offset pagination get slow on deep pages, and what replaces it?
OFFSET makes the database read and throw away every skipped row, so page 10,000 costs far more than page 1, and inserts shift rows between pages. Keyset (cursor) pagination filters on the last key seen (e.g. date + id) with an index seek, so every page costs the same and stays stable.
Fan-out on write or fan-out on read for a news feed?
Both: push each post into followers' timeline caches for normal accounts (fast reads), but pull posts from celebrity accounts at read time and merge, so one post doesn't trigger millions of writes.
What is a fencing token and why do you need one?
An increasing number handed out with each lock or leader lease. Storage rejects writes carrying an older number, so a leader that paused (GC, network split) and lost its lease can't overwrite the new leader's data. It is the fix for split brain at the storage layer.
Why add jitter to exponential backoff?
Without it, every client that failed at the same moment retries at the same moments too, hitting the recovering service in synchronised waves. Randomising each wait spreads the retries out so the dependency can recover.
How does a gossip protocol spread cluster state, and how fast?
Each node periodically swaps state with a few random peers; newer versions win. The number of informed nodes roughly doubles each round, so the whole cluster converges in about O(log N) rounds with no central server, but only eventually.
Why can't a failure detector be certain a node is dead?
A slow node (GC pause, overload, network partition) looks the same as a dead one: both stop answering heartbeats. Detectors such as heartbeat timeouts or phi accrual only raise a suspicion; leaders still need leases and fencing tokens for safety.
What security points should every system design answer mention?
AuthN with OIDC/OAuth at the gateway, AuthZ policies in the service, TLS in transit and encryption at rest, secrets in a vault via managed identity, least privilege, input validation and rate limits, data classification with PII masking, and an audit log.
What is a correlation ID and why does it matter?
An ID (in practice the W3C traceparent trace ID) passed on every hop and message and written on every log line and span. One search then shows the whole request across services, which turns a multi-service incident into a single timeline.