Section 01 Β· Start here

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.

Dashed chips = concepts added to your list.

1.1 Process vs Thread

A process is a running program with its own memory. A thread is a worker inside a process that shares that memory.

Visual

Process A Β· own memory space Shared heap Β· globals Β· open files all threads read and write it at once β†’ needs locks Thread 1own stackown registers Thread 2own stackown registers Thread 3own stackown registers Process B separate memory Thread 1 can't see A's heap IPC pipes Β· sockets Β· message queues

How it works

ProcessThread
MemoryIsolatedShared within process
Creation costHeavyLight
CommunicationIPC (slower, safer)Shared variables (fast, needs locks)
Crash impactOnly that process diesOne bad thread can crash the whole process
ExampleEach Chrome tab, each containerWeb server thread pool handling requests
  • Shared memory brings race conditions, deadlocks and starvation. Use locks, mutexes, semaphores or immutable data.
  • Thread pools reuse threads, so you don't pay the cost of creating one per request.
  • Python's GIL lets only one thread run Python code at a time. For CPU-bound work, use multiple processes.
  • Node.js runs your JavaScript on one thread with an event loop and uses a background pool for I/O.
  • Lightweight "green" threads exist too: Go goroutines, Java 21 virtual threads, .NET async tasks. You can run millions of them.
  • A container is just an isolated process, not a VM.
How to implement it
Library / APIPackageUse it for
Processbuilt-in System.DiagnosticsStart, watch and stop other processes (Process.Start, WaitForExitAsync)
Thread / ThreadPoolbuilt-in System.ThreadingRaw threads are rare. Task runs work on the shared thread pool for you
Lock, SemaphoreSlim, Interlockedbuilt-in System.ThreadingProtect shared memory: Lock (dedicated lock type, .NET 9+), SemaphoreSlim (async-friendly, limits concurrency), Interlocked (lock-free counters)
Concurrent collectionsbuilt-in System.Collections.ConcurrentConcurrentDictionary, ConcurrentQueue: thread-safe without your own locks
IPC between processesbuilt-in System.IO.Pipes, System.IO.MemoryMappedFilesNamed pipes for messages; memory-mapped files to share a block of memory
gRPC on the same machineGrpc.AspNetCore + Kestrel ListenNamedPipe / ListenUnixSocketTyped, fast calls between local processes without opening a TCP port
public sealed class Account
{
    private readonly Lock _gate = new();   // .NET 9+ dedicated lock type
    private decimal _balance;
    private long _requests;

    public void Deposit(decimal amount)
    {
        Interlocked.Increment(ref _requests);   // lock-free counter
        lock (_gate) { _balance += amount; }    // one thread at a time
    }
}

Never hold a lock across an await (the compiler blocks it). Use SemaphoreSlim.WaitAsync when async code needs mutual exclusion.

Process vs Thread in one picture 🧡

🏠 Process = a house (own walls, own kitchen). πŸ‘¨β€πŸ‘©β€πŸ‘§ Threads = people in the house sharing the kitchen.

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.
How to implement it
Library / APIPackageUse it for
Task + async/awaitbuilt-in System.Threading.TasksConcurrency for I/O: many calls in flight, few threads. Task.WhenAll to fan out
Parallel.ForEachAsyncbuilt-in System.Threading.TasksProcess a list concurrently with a cap (MaxDegreeOfParallelism)
PLINQ / Parallel.Forbuilt-in System.Linq (AsParallel)True parallelism for CPU-bound work across all cores
Channelsbuilt-in System.Threading.ChannelsProducer/consumer queues between tasks, with back-pressure (bounded channels)
TPL DataflowSystem.Threading.Tasks.DataflowMulti-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
SynchronousAsynchronous
CouplingTight. Both must be up at the same timeLoose. The consumer can be down for a while
SimplicityEasy to code, debug and traceHarder: correlation IDs, retries, ordering
ConsistencyImmediate resultEventual consistency
Load spikesCaller feels every slowdownThe queue absorbs spikes (load levelling)
Best forQueries, 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."
How to implement it
LevelLibrary / APIUse it for
In codebuilt-in async / await, IAsyncEnumerable<T>Release the thread while waiting for I/O; stream results as they arrive
Background workbuilt-in BackgroundService / IHostedServiceLong-running workers inside the same app
Async HTTP APIbuilt-in TypedResults.AcceptedReturn 202 + a status URL, then do the work later
In-process queuebuilt-in System.Threading.ChannelsHand work from the request to the background worker (lost if the process dies)
Between systemsAzure.Messaging.ServiceBusDurable 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).

Same coffee, but the cafΓ© serves 5x more people. That's async: better throughput, not faster coffee.

#SystemDesign #APIs #Integration

1.4 Latency vs Throughput vs Bandwidth Added

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.
How to implement it
Library / APIPackageUse it for
Stopwatchbuilt-in System.DiagnosticsTime one operation: Stopwatch.GetTimestamp() + Stopwatch.GetElapsedTime(start) (no allocation)
Histogram / Counterbuilt-in System.Diagnostics.MetricsRecord latency (histogram, for p95/p99) and throughput (counter)
http.server.request.durationbuilt-in ASP.NET Core metricRequest latency and request rate for every endpoint, for free
dotnet-counters.NET global toolWatch request rate, latency, GC and thread-pool queue live on a running process
Response compressionbuilt-in AddResponseCompressionSend 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.

Latency ladder (approximate)

OperationTime
L1 cache reference~1 ns
Main memory (RAM) read~100 ns
SSD random read~16–100 Β΅s
Round trip within a data centre~0.5 ms
Redis GET over network~0.5–1 ms
Simple indexed DB query~1–10 ms
Sydney ⇄ US West round trip~150 ms

Handy conversions

  • 1 day β‰ˆ 86,400 s, about 10⁡ s
  • 1M requests/day β‰ˆ 12 req/s on average
  • Peak β‰ˆ 2–5Γ— average
  • 2¹⁰ β‰ˆ 1 thousand (KB), 2²⁰ β‰ˆ 1 million (MB), 2³⁰ β‰ˆ 1 billion (GB)
  • Availability: 99.9% β‰ˆ 8.8 h downtime/year

Worked example

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.
How to implement it
Library / toolPackageUse it for
BenchmarkDotNetBenchmarkDotNetMeasure the real cost of one operation (time and allocations) to replace a guess
NBomberNBomber commercial licenceLoad tests written in C#. Free for personal use only; an organisation needs a paid licence
k6 / Azure Load TestingNot .NET (JavaScript / JMeter / Locust)Free or managed alternatives to drive req/s at your API and check the estimate
dotnet-counters.NET global toolWatch 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

PrimitiveLets inUse for
Mutex / lock1 threadProtect a small critical section
SemaphoreN at onceCap concurrency (e.g. 10 DB calls)
Reader-writer lockMany readers or 1 writerRead-mostly data, like a cache
Atomic / CASNo lockCounters, 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 conditionMeansBreak it with
Mutual exclusionOnly one holderImmutable data, lock-free ops
Hold and waitHold one, wait for moreTake all locks at once, or smaller lock scope
No preemptionLocks can't be taken awayTimeouts, release and retry
Circular waitA waits B waits AGlobal 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

SituationChooseAvoid
One counter or flagAtomic / CASA full lock
Small in-memory sectionlockHolding it during I/O
Async code, or cap of NSemaphorelock around await
DB row, rare conflictsOptimistic versionLong DB locks
DB row, hot contentionPessimistic, short txRetry storms
Several app instancesDB constraint or distributed lockIn-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.
How to implement it
Library / APIPackageUse it for
Lock + lockbuilt-in System.Threading (.NET 9+)Mutual exclusion; TryEnter(timeout) to avoid waiting forever
Interlockedbuilt-in System.ThreadingIncrement, Add, CompareExchange (CAS)
SemaphoreSlimbuilt-in System.ThreadingAsync mutual exclusion (WaitAsync) or a cap of N
ReaderWriterLockSlimbuilt-in System.ThreadingMany readers, one writer; sync code only (thread-affine)
ConcurrentDictionarybuilt-in System.Collections.ConcurrentThread-safe map; GetOrAdd, AddOrUpdate
ImmutableArray, FrozenDictionarybuilt-in System.Collections.Immutable, System.Collections.FrozenShare read-only data with no locks; swap the whole reference to update
EF Core concurrency tokenMicrosoft.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?

#Concurrency #DotNet #SoftwareEngineering #SystemDesign

1.7 Concurrency models: threads, event loop, actors, channels Added

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.

Visual: five ways to share work

Compare the models

ModelUse whenFailure modeExamples
Shared memory + locksSmall hot state in one processRaces, deadlocks, contentionJava, C#, C++ threads
Event loopMany I/O waits, little CPUOne CPU-heavy or blocking call freezes everyoneNode.js, Python asyncio
ActorsMany independent entities with stateMailbox overflow, A→B→A call deadlock, hot actorErlang, Akka.NET, Orleans
CSP / channelsPipelines, producer/consumerBlocked sends, leaked consumers, unbounded buffersGo, .NET Channels
Data parallelismCPU work on big dataOverhead on small jobs, races in the loop bodyParallel.For, PLINQ, SIMD, GPU

Key ideas

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.
How to implement it
ModelPackageUse it for
Tasks / asyncbuilt-in System.Threading.TasksAsync I/O on the thread pool; Task.WhenAll to fan out
Channels (CSP)built-in System.Threading.ChannelsBounded producer/consumer queues between tasks
TPL DataflowSystem.Threading.Tasks.Dataflow 10.0 (NuGet, MIT)Linked blocks, each with its own buffer and parallelism
Virtual actorsMicrosoft.Orleans.Server, .Client, .Sdk 10.xGrains across a cluster; single-threaded per grain by default
Classic actorsAkka 1.5.x (1.6 preview)Actors, supervision, clustering, streams (Apache 2.0)
Data parallelismbuilt-in Parallel, PLINQ, System.Numerics.Vector<T>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?

#Concurrency #DotNet #SystemDesign #SoftwareArchitecture

1.8 OS essentials for engineers Added

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

ConceptWhat it meansInterview angle
User vs kernel spaceApps run unprivileged; a syscall jumps into the kernel and backCrossings cost: batch I/O
Virtual memoryPrivate address space per process, mapped to RAM in pages (often 4 KB)Reserved is not used
OOM killContainer hits its memory limit; kernel sends SIGKILL: exit 137, OOMKilledNo exception, no log line
File descriptorsEvery open file and socket is an fd, capped per process (ulimit -n)"Too many open files"
Port exhaustionEach outbound connection uses a local ephemeral port; closed ones linger in TIME_WAITSee 23.7 HttpClient
Context switchSwap one thread's registers for another's: microseconds, plus cold caches10k 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

ModelThread while waitingWhere you see it
BlockingAsleep, heldThread-per-request servers, scripts
Non-blockingFree, but must keep asking (EAGAIN)Building block, rarely used alone
Multiplexing (readiness)Free; one waiter for many socketsepoll (Linux), kqueue (BSD, macOS)
CompletionFree; kernel does the I/OIOCP (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.
How to implement it
Library / toolPackageUse it for
Sockets, SocketsHttpHandler, Kestrelbuilt-in System.Net.SocketsAsync socket calls sit on epoll (Linux), kqueue (macOS) and IOCP (Windows); completions resume on the thread pool
Container-aware GCbuilt-in runtime configWith a container memory limit, the GC heap defaults to 75% of it. Tune with DOTNET_GCHeapHardLimitPercent
dotnet-countersdotnet-counters (.NET global tool)Live GC heap, working set, thread-pool queue, HTTP connections
dotnet-tracedotnet-trace (.NET global tool)CPU sampling and runtime events; open in Visual Studio or PerfView
dotnet-dumpdotnet-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?

#SoftwareEngineering #Linux #dotnet #Kubernetes #Backend

1.9 Memory and storage hierarchy Added

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

Write
β†’
WAL append+ memtable in RAM
β†’
Flushsorted file (SSTable)
β†’
Compactionmerge in background

Trade-offs

ChoiceWins atPays with
B-tree (SQL Server, PostgreSQL)Reads, range scansRandom in-place writes
LSM treeHigh write ratesReads check several files; compaction I/O
Row store (OLTP)Fetch or update one recordScanning a few columns of many rows
Columnar (Parquet, columnstore)Analytics scans, compressionSingle-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.
How to implement it
Library / APIPackageUse it for
Span<T> / Memory<T>built-in SystemSlice contiguous memory without copies (see 23.9)
ArrayPool<T>.Sharedbuilt-in System.BuffersReuse I/O buffers instead of allocating new ones
Struct arrays, [StructLayout]built-in System.Runtime.InteropServicesAn array of structs is contiguous; an array of classes is an array of pointers
FileStream + FileStreamOptionsbuilt-in System.IOBufferSize, FileOptions.SequentialScan, FileOptions.WriteThrough; Flush(true) asks the OS to flush to disk
Parquet.NetParquet.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.

The structures, where they live, what they cost

StructureUsed inKey costTrade-off
Hash table
key β†’ bucket
Caches, Redis keys, hash joinsO(1) avg get/putNo order, no range queries; bad hash = slow
B+tree
wide sorted tree, linked leaves
Relational indexes (SQL Server, PostgreSQL, InnoDB)O(log n) seek, 3-4 levels; fast range scanRandom writes, page splits
LSM tree
memtable + SSTables
Cassandra, RocksDB, write-heavy storesVery fast writes (append)Reads touch many files; compaction I/O
Skip list
linked list with express lanes
Redis sorted sets, leaderboardsO(log n) insert, rank, rangeExtra pointers; probabilistic balance
Heap (priority queue)
min/max on top
Schedulers, top-K, timers, retriesPeek O(1), push/pop O(log n)Search is O(n); no sorted scan
Trie
tree of characters
Autocomplete, IP routing (longest prefix)O(key length)Memory heavy; use a radix trie
Inverted index
term β†’ list of doc IDs
Search engines (Lucene, Elasticsearch)Fast term lookup, merge posting listsSlow, costly writes; refresh lag
Ring buffer
fixed array, wrap-around
Log buffers, high-throughput queuesO(1) push/pop, no allocationFixed size: block or overwrite when full
Geohash / quadtree
map cells
Proximity: nearby drivers, ATMsPrefix or tree lookup, O(log n)Cell edges (check neighbours); density skew

B+tree vs LSM tree: the classic trade-off

B+treeLSM tree
Optimised forReads, rangesWrites, ingest
WritesUpdate in place (random I/O)Append, flush, compact
ReadsOne path down the treeMemtable + several SSTables
Pick forOLTP, payments, ledgersTelemetry, events, time series

Interview angle: "which would you pick for X?"

Deeper: indexing, search, Bloom filter & HyperLogLog, Big-O & .NET collections.

  • Hash = O(1) but no ranges. Need order β†’ tree or skip list.
  • B+tree reads, LSM writes. Bloom filters keep LSM reads cheap.
  • Top-K = min-heap of size K, O(n log K).
  • Geohash search must check neighbouring cells.
  • A bounded buffer gives back-pressure: block or drop.
How to implement it
Library / APIPackageUse it for
Dictionary / HashSetbuilt-in System.Collections.GenericHash tables: O(1) average lookups, "seen before?"
SortedDictionary / SortedSetbuilt-in System.Collections.GenericSelf-balancing (red-black) tree: ordered keys, min/max, ranges via SortedSet.GetViewBetween
PriorityQueue<TElement,TPriority>built-in System.Collections.GenericArray-backed quaternary min-heap: top-K, schedulers. Equal priorities are not FIFO
FrozenDictionary / FrozenSetbuilt-in System.Collections.FrozenRead-only hash lookups built once at startup, fastest reads
Bounded Channel<T>built-in System.Threading.ChannelsBounded buffer with back-pressure; FullMode: Wait, DropOldest, DropNewest, DropWrite
StackExchange.RedisStackExchange.RedisSorted sets (SortedSetAddAsync, rank, range) and GEO commands
Lucene.NETLucene.Net previewIn-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 πŸ§₯

⚑ Redis cache β†’ hash table. Leaderboard β†’ skip list.

πŸ—„οΈ SQL index β†’ B+tree. Cassandra / RocksDB β†’ LSM tree.

πŸ”Ž Elasticsearch β†’ inverted index. Typeahead β†’ trie.

πŸ“ "ATMs near me" β†’ geohash cells. Job scheduler β†’ heap.

In a design interview, don't start with the product. Start with the access pattern: point, range, prefix, top-K or nearby. The structure follows.

Which one surprised you most in production?

#SystemDesign #DataStructures #SoftwareArchitecture #Backend

1.11 What happens when you type a URL Added

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

Trade-offs you tune at each layer

LayerKnobTrade-off
DNSTTLLong = fewer lookups, slow failover
TLSCert lifetimeShort = safer, must automate renewal
CDNCache TTLHigher hit ratio vs stale content
Load balancerSticky sessionsSimple state vs uneven load
ResponseCompressionFewer 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.
  • TLS 1.3 = 1 RTT; QUIC folds TCP + TLS into 1 RTT.
  • Edge order: CDN/WAF β†’ LB β†’ gateway β†’ app β†’ cache β†’ DB.
  • Render: DOM + CSSOM β†’ render tree β†’ layout β†’ paint.
How to implement it
Library / APIPackageUse it for
Kestrelbuilt-in ASP.NET CoreThe web server. HTTP/3 is off by default: set Http1AndHttp2AndHttp3 + HTTPS; it adds alt-svc for you
HttpClient / SocketsHttpHandlerbuilt-in System.Net.HttpOutbound calls. HTTP/3 is opt-in: DefaultRequestVersion = 3.0 + RequestVersionOrHigher; falls back if QUIC is missing
HSTS + HTTPS redirectbuilt-in UseHsts, UseHttpsRedirectionSend the Strict-Transport-Security header; redirect http to https
Response compressionbuilt-in AddResponseCompressionBrotli + Gzip. HTTPS is off by default (CRIME/BREACH); prefer CDN or proxy compression
Output cachingbuilt-in AddOutputCache, CacheOutput()Cache whole responses on the server, with tags for eviction
Output cache in RedisMicrosoft.AspNetCore.OutputCaching.StackExchangeRedisShare the output cache across instances
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(k => k.ListenAnyIP(5001, o =>
{
    o.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;   // needs MsQuic
    o.UseHttps();
}));
builder.Services.AddResponseCompression();
builder.Services.AddOutputCache();

var app = builder.Build();
app.UseHsts();
app.UseHttpsRedirection();
app.UseResponseCompression();
app.UseOutputCache();

app.MapGet("/rates", () => Rates.Today())
   .CacheOutput(p => p.Expire(TimeSpan.FromSeconds(30)));
app.Run();

HTTP/3 in .NET needs MsQuic: built in on Windows 11 / Server 2022+, install libmsquic on Linux. Always keep HTTP/1.1 and HTTP/2 enabled as a fallback.

"What happens when you type a URL?" is not a trivia question. It's a map of your whole stack πŸ—ΊοΈ

🌐 DNS: resolver β†’ root β†’ TLD β†’ authoritative, cached for the TTL.

🀝 TCP + TLS 1.3 = 2 round trips. HTTP/3 over QUIC = 1.

πŸ›‘οΈ CDN + WAF β†’ load balancer β†’ API gateway β†’ app β†’ cache β†’ DB.

🎨 Back in the browser: DOM + CSSOM β†’ layout β†’ paint.

The best answer goes breadth first, then asks the interviewer which layer to go deep on. That one question shows you know the whole system.

Which layer do you always get asked about?

#SystemDesign #Networking #WebPerformance #InterviewPrep

1.12 Building blocks of any system Added

Almost every web-scale design is the same dozen blocks: start with client β†’ app β†’ database and add a block only when a requirement demands it.

Block β†’ job β†’ when you add it

BlockIts jobAdd it whenDeep dive
DNSName β†’ IP, can steer by regionAlways; GeoDNS for multi-region2.4
CDNServe static/cacheable content at the edgeGlobal users, heavy assets5.6
Load balancerSpread traffic, drop unhealthy nodesMore than one app instance3.3
Gateway / reverse proxyAuth, rate limit, routing, TLSMany services or public APIs9.9
Stateless appBusiness logic, no local sessionTo scale horizontally3.2
CacheFast reads of hot dataRead-heavy, latency target5.2
DatabaseSource of truth; replicas for readsAlways; replicas/shards at scale6.7
Queue / streamDecouple, buffer spikesSlow or bursty work, fan-out10.3
WorkersProcess jobs off the request pathEmails, reports, video, retries10.9
Object storageCheap, durable files/blobsUploads, media, backups6.8
Search indexFull-text and faceted queries"Search" or "filter" requirement6.5
ObservabilityLogs, metrics, traces, alertsAlways, from day one15.9

Gaps worth knowing

Client-server vs peer-to-peer

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 proxyReverse proxy
Sits in front ofClientsServers
HidesWho the client isWhich server answers
Used forEgress control, URL filtering, cachingTLS offload, routing, LB, caching, WAF
ExamplesCorporate web proxy, SquidNGINX, 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.
  • Edge order: DNS β†’ CDN β†’ LB β†’ gateway β†’ stateless app.
  • Data: cache for hot reads, replicas for read scale, queue for slow work.
  • Reverse proxy protects servers; forward proxy controls clients.
  • GeoDNS steers by answer (TTL-bound); anycast steers by routing (same IP).
How to implement it
Library / APIPackageUse it for
YARPYarp.ReverseProxy (2.3.0)Reverse proxy / lightweight gateway: routes, clusters, load balancing, health checks, transforms
Rate limitingbuilt-in AddRateLimiterPer-route limits; YARP routes can reference a RateLimiterPolicy
HybridCacheMicrosoft.Extensions.Caching.HybridIn-memory + distributed cache with stampede protection
OpenTelemetryOpenTelemetry.Extensions.HostingLogs, metrics and traces for the observability block
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
       .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();

// appsettings.json
"ReverseProxy": {
  "Routes": {
    "accounts": {
      "ClusterId": "accounts",
      "Match": { "Path": "/accounts/{**catch-all}" }
    }
  },
  "Clusters": {
    "accounts": {
      "LoadBalancingPolicy": "RoundRobin",
      "HealthCheck": { "Active": { "Enabled": true, "Path": "/health" } },
      "Destinations": {
        "a1": { "Address": "https://accounts-1:8443/" },
        "a2": { "Address": "https://accounts-2:8443/" }
      }
    }
  }
}

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.

⚑ Cache + read replicas: survive read-heavy traffic.

πŸ“¬ Queue + workers: move slow work off the request path.

πŸ”­ Logs, metrics, traces: from day one, not after the first outage.

Start with client β†’ app β†’ database. Add a box only when a requirement asks for it, and say which one. That's what interviewers listen for.

Which box do you see added too early most often?

#SystemDesign #SoftwareArchitecture #InterviewPrep #DotNet

1.13 Requirements and quality attributes Added

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.

Quantify every "-ility"

AttributeHow to state itExample target
LatencyPercentiles, not averagesp95 < 200 ms, p99 < 500 ms
ThroughputAverage and peak req/s or TPS2k req/s avg, 10k peak
AvailabilityNines over a window (SLO)99.95% monthly (3.7, 3.10)
DurabilityChance of losing stored dataZero loss of committed payments
RPO / RTOMax data loss / max downtime after disasterRPO 0, RTO 15 min (3.13)
ConsistencyStrong or eventual, per data typeBalance strong; feed eventual (3.12)
ScalabilityGrowth target and headroom10Γ— users in 2 years
Security / complianceControls and regulations namedMFA, encryption, data residency
CostBudget per month or per transaction< $0.001 per request
MaintainabilityChange speed, deploy safetyDaily deploys, MTTR < 1 h

Read/write ratio and access patterns

🎯 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.
How to implement it
Library / APIPackageUse it for
Meter + Histogram<T>built-in System.Diagnostics.MetricsRecord durations; the backend computes p95/p99 from buckets
http.server.request.durationbuilt-in ASP.NET Core meterRequest latency histogram, emitted by the framework
OpenTelemetry hostingOpenTelemetry.Extensions.HostingWire up metrics and traces
OTLP exporterOpenTelemetry.Exporter.OpenTelemetryProtocolSend to any OTLP backend (Prometheus, Grafana, Azure Monitor)
k6Grafana k6, open source CLILoad test with thresholds, e.g. p(95)<200
NBomberNBomber commercial licenceLoad 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?

#SystemDesign #NonFunctionalRequirements #SoftwareArchitecture #SRE #InterviewPrep

1.14 Scaling and partitioning fundamentals Added

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)

MoveIn one lineDeep
Vertical vs horizontalBigger box (simple, has a ceiling) vs more boxes (no ceiling, needs stateless apps)3.1
Stateless servicesKeep session and state in Redis or the DB, so any instance serves any request3.2
Load balancingSpread requests; health checks take bad instances out3.3
CachingServe hot reads from memory; plan invalidation5.1
CDNStatic and cacheable content at the edge, near users5.6
Async via queuesAccept now, process later; absorbs spikes10.3
Read replicasCopies take reads; replication lag means stale reads4.2

Replication vs partitioning: different problems

Replication = copies

  • Same data on N nodes.
  • Fixes read scale and availability.
  • Does not fix write volume or data size.

Partitioning = split

  • Each node owns a slice of the rows.
  • Fixes write throughput and data size.
  • Costs cross-shard queries and transactions.

Real systems do both: each shard is a small replicated cluster.

Sharding strategies

StrategyWinsPays
Range (date, A-H)Range scans, simpleHotspots on the newest range
Hash (hash(key))Even spreadRanges fan out to all shards
Directory (lookup table)Move any key or tenant freelyExtra hop; the directory must be HA and cached
Geo / tenantData residency, isolationUneven 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

Interview angle: "How would you scale this to 10x?"

Also: "Replicas or shards?", "Pick a shard key", "One customer is hot. Now what?"

  1. Measure: which resource saturates first (CPU, DB reads, DB writes, storage)?
  2. Cache hot reads; push static content to a CDN.
  3. Read replicas for read-heavy load; route read-your-own-writes to the primary.
  4. Async: move slow or spiky work to a queue.
  5. 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.
How to implement it
Library / APIPackageUse it for
Azure Cosmos DB SDKMicrosoft.Azure.Cosmos 3.63Partition keys (up to 3 levels with hierarchical keys); a logical partition holds up to 20 GB and 10,000 RU/s
EF Core SQL ServerMicrosoft.EntityFrameworkCore.SqlServer 10.0One DbContext per shard database
DbContextOptionsBuilderbuilt-in EF CoreBuild 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.

"Add read replicas" doesn't fix a write bottleneck. Here's why πŸ‘‡

πŸ“– Replication = copies. It scales reads and availability, not writes or data size

πŸ”ͺ Partitioning = split. It scales writes and size, but every query now needs the shard key

πŸ“… Range shards make scans easy but put every new write on the last shard

🎲 Hash shards spread load evenly but turn range queries into scatter-gather

πŸ”₯ One hot key beats any strategy: salt it, split it or cache it

My 10x order: measure β†’ cache β†’ replicas β†’ queues β†’ shard. What's the first thing you'd measure?

#SystemDesign #Scalability #Databases #Architecture #dotnet

1.15 Data fundamentals Added

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

ConceptIn one lineDeep
SQL vs NoSQLSchema + joins + transactions vs models built for one access pattern at scale6.1
ACID vs BASEAll-or-nothing, isolated, durable vs basically available, eventually consistent6.2
IndexingFaster reads, slower writes; match the index to the query6.3
CAP / PACELCIn a partition: consistency or availability. Else: latency or consistency3.11, 3.12
ETL / ELT, CDCMove data to analytics; CDC streams changes from the DB log6.11, 6.12

Database types: when to use each

TypeBest forExamples
RelationalTransactions, ledgers, ad-hoc joinsSQL Server, PostgreSQL
Key-valueLookup by key: sessions, cacheRedis, DynamoDB
DocumentJSON aggregates read whole: profiles, catalogueMongoDB, Cosmos DB
Wide-columnHuge write rates, known queriesCassandra
GraphMany-hop relationships: fraud ringsNeo4j
Time-seriesMetrics by time, downsamplingInfluxDB, TimescaleDB
SearchFull-text, relevance, facetsElasticsearch, OpenSearch
VectorSimilarity search for AI (17.7)pgvector, Qdrant

Model and workload

Normalise (write model)

Each fact stored once. Safe updates, no anomalies. Reads need joins.

Denormalise (read model)

Copy data into the shape the screen needs: one read, no joins. Pays with write fan-out and staleness. See CQRS.

OLTPOLAP
WorkMany small reads and writesFew big scans and aggregates
StorageRow store, normalisedColumnar, denormalised (star schema)
ExamplePayments DBWarehouse, lakehouse (6.13)

Why rows vs columns: 1.9. Feed OLAP with ETL or CDC; never run big reports on the OLTP primary.

Batch vs stream processing

BatchStream
DataBounded (a file, a day)Unbounded (never ends)
Trade-offComplete, minutes to hours lateLow latency, must handle late and out-of-order events
Use forStatements, reconciliation, ML trainingFraud alerts, live dashboards

Interview angle: "Which database would you choose?"

Also: "SQL or NoSQL?", "Batch or real time?", "Why not run reports on the main DB?"

  1. Access pattern first: key lookups, rich queries, joins, scans, text or similarity?
  2. Consistency: money moves need ACID; feeds can be eventual.
  3. Scale and shape: read/write ratio, data size, growth, hot keys.
  4. Default relational; add a specialised store only for a pattern it can't serve.
  5. Name the cost: ops, skills, sync (CDC), and keep one source of truth.
  • Access pattern decides the store, not fashion.
  • Normalise to write, denormalise to read.
  • OLTP = rows, small transactions. OLAP = columns, big scans.
  • Eventual = may be stale for a while; strong = slower but never stale.
  • Batch = complete but late; stream = fast but needs windows and watermarks.
How to implement it
Store / needPackageNotes
SQL Server / Azure SQLMicrosoft.EntityFrameworkCore.SqlServer 10.0EF Core for relational OLTP
PostgreSQLNpgsql.EntityFrameworkCore.PostgreSQL 10.0 (driver: Npgsql)EF Core provider; pgvector has its own plugin
Cosmos DBMicrosoft.Azure.Cosmos 3.63Document store; pick the partition key from the main query
MongoDBMongoDB.Driver 3.12Official driver
In-process streamsbuilt-in System.Threading.ChannelsBounded producer/consumer with back-pressure
KafkaConfluent.Kafka 2.16Consume and produce durable event streams
// Batch: bounded data, exact answer (EF Core, runs nightly)
var daily = await db.Payments
    .Where(p => p.At >= day && p.At < day.AddDays(1))
    .GroupBy(p => p.AccountId)
    .Select(g => new { AccountId = g.Key, Total = g.Sum(p => p.Amount) })
    .ToListAsync(ct);

// Stream: unbounded data, 1-minute tumbling windows by event time
var channel = Channel.CreateBounded<Payment>(new BoundedChannelOptions(10_000)
    { FullMode = BoundedChannelFullMode.Wait });          // back-pressure

var windows = new Dictionary<DateTime, decimal>();
await foreach (var p in channel.Reader.ReadAllAsync(ct))
{
    var start = new DateTime(p.At.Ticks - p.At.Ticks % TimeSpan.TicksPerMinute, DateTimeKind.Utc);
    windows[start] = windows.GetValueOrDefault(start) + p.Amount;
    // watermark: emit and remove windows older than (latest event time - 2 min)
}

Stick to the in-process Channel for a single service. For durable, replayable, multi-consumer streams use Kafka or Azure Event Hubs.

"Which database would you use?" is the wrong first question. Ask "how will the data be read?" πŸ‘‡

πŸ”‘ Lookups by key β†’ key-value. Whole JSON aggregates β†’ document

🧾 Money and joins β†’ relational with ACID

πŸ•ΈοΈ Many-hop relationships β†’ graph. Similarity for AI β†’ vector

πŸ“Š Transactions on rows (OLTP), analytics on columns (OLAP)

⏱️ Batch is complete but late. Streams are fast but need windows and watermarks

Default to relational, then add a store only when a pattern demands it. What did you add last, and why?

#SystemDesign #Databases #DataEngineering #Architecture #dotnet

1.16 Communication and data-flow patterns Added

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).

Primer: the basics

ConceptIn one lineDeep dive
Sync vs asyncWait for the answer vs move on; queues decouple1.3, 10.3
ProtocolsHTTP, gRPC, AMQP, MQTT, WebSocket2.5, 2.6
Pub/sub + guaranteesOne event, many subscribers; at-least-once is the norm10.4
BackpressureSlow consumer throttles the producer10.5
REST / gRPC / GraphQLPublic resources; fast internal calls; client-shaped queries8.1

Push vs pull

Push: the sender starts

  • Low latency.
  • Sender must handle slow or offline receivers.
  • e.g. webhooks, mobile push, WebSocket.

Pull: the receiver starts

  • Receiver controls the rate (natural backpressure).
  • Latency = poll interval; empty polls waste calls.
  • e.g. Prometheus scraping /metrics, Kafka consumers.

Hybrid: push a thin "changed" signal; the client pulls the data.

How clients get updates

OptionHowBest forWatch out
Short pollingAsk every N sRare changesWasted calls, N s lag
Long pollingServer holds the request until dataFallback when WS blockedOne open request each
SSEOne HTTP response streams eventsFeeds, AI token streamsOne-way, text only
WebSocketFull-duplex upgraded connectionChat, trading, collaborationStateful; sticky or backplane
WebhookServer POSTs to your URLServer-to-server eventsRetries, signing, public URL

Fan-out on write vs fan-out on read

Write: fast reads, costly writes. Read: cheap writes, slow reads. Real feeds are hybrid (21.5).

Pagination: offset vs cursor

Offset (?page=500)Cursor / keyset (?after=…)
HowSkip N rows, take MRows after the last key seen, take M
Deep pagesSlow: DB reads and drops N rowsFast: index seek every time
Inserts while pagingDuplicates or skipped rowsStable
Jump to page 37YesNo, next/previous only
Use forSmall admin gridsFeeds, public APIs, exports

Head-of-line (HOL) blocking

One slow item at the front holds up everything behind it (2.3).

WhereCauseFix
HTTP/1.1One response at a time per connection~6 connections per host; upgrade
HTTP/2One lost TCP packet stalls all streamsHTTP/3
HTTP/3 (QUIC)Independent streams over UDPLoss stalls only its own stream
QueuesPoison message blocks a partitionRetry limit + DLQ, more partitions

🎀 "How will clients get updates?"

1. Clarify: freshness, direction, client count, browser or server.
2. Pick: polling (rare changes), SSE (server to client), WebSocket (two-way), webhooks (partners).
3. Scale: connections, backplane, fan-out strategy.
4. Failure: reconnect with backoff, resume from the last event ID.

⚠️ Also asked

News feed (fan-out). Paging 10M rows (keyset). HTTP/2 on lossy mobile (TCP HOL).

  • Push = low latency, sender owns delivery; pull = receiver owns the rate.
  • Default ladder: polling β†’ SSE β†’ WebSocket; webhooks between servers.
  • Feeds: hybrid fan-out, pull for celebrities.
  • Keyset pagination costs the same on page 1 and page 10,000.
  • HTTP/2 fixes HOL at the HTTP layer; QUIC fixes it at transport.
How to implement it
NeedLibrary / APIUse it for
Real-time pushbuilt-in ASP.NET Core SignalR: AddSignalR, MapHub, IHubContext<T>Negotiates WebSockets, then SSE, then long polling
Scale out SignalRMicrosoft.Azure.SignalR or Microsoft.AspNetCore.SignalR.StackExchangeRedisManaged service or Redis backplane so any instance reaches any client
One-way streambuilt-in TypedResults.ServerSentEvents (new in .NET 10)SSE without SignalR
Producer/consumerbuilt-in System.Threading.Channels: Channel.CreateBoundedIn-process queue; a full bounded channel makes writers wait (backpressure)
Keyset pagingMicrosoft.EntityFrameworkCore 10Where 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.

Primer: the basics

ConceptIn one lineDeep dive
Fault toleranceKeep serving when a part fails3.5
Resilience patternsTimeout, retry, breaker, bulkhead, fallback3.6
Availability99.9% β‰ˆ 8.8 h down a year; 99.99% β‰ˆ 53 min3.7
DRRPO = data you can lose; RTO = time to recover3.13
IdempotencySame request twice = same effect8.7
Rate limitingCap each client's requests8.6

Redundancy: find the SPOFs

Failover modes

ModeStandbyRTOCost / risk
ColdBuilt from IaC + backupHoursCheapest; untested restores
WarmRunning small, data replicatingMinutesMust scale up under stress
Hot (active-passive)Full size, in sync, idleSeconds-minutesPaying for idle
Active-activeAll sites serve trafficNear zeroWrite conflicts

Detect: heartbeats and health checks

Timeouts, retries, backoff, jitter

Overload: shed load, degrade gracefully

Load shedding

  • Reject excess early and cheaply (503 + Retry-After) so admitted requests meet the SLO.
  • Signals: in-flight count, queue length or wait.
  • Drop requests already past their deadline.

Graceful degradation

  • Drop first: batch, analytics, recommendations. Keep: login, balances, payments.
  • Kill-switch feature flags (15.6).
  • Stale cache, static fallback, read-only.

Split brain and fencing tokens

Checksums and end-to-end integrity

🎀 "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.
  • Split brain β†’ quorum + fencing tokens.
How to implement it
NeedLibrary / APIUse it for
HttpClient resilienceMicrosoft.Extensions.Http.Resilience (10.x): AddStandardResilienceHandler()Concurrency limiter, total timeout, retry (exponential + jitter), circuit breaker, per-attempt timeout
Non-HTTP callsPolly.Core (Polly v8, 8.x): ResiliencePipelineBuilderThe same strategies around DB or SDK calls
Load sheddingbuilt-in AddRateLimiter + AddConcurrencyLimiterPermitLimit in flight, QueueLimit waiting, the rest rejected (503 by default)
Health checksbuilt-in AddHealthChecks / MapHealthChecks; community AspNetCore.HealthChecks.*Separate live and ready endpoints via tags
Kill switchesMicrosoft.FeatureManagement.AspNetCoreTurn off non-critical features under load
ChecksumsSystem.IO.Hashing (Crc32, Crc64); built-in SHA256Verify payloads and files end to end
builder.Services.AddHttpClient<LedgerClient>(c => c.BaseAddress = new("https://ledger.internal"))
    .AddStandardResilienceHandler(o =>
    {
        o.AttemptTimeout.Timeout = TimeSpan.FromSeconds(2);
        o.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(6);
        o.Retry.MaxRetryAttempts = 2;            // exponential backoff + jitter by default
    });

builder.Services.AddRateLimiter(o =>
{
    o.RejectionStatusCode = StatusCodes.Status503ServiceUnavailable;
    o.AddConcurrencyLimiter("shed", c => { c.PermitLimit = 200; c.QueueLimit = 50; });
});

builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"])
    .AddCheck<LedgerReadyCheck>("ledger", tags: ["ready"]);

var app = builder.Build();
app.UseRateLimiter();
app.MapHealthChecks("/health/live",  new() { Predicate = r => r.Tags.Contains("live") });
app.MapHealthChecks("/health/ready", new() { Predicate = r => r.Tags.Contains("ready") });
app.MapPost("/payments", PayAsync).RequireRateLimiting("shed");

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

PrimitiveIn one lineDeep dive
ReplicationCopies on several nodes: leader-follower, multi-leader or leaderless4.2
QuorumW + R > N, so a read overlaps the latest write4.3
Consensus (Raft)A majority agrees on one ordered log; survives minority loss4.4
Lock + fencingLease-based lock; a rising token rejects stale holders4.5
Two-phase commitPrepare, then commit; blocks if the coordinator dies4.6
ClocksWall clocks drift; Lamport / vector clocks order events4.7
Consistent hashingHash ring: adding a node moves only ~1/N of keys3.4

Gossip protocol (epidemic spread)

Merkle trees and anti-entropy repair

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?"

  1. Split the question: membership, failure detection, agreement, repair.
  2. Membership: registry for services; gossip for large peer clusters.
  3. Failure: heartbeats + timeout (or phi accrual); say "suspect, not dead".
  4. Agreement: need one leader or strict order? Use Raft / etcd plus fencing. Else quorum + anti-entropy.
  5. 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.
How to implement it
Library / APIPackageUse it for
Orleans membershipMicrosoft.Orleans.Server 10.x + a clustering provider, e.g. Microsoft.Orleans.Clustering.AzureStorageSilos probe each other and vote dead silos out via a membership table. See 23.10
DistributedLockDistributedLock 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
BlobLeaseClientAzure.Storage.Blobs 12.xLease-based leader (15–60 s, renewable). Example in 4.4
Service discoveryMicrosoft.Extensions.ServiceDiscovery 10.xClient-side: https+http://accounts resolves from config, Aspire or Kubernetes DNS
SHA256.HashDatabuilt-in System.Security.CryptographyMerkle 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.

Which of these have you debugged in production?

#DistributedSystems #SystemDesign #Architecture #DotNet

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.

Visual: one request, every control and signal

Security baseline (primer)

ControlSay thisDeep dive
AuthN vs AuthZOIDC / OAuth tokens prove identity; policies decide access14.1
Least privilegeManaged identities, scoped roles, no shared admin14.2
In transitTLS everywhere; mTLS service-to-service14.6
At restEncrypted storage; customer-managed keys if required14.4
SecretsVault + rotation, never in code or config14.7
Input / OWASPValidate at the edge; object-level authZ checks14.9
Zero trustVerify every call; no trusted network14.8
ClassificationTag PII / confidential; mask in logs7.2
Audit logAppend-only record of who changed what; kept apart from debug logs—

Observability baseline (primer)

Three signals

  • Logs: structured events (15.10).
  • Metrics: cheap numbers over time (15.11).
  • Traces: one request across services (15.9).

SLIs, SLOs, alerts

  • SLI: measured, e.g. % of 2xx under 300 ms.
  • SLO: target, e.g. 99.9% over 30 days (15.14).
  • Alert on symptoms users feel, not CPU (15.12).

Correlation ID

  • W3C traceparent header flows through every hop and message.
  • Every log line carries the trace ID: one search finds the whole request.

Say this in every design (60 seconds)

Interview angle: weave it in, don't derail

  1. Don't open with it. Get requirements and the core design first.
  2. One sentence per hop as you draw: "the gateway validates the token", "this queue carries the trace context".
  3. Give it a 60-second slot near the end: run the 10-point list.
  4. Go deep only where the domain needs it: payments = audit and encryption; public API = OWASP and rate limits.
  5. Offer, don't lecture: "Happy to go deeper on the threat model."
  • AuthN = who you are; AuthZ = what you may do. Check both, at the object level.
  • Encrypt in transit and at rest; secrets in a vault, accessed by managed identity.
  • Three signals: logs, metrics, traces, joined by a trace / correlation ID.
  • Alert on SLO burn (errors, latency), not on CPU.
  • Keep an audit log separate from debug logs, and never log secrets or raw PII.
How to implement it
Library / APIPackageUse it for
Authentication / authorisationbuilt-in ASP.NET Core + Microsoft.AspNetCore.Authentication.JwtBearer 10.0Validate bearer tokens; policies with RequireAuthorization. See 23.15
Microsoft Identity WebMicrosoft.Identity.Web 4.xEntra ID sign-in and token validation helpers
Key Vault configurationAzure.Extensions.AspNetCore.Configuration.Secrets 1.5 + Azure.Identity 1.xLoad secrets into IConfiguration using a managed identity
OpenTelemetry hostOpenTelemetry.Extensions.Hosting 1.xWire traces, metrics and logs into DI
InstrumentationOpenTelemetry.Instrumentation.AspNetCore, .Http, .SqlClient 1.xAutomatic spans and metrics for incoming requests, HttpClient and SQL
ExportersOpenTelemetry.Exporter.OpenTelemetryProtocol 1.x, or Azure.Monitor.OpenTelemetry.AspNetCore 1.xSend 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?

#SystemDesign #Security #Observability #SolutionArchitecture #DotNet

⚑ Quick-fire Q&A

Why are threads cheaper than processes?

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.

← Roadmap