Module 1 · Method and Building Blocks · Lesson 1 of 12
Running a .NET System Design Case: Requirements, Capacity Math and the Answer Shape
Learning outcome
You can run any case in this course in the same order: clarify requirements, write down assumptions, turn them into numbers (QPS, storage, cache, bandwidth), pick a .NET stack, and then spend the remaining time on trade-offs and failure paths. You can do the arithmetic out loud without inventing numbers, because every number comes from an assumption you said first.
Intuition
Interviewers do not grade a diagram. They grade whether your decisions follow from the requirements. Capacity math is the bridge: it tells you whether one SQL Server is enough, whether you need a cache, and which part of the system is the bottleneck. A rough number you can defend beats a precise number you cannot explain.
flowchart LR
R["Requirements<br/>functional + non-functional"] --> A["Assumptions<br/>said out loud"] --> N["Numbers<br/>QPS, storage, cache, egress"]
N --> S[".NET stack<br/>choices"] --> T["Trade-offs"] --> F["Failure paths"]Deep dive
- This course is the case-study layer. The building blocks (load balancers, caching, sharding, queues, distributed locks, background jobs) are taught in System Design Fundamentals for .NET and C# and System Design for .NET Developers. Here we apply them to seven concrete systems and go deep on the parts where .NET details change the answer.
- The answer shape (about 45 minutes):
- Requirements, 5 minutes. Functional: what the API does. Non-functional: latency target, availability, consistency, scale, multi-region, compliance.
- Assumptions and numbers, 5 minutes. Write them down where the interviewer can see them.
- API and data model, 10 minutes. Endpoints, idempotency keys, the main table and its key.
- High-level design, 10 minutes. The request path and the async path.
- Deep dive and failure paths, 15 minutes. What breaks first, and what happens when a dependency is down.
- Units that trip people up: a day is 86,400 seconds; a month of 30 days is about 2.6 million seconds. Storage vendors use decimal TB (10^12 bytes); the OS and many monitoring tools show TiB (2^40 bytes); the gap is about 9%. Network is in bits per second, storage in bytes.
- Average vs peak: size for peak, not average. A peak factor (2x to 5x) is an assumption you state, not a fact.
- Read:write ratio decides the architecture. With 100:1 reads, a cache and read replicas matter more than write throughput.
- .NET stack defaults to name, then justify: ASP.NET Core minimal APIs or controllers on Kestrel; SQL Server or PostgreSQL via EF Core (Dapper on hot paths); Redis via StackExchange.Redis or HybridCache for distributed caching; a broker (Azure Service Bus, RabbitMQ, Kafka) for async work;
BackgroundServiceor a scheduler for jobs; OpenTelemetry for traces and metrics. Each case lesson says when to deviate.
Code example: URL shortener capacity math with every assumption printed
// Back-of-the-envelope capacity math for a URL shortener, with every assumption printed.
const double SecondsPerDay = 86_400;
long newUrlsPerMonth = 100_000_000; // assumption: 100 M new short links per month
int readsPerWrite = 100; // assumption: 100 redirects per created link
double peakFactor = 3; // assumption: peak hour = 3x the daily average
int bytesPerRow = 500; // assumption: code + long URL (avg ~400 B) + owner + timestamps + index overhead
int years = 5;
double cacheShareOfDailyReads = 0.20; // assumption: 20% of links serve ~80% of reads; cache that 20% of a day's distinct links
double writesPerDay = newUrlsPerMonth / 30.0;
double writeQps = writesPerDay / SecondsPerDay;
double readQps = writeQps * readsPerWrite;
Console.WriteLine("Assumptions: 100 M writes/month, 100:1 read:write, peak 3x, 500 B/row, 5 years, cache 20% of daily links");
Console.WriteLine($"writes/day = {writesPerDay:N0}");
Console.WriteLine($"write QPS (avg/peak)= {writeQps:N0} / {writeQps * peakFactor:N0}");
Console.WriteLine($"read QPS (avg/peak)= {readQps:N0} / {readQps * peakFactor:N0}");
long rows = newUrlsPerMonth * 12 * years;
double storageBytes = (double)rows * bytesPerRow;
Console.WriteLine($"rows after {years} years = {rows:N0}");
Console.WriteLine($"storage = {storageBytes / 1e12:N1} TB (decimal), {storageBytes / Math.Pow(1024, 4):N2} TiB, before replication");
Console.WriteLine($"storage x3 replicas = {storageBytes * 3 / 1e12:N1} TB");
double cacheBytes = writesPerDay * cacheShareOfDailyReads * bytesPerRow;
Console.WriteLine($"cache for 20% of one day's links = {cacheBytes / 1e9:N2} GB");
// Key space: how long must a base62 code be?
foreach (var len in new[] { 6, 7, 8 }) Console.WriteLine($"base62 length {len}: {Math.Pow(62, len):E2} codes = {Math.Pow(62, len) / rows:N0}x the 5-year row count");
// Bandwidth: a 301/302 response is small
int responseBytes = 300; // assumption: headers of a redirect response
Console.WriteLine($"redirect egress at peak = {readQps * peakFactor * responseBytes * 8 / 1e6:N0} Mbit/s");Output (.NET 10)
Assumptions: 100 M writes/month, 100:1 read:write, peak 3x, 500 B/row, 5 years, cache 20% of daily links writes/day = 3,333,333 write QPS (avg/peak)= 39 / 116 read QPS (avg/peak)= 3,858 / 11,574 rows after 5 years = 6,000,000,000 storage = 3.0 TB (decimal), 2.73 TiB, before replication storage x3 replicas = 9.0 TB cache for 20% of one day's links = 0.33 GB base62 length 6: 5.68E+010 codes = 9x the 5-year row count base62 length 7: 3.52E+012 codes = 587x the 5-year row count base62 length 8: 2.18E+014 codes = 36,390x the 5-year row count redirect egress at peak = 28 Mbit/s
Code walkthrough
- 100 M links a month is 3,333,333 a day, which is 39 writes per second on average and 116 at a 3x peak. One SQL Server handles that easily, so writes are not the problem.
- At 100 reads per write, redirects are 3,858 per second on average and 11,574 at peak. This is the number that drives the design: cache the hot links and keep the redirect path free of joins.
- Five years is 6,000,000,000 rows. At an assumed 500 bytes per row that is 3.0 TB (2.73 TiB), or 9.0 TB with three replicas. This is where you mention partitioning (Lesson 2) or a key-value store.
- Caching 20% of one day's new links costs only 0.33 GB, so the cache fits in one Redis node. The 20% figure is an assumption about skew, not a measurement.
- Base62 length 6 gives 5.68E+010 codes, only 9x the 5-year row count. Length 7 gives 3.52E+012, which is 587x. Lesson 5 shows why random codes need far more headroom than "9x" suggests.
- Redirect egress at peak is 28 Mbit/s with 300-byte responses: bandwidth is not a concern for this system.
Failure modes
- Doing the arithmetic before stating assumptions, so the interviewer cannot follow or correct you.
- Sizing for average traffic, then being surprised at the daily peak or a marketing spike.
- Mixing TB and TiB or bits and bytes, which moves the answer by 9% or 8x.
- Picking the stack first ("we use Cosmos DB and Kafka") and then justifying it backwards.
- Spending 30 minutes on the happy path and never reaching failure paths, which is where senior candidates are separated.
Interview angle
Say it like this: "Let me write down assumptions: 100 M new links a month, 100 reads per write, 3x peak. That gives about 40 writes per second and about 11,600 redirects per second at peak, about 3 TB over five years before replication. So the redirect read path is the hot path. I will put a cache in front of the store and spend most of the time there." Then ask whether the numbers match what they had in mind.
Practice questions
- 50 M writes per month at a 10:1 read ratio. Average read QPS?
Answer: 50,000,000 / 30 / 86,400 is about 19 writes per second, so about 190 reads per second on average. State it as "about 190", not a precise figure.
- Why size for TiB when your cloud bill shows TB?
Answer: You do not have to; you have to say which one you mean. Disks are sold in TB, many tools show TiB, and the difference is about 9%.
- Your estimate says 116 writes per second. Do you need sharding for writes?
Answer: No. A single relational primary handles that comfortably. Shard for storage size or read load if those require it, not for this write rate.
- What is the first question to ask when the interviewer says "design a rate limiter"?
Answer: Who is being limited and where: per user, per API key, per tenant or per IP, at the gateway or in each service, and whether the limit must be global across instances.
Revision checklist
- Requirements, assumptions, numbers, stack, trade-offs, failure paths.
- 86,400 seconds per day; TB vs TiB; bits vs bytes.
- Size for peak; the peak factor is an assumption.
- The read:write ratio picks the architecture.
Related on TechInterviewNotes
- Course: System Design Fundamentals for .NET and C# (building blocks this course applies)
- Lesson: What to optimize first
- Lesson: Cost trade-offs
- Lesson: Design review checklist
- Course: Microservices and Messaging in .NET (service boundaries, outbox, sagas)
- Mock: System design mock interview for .NET
Analogy
Planning a wedding dinner
You do not order food before asking how many guests are coming, how many eat meat, and whether people arrive all at once. Guests are QPS, plates are storage, and the moment everyone sits down at the same time is your peak factor.
Quick reference
The 45-minute shape
- 5 min requirements, 5 min numbers, 10 min API and data, 10 min design, 15 min deep dive and failures.
- QPS = per day / 86,400; size for peak.
- Storage = rows x bytes x replicas; say TB or TiB.
- Egress in bits: bytes x 8.