Index Ai Academy
Phase 11 · Professional practice · System design

System design fundamentals

Requirements, rough numbers, the standard parts and where systems break.

About 40 minutes · Free lesson

Start this lesson

Why it matters

Writing code that works on your laptop is one skill. Designing a system that serves a million people, survives a server dying and doesn't bankrupt the company is another. It's the skill senior engineers are hired for and the core of system design interviews. It rests on a few habits: ask what's really needed, do rough math before building, and know the standard parts and their trade-offs.

What you'll be able to do

  • Turn a vague product idea into requirements, including how fast, how big and how reliable
  • Estimate traffic, storage and server counts with rough numbers
  • Name the standard building blocks and when each one is needed
  • Work out availability for parts in a chain and in parallel, and spread data across servers with consistent hashing

What it covers

  1. Start with requirements
  2. A back-of-the-envelope estimate
  3. The standard parts
  4. What a cache does to the database
  5. When one database isn't enough
  6. Consistency trade-offs
  7. Why modulo sharding hurts
  8. Don't build for scale you don't have
  9. Replicas lag

Start with requirements

Before designing a house, an architect asks how many people will live there and whether it has to survive hurricanes. Before drawing boxes, pin down two kinds of requirements:

  • Functional: what it does. "Users upload a photo; followers see it in their feed."
  • Non-functional: how well. Latency, meaning how long people wait ("feed loads in under 200 ms"), scale ("50 million daily users"), availability ("99.9% of the time"), consistency ("a like may take a few seconds to show"), cost and security.

The non-functional ones drive the design. A feed for 1,000 people is one server and a database. For 50 million, it's a different system. Ask, and write the answers down.

Rough numbers

Planning a party, you work out guests times slices before ordering pizza. Back-of-the-envelope math is the same habit, and it tells you which problems are real:

  • Requests per second: daily users × requests per user ÷ 86,400 seconds. Peaks are often 2 to 3 times the average.
  • Storage: items per day × size × how long you keep them.
  • Servers: peak requests per second ÷ what one server handles, plus spares.

Know a few speeds by their order of size:

operationabout
read from memory100 nanoseconds
read from an SSD100 microseconds
round trip inside a data center0.5 milliseconds
round trip across an ocean100+ milliseconds
one LLM reply1 to 30 seconds

Think of memory as the pen on your desk, disk as a cabinet down the hall, and the network as a letter across town. Memory is about 1,000 times faster than disk, and the network is slower still. That's why caches exist, and why you avoid many sequential network calls on one request.

Worked example

A back-of-the-envelope estimate

Say a shop has 200,000 daily users who make 25 requests each. Is that a big system? Rough arithmetic answers in a minute.

users = 200_000
requests_each = 25
peak_factor = 3
per_server = 500       # requests a second one server handles
order_size_kb = 2
orders_a_day = 8_000

average = users * requests_each / 86_400
peak = average * peak_factor
servers = int(peak // per_server) + 1 + 1   # round up, plus one spare

print("average requests a second:", round(average))
print("peak requests a second:", round(peak))
print("servers:", servers)
print("order storage a year (GB):", round(orders_a_day * order_size_kb * 365 / 1_000_000, 1))
It runs right here in your browser, with nothing to install.

About 170 requests a second at peak is two small servers, and a few GB of orders a year fits one database. So this design doesn't need sharding or a fleet of services. The numbers save you from building for problems you don't have.

Quick check
10 million daily users each make 20 requests a day. Roughly what's the average request rate?
  1. About 23 per second
  2. About 200 million per second
  3. About 230,000 per second
  4. About 2,300 per second
Show the answer

About 2,300 per second

200 million requests ÷ 86,400 seconds ≈ 2,300 per second. Plan for a peak of maybe 5,000 to 7,000.

The standard parts

Most systems are built from the same parts, the way most restaurants have a host, waiters, a ledger and a storeroom. The host at the door is your load balancer, sending each guest to a free waiter. Here's the full list:

partjobadd it when
load balancerspreads requests across serversyou have more than one server
stateless app serversrun your code; keep no user state in memoryalways: it lets you add servers freely
databasethe source of truthalways
read replicascopies of the database for readsreads overwhelm one database
cachekeeps hot data in memorythe same data is read again and again
queue and workersdo slow work later, outside the requestwork takes seconds, or can be retried
object storagefiles: images, videos, backupsyou store files, not only rows
CDNserves static files from near the userusers are spread around the world

Stateless servers are the key idea. Any waiter can serve any table if orders live in a shared system instead of one waiter's head. If any server can handle any request, because sessions live in the database or a cache, scaling is "add more servers behind the load balancer".

In plain words: add a part when you have the problem it solves, and not before.

Worked example

What a cache does to the database

A cache in front of the database takes most reads away from it. Watch how fast the load falls as the hit rate climbs.

reads = 2000     # reads a second

for hit_rate in [0, 0.5, 0.9, 0.99]:
    to_database = reads * (1 - hit_rate)
    print(f"hit rate {hit_rate:>4.0%}: {to_database:>6.0f} reads a second reach the database")
In the lesson you can change this and run it.

Going from 90% to 99% cuts the database's load by ten times, even though the hit rate only moved nine points. It works the other way as well: if a cache restarts empty, all that load lands on the database at once.

When one database isn't enough

First add read replicas, which are like photocopies of a master ledger. Replicas lag a little behind, so a user might not see their own write for a moment; read from the primary right after a write if that matters.

Next comes sharding: splitting data across several databases by a key, such as user id. A library might keep surnames A to H in one branch and I to Z in another. The simple way, hash(key) % n, has a catch: when n changes, almost every key moves. Consistent hashing places servers and keys on a ring, and each key goes to the next server clockwise. Adding a server then moves only about 1/n of the keys.

Availability math

Availability is the share of time a system works. Old holiday lights on one wire go dark when a single bulb fails. Parts in a chain, all needed, behave the same way, and their availabilities multiply: a 99.9% load balancer, 99.9% servers and a 99.9% database give 99.7%.

Carrying two flashlights is different. You're only in the dark if both die. Redundant copies, any one enough, fail only if all fail: two 99% servers give 1 − 0.01² = 99.99%.

So every part that's needed and has no backup, a single point of failure, caps the whole system. Find them, and add a spare where the cost is worth it.

Try it now

Chains multiply, so every dependency costs you. Copies fail only if they all fail at once.

Consistency trade-offs

Say two bank branches lose their phone line. They can refuse withdrawals until it's back, or keep paying out and risk mistakes. When the network between data centers breaks, a system must make the same choice: refuse some requests to stay consistent, or keep answering with possibly stale data to stay available. That's the CAP theorem in practice. Bank balances choose consistency. Like counts choose availability, with eventual consistency: copies agree soon, not instantly.

Quick check
Three copies of a 99% available server, and any one is enough. What does this print?
a = 0.99
print(round(1 - (1 - a) ** 3, 6))
  1. 0.970299
  2. 2.97
  3. 0.99
  4. 0.999999
Show the answer

0.999999

All three must fail together for an outage, which is 0.01³, so availability is 1 − 0.000001 = 0.999999. Redundancy turns two nines into six.

Worked example

Why modulo sharding hurts

Count how many of 1,000 keys move when a fourth server becomes five.

import hashlib

def h(text):
    return int(hashlib.sha256(text.encode()).hexdigest()[:8], 16)

keys = [f"user-{i}" for i in range(1000)]
before = {k: h(k) % 4 for k in keys}
after = {k: h(k) % 5 for k in keys}
moved = sum(before[k] != after[k] for k in keys)
print(f"{moved} of 1000 keys moved")
In the lesson you can change this and run it.

About 80% move, so adding a server means copying most of the data. Note the use of hashlib: Python's own hash() of a string changes every time the program starts, so it can't be used to place data.

Quick check
Why use hashlib, not Python's built-in hash(), to choose which server holds a key?
  1. hash() of a string changes each time Python starts, so keys would move after a restart
  2. hash() only works on numbers
  3. hashlib is faster
  4. hashlib makes data smaller
Show the answer

hash() of a string changes each time Python starts, so keys would move after a restart

Placement must be stable across processes and restarts. A real hash function gives the same answer every time.

Don't build for scale you don't have

You don't build a stadium for a backyard game. Ask how big it needs to be before designing anything. And never use Python's hash() to place data across servers: it changes each time Python starts.

Replicas lag

A photocopy can't include what was written after it was made. A replica may be a moment behind, so someone can save a change and not see it on their next read.

Practice what you just read

  • 3 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Start this lesson

Try a quiz question

Why should app servers be stateless?

  1. Any server can handle any request, so you scale by adding servers behind a load balancer
  2. Stateless servers use less electricity, since they don't hold user data in memory
  3. Databases require it, because two servers can't connect to the same one at once
  4. So they can't crash, because there's nothing in memory that could become corrupted

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson