ЁЯПл The SchoolтА║ЁЯУР System DesignтА║ЁЯзо рдзрдбрд╛ 02 тАФ Capacity рдЕрдВрджрд╛рдЬ: рд░рдЪрдиреЗрдЪрд╛ рдЖрдХрд╛рд░ рдард░рд╡рдгрд╛рд░реЗ рдЖрдХрдбреЗ
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯзо рдзрдбрд╛ 02 тАФ Capacity рдЕрдВрджрд╛рдЬ: рд░рдЪрдиреЗрдЪрд╛ рдЖрдХрд╛рд░ рдард░рд╡рдгрд╛рд░реЗ рдЖрдХрдбреЗ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 18 рдкреИрдХреА рдзрдбрд╛ 02 ┬╖ рдорд╛рдЧреЗ: lesson-01-requirements ┬╖ рдкреБрдвреЗ: lesson-03-api-design


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбрд╛ 01, рдЖрдгрд┐ рдкрджреНрдзрддреАрдЪреА рджреБрд╕рд░реА рдкрд╛рдпрд░реА: рдХрд╛рдЧрджрд╛рд▓рд╛ рдвреЛрдмрд│ рдЖрдХрдбреНрдпрд╛рдВрдд рдмрджрд▓рд╛ тАФ рдЬреЗ рддреБрдореНрд╣реА рдкрд╛рдЪ рдорд┐рдирд┐рдЯрд╛рдВрдд рдХрд╛рдЧрджрд╛рд╡рд░ рдХрд╛рдвреВ рд╢рдХрддрд╛ тАФ requests per second, peak, рд╡рд░реНрд╖рд╛рдиреБрд╡рд░реНрд╖рд╛рдВрдЪреЗ storage (рдкреНрд░рддреАрдВрд╕рд╣), bandwidth рдЖрдгрд┐ servers. design/blocks.py рдордзреАрд▓ estimate() рдЧрдгрд┐рдд рдХрд░рддреЗ; design/demo.py рдордзреАрд▓ capacity() рддреЗ рд╕реВрдЪрдирд╛ рдлрд▓рдХрд╛рд╕рд╛рдареА рдЪрд╛рд▓рд╡рддреЗ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдХрддрд░рд┐рдирд╛ рдореНрд╣рдгрддреЗ рдирд╡рд╛ рд╣реЙрд▓ "рдЦреВрдк рдореБрд▓рд╛рдВрд╕рд╛рдареА" рдЖрд╣реЗ. рджреАрдкрд┐рдХрд╛ рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рддреАрд▓ рдЖрдХрдбреНрдпрд╛рдВрдЪреА рдЬрд╛рдгрдХрд╛рд░ рдРрд╢реНрд╡рд░реНрдпрд╛рд▓рд╛ "рдЦреВрдк" рдЪреЗ рдЖрдХрдбреЗ рдХрд░рд╛рдпрд▓рд╛ рд╕рд╛рдВрдЧрддреЗ.

рдРрд╢реНрд╡рд░реНрдпрд╛рд▓рд╛ рдЕрдЪреВрдХ рдЕрд╕рдгреНрдпрд╛рдЪреА рдЧрд░рдЬ рдирд╛рд╣реА. рд╣реЙрд▓ рд╡рд░реНрдЧрдЦреЛрд▓реА, рд╡реНрдпрд╛рдпрд╛рдорд╢рд╛рд│рд╛ рдХреА рд╕реНрдЯреЗрдбрд┐рдпрдо рдПрд╡рдвреНрдпрд╛ рдЖрдХрд╛рд░рд╛рдЪрд╛ рдЖрд╣реЗ, рдПрд╡рдвреЗрдЪ рддрд┐рд▓рд╛ рдХрд│рд╛рдпрд▓рд╛ рд╣рд╡реЗ. рдореНрд╣рдгреВрди рддреА рдвреЛрдмрд│ рдореЛрдЬрддреЗ:

рдЖрддрд╛ рдРрд╢реНрд╡рд░реНрдпрд╛ рд╕рд╛рдВрдЧреВ рд╢рдХрддреЗ: "рд╣реА рд╡реНрдпрд╛рдпрд╛рдорд╢рд╛рд│рд╛ рдЖрд╣реЗ, рд╕реНрдЯреЗрдбрд┐рдпрдо рдирд╛рд╣реА. рд╡рд╛рдЪрдгрд╛рд░реЗ рдЦреВрдк, рд▓рд┐рд╣рд┐рдгрд╛рд░реЗ рдлрд╛рд░ рдереЛрдбреЗ, рдЖрдгрд┐ рдиреЛрдВрджрд╡рд╣реА рдПрдХрд╛ рдХрдкрд╛рдЯрд╛рдд рдорд╛рд╡рддреЗ." рддреЗ рд╡рд╛рдХреНрдп рдореНрд╣рдгрдЬреЗ рдЙрддреНрддрд░. рдиреЗрдордХреЗ рдЕрдВрдХ рдирд╡реНрд╣реЗрдд.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    u["ЁЯСк 5M parents<br/>1M notices/day"] --> w["тЬНя╕П writes<br/>1M ├╖ 86,400 тЙИ 11.6/s"]
    w --> r["ЁЯСА reads ├Ч100<br/>тЙИ 1,157/s"]
    r --> avg["ЁЯУК average<br/>тЙИ 1,169 req/s"]
    avg --> pk["тЫ░я╕П peak ├Ч3<br/>тЙИ 3,507 req/s"]
    pk --> sv["ЁЯЦея╕П servers<br/>3,507 ├╖ (500 ├Ч 60%) тЖТ 12"]
    w --> st["ЁЯТ╛ storage<br/>1M ├Ч 2 KB ├Ч 365 ├Ч 5 ├Ч 3 copies<br/>тЙИ 10.95 TB"]
    pk --> bw["ЁЯУ╢ bandwidth<br/>3,507 ├Ч 2 KB тЙИ 7 MB/s"]
    sv --> shape["ЁЯзн the SHAPE<br/>tiny writes ┬╖ heavy reads<br/>тЖТ cache the reads"]
    st --> shape

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/system-design/lesson-diagrams.html#l02

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдЖрдХрдбреЗ рдЖрдХрд╛рд░ рдард░рд╡рддрд╛рдд. рд╕реЗрдХрдВрджрд╛рд▓рд╛ 11.6 writes рдЦреВрдкрдЪ рдХрдореА рдЖрд╣реЗрдд: рдПрдХ database primary рддреЗ рд╕рд╣рдЬ рд╣рд╛рддрд╛рд│рддреЛ, рдореНрд╣рдгреВрди рддреБрдореНрд╣рд╛рд▓рд╛ sharding рд▓рд╛рдЧрдд рдирд╛рд╣реА. Peak рд╡реЗрд│реА рд╕реЗрдХрдВрджрд╛рд▓рд╛ 3,507 reads рд╣реЗ рдЦрд░реЗ рдХрд╛рдо рдЖрд╣реЗ, рдЖрдгрд┐ рддреНрдпрд╛рддрд▓реЗ 99% reads рдЖрд╣реЗрдд, рдореНрд╣рдгреВрди рддреБрдореНрд╣рд╛рд▓рд╛ cache рд╣рд╡рд╛рдЪ (рдзрдбрд╛ 05). 5 рд╡рд░реНрд╖рд╛рдВрдд 11 TB рдПрдХрд╛ database cluster рдордзреНрдпреЗ рдорд╛рд╡рддреЛ. рдЖрдХрдбреНрдпрд╛рдВрд╢рд┐рд╡рд╛рдп рд▓реЛрдХ рдЕрдВрджрд╛рдЬ рдмрд╛рдВрдзрддрд╛рдд тАФ рдЖрдгрд┐ рдЕрдВрджрд╛рдЬ рджреЛрдиреНрд╣реА рджрд┐рд╢рд╛рдВрдиреА рдЪреБрдХреВ рд╢рдХрддрд╛рдд: рдкрд╣рд┐рд▓реНрдпрд╛рдЪ рджрд┐рд╡рд╢реА рдХреЛрд╕рд│рдгрд╛рд░реА рд░рдЪрдирд╛, рдХрд┐рдВрд╡рд╛ рдПрдХ server рд╕рд╣рдЬ рдЙрдЪрд▓реЗрд▓ рдЕрд╢рд╛ load рд╕рд╛рдареА 40 microservices рдЖрдгрд┐ sharded database.

рдЙрддреНрддрд░ рдореНрд╣рдгрдЬреЗ рдЖрдХрд╛рд░, рдЕрдВрдХ рдирд╡реНрд╣реЗ. рддреБрдореНрд╣реА 12 servers рдореНрд╣рдгрд╛рд▓рд╛рдд рдХреА 14, рд╣реЗ рдХреЛрдгреА рддрдкрд╛рд╕рдгрд╛рд░ рдирд╛рд╣реА. рддреБрдореНрд╣реА "writes рд▓рд╣рд╛рди, reads рдЬрдб" рд╣реЗ рдкрд╛рд╣рд┐рд▓реЗ рдХрд╛ рдЖрдгрд┐ reads рдЪреНрдпрд╛ рдкреБрдвреЗ cache рдареЗрд╡рд▓рд╛ рдХрд╛, рд╣реЗ рддреЗ рддрдкрд╛рд╕рддреАрд▓.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

design/blocks.py рдордзреАрд▓ estimate(daily_users, actions_per_user, read_write_ratio, bytes_per_write, peak_factor=3, per_server_rps=500, years=5, replicas=3):

  1. рджрд┐рд╡рд╕рд╛рдЪреЗ writes = users ├Ч рдкреНрд░рддреНрдпреЗрдХрд╛рдЪреНрдпрд╛ рдХреГрддреА (5M ├Ч 0.2 = 1M рд╕реВрдЪрдирд╛)
  2. рджрд┐рд╡рд╕рд╛рдЪреЗ reads = writes ├Ч ratio (├Ч 100)
  3. рд╕рд░рд╛рд╕рд░реА req/s = (reads + writes) ├╖ DAY (86,400)
  4. peak = рд╕рд░рд╛рд╕рд░реА ├Ч peak factor
  5. servers = ceil(peak ├╖ (per_server_rps ├Ч 0.6))
  6. storage = рджрд┐рд╡рд╕рд╛рдЪреЗ writes ├Ч bytes ├Ч 365 ├Ч рд╡рд░реНрд╖реЗ ├Ч replicas
  7. egress = peak ├Ч рдкреНрд░рддреНрдпреЗрдХрд╛рдЪреЗ bytes

human() bytes рдирд╛ KB, MB, GB, TB рдордзреНрдпреЗ рдЫрд╛рдкрддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 design/demo.py capacity
python3 - <<'EOF'
import sys; sys.path.insert(0, "design"); from blocks import estimate, human, DAY
print(f"1M a day = {1_000_000 / DAY:.1f} per second ┬╖ 100M a day = {100_000_000 / DAY:,.0f} per second")
for users in (500_000, 5_000_000, 50_000_000):
    e = estimate(users, 0.2, 100, 2_000)
    print(f"{users:>10,} parents тЖТ peak {e['peak_rps']:>6,} req/s ┬╖ {e['servers']:>3} servers ┬╖ {e['storage_tb']:>6} TB")
for peak in (2, 3, 10):
    print(f"peak ├Ч{peak:<2} тЖТ {estimate(5_000_000, 0.2, 100, 2_000, peak_factor=peak)['servers']:>2} servers")
for copies in (1, 3):
    print(f"replicas={copies} тЖТ {estimate(5_000_000, 0.2, 100, 2_000, replicas=copies)['storage_tb']:>5} TB in 5 years")
print("one year of notices, one copy:", human(1_000_000 * 2_000 * 365))
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

capacity рдЫрд╛рдкрддреЗ:

тФАтФА 5M parents ┬╖ 1M notices a day across all schools ┬╖ 100 reads per notice ┬╖ 2 KB each ┬╖ peak ├Ч3 ┬╖ 5 years ┬╖ 3 copies
   writes_per_s   11.6
   reads_per_s    1157.4
   avg_rps        1169
   peak_rps       3507
   servers        12
   storage_tb     10.95
   egress_mb_s    7.0
   rules of thumb: a day тЙИ 86,400 s тЙИ 10^5 ┬╖ 1M/day тЙИ 12/s ┬╖ 2 KB ├Ч 1M = 2.0 GB
   the answer is a SHAPE, not a digit: tiny writes, heavy reads, ~11 TB in 5 years тЖТ cache the reads; one database cluster is enough

рддреБрдордЪрд╛ snippet рдЫрд╛рдкрддреЛ:

1M a day = 11.6 per second ┬╖ 100M a day = 1,157 per second
   500,000 parents тЖТ peak    351 req/s ┬╖   2 servers ┬╖   1.09 TB
 5,000,000 parents тЖТ peak  3,507 req/s ┬╖  12 servers ┬╖  10.95 TB
50,000,000 parents тЖТ peak 35,069 req/s ┬╖ 117 servers ┬╖  109.5 TB
peak ├Ч2  тЖТ  8 servers
peak ├Ч3  тЖТ 12 servers
peak ├Ч10 тЖТ 39 servers
replicas=1 тЖТ  3.65 TB in 5 years
replicas=3 тЖТ 10.95 TB in 5 years
one year of notices, one copy: 730.0 GB

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рджрд╣рд╛рдкрдЯ рдкрд╛рд▓рдХ рдореНрд╣рдгрдЬреЗ рд╕рдЧрд│реЗрдЪ рджрд╣рд╛рдкрдЯ тАФ рдЧрдгрд┐рдд рд╕реЛрдкреЗ рдЖрд╣реЗ, рдЖрдгрд┐ рддреЛрдЪ рдореБрджреНрджрд╛ рдЖрд╣реЗ. 50 million рдкрд╛рд▓рдХрд╛рдВрд╡рд░, 109.5 TB рдЖрдгрд┐ 117 servers рд╣рд╛ рд╡реЗрдЧрд│рд╛ рдЖрдХрд╛рд░ рдЖрд╣реЗ: рдЖрддрд╛ data рд╡рд┐рднрд╛рдЧрд╛рд╡рд╛ рд▓рд╛рдЧреВ рд╢рдХрддреЛ (Database рд╢рд╛рд│рд╛) рдЖрдгрд┐ reads cache рдХрд░рд╛рд╡реЗрдЪ рд▓рд╛рдЧрддрд╛рдд. ├Ч10 рдЪрд╛ peak (рдирд┐рдХрд╛рд▓рд╛рдЪрд╛ рджрд┐рд╡рд╕) 12 рдирд╡реНрд╣реЗ рддрд░ 39 servers рдорд╛рдЧрддреЛ. рдЖрдгрд┐ 3 рдкреНрд░рддреА disk рддрд┐рдкреНрдкрдЯ рдХрд░рддрд╛рдд: 3.65 TB рдЪреЗ 10.95 TB рд╣реЛрддрд╛рдд.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдкреНрд░рддреНрдпрдХреНрд╖ рдХрд╛рдорд╛рдд рдЖрдХрдбреЗ рдлрдХреНрдд рдЕрдВрджрд╛рдЬрд╛рдВрддреВрди рдирд╛рд╣реА, рддрд░ рдореЛрдЬрдорд╛рдкрд╛рдВрддреВрди рдпреЗрддрд╛рдд. On a real account тАФ рдЦрд░рд╛ peak factor load balancer рдЪреНрдпрд╛ request count рдордзреНрдпреЗ, рдорд┐рдирд┐рдЯрд╛-рдорд┐рдирд┐рдЯрд╛рд▓рд╛ рдЕрд╕рддреЛ; рд╕рд░реНрд╡рд╛рдд рдЧрд░реНрджреАрдЪреЗ рдорд┐рдирд┐рдЯ рджрд┐рд╡рд╕рд╛рдЪреНрдпрд╛ рд╕рд░рд╛рд╕рд░реАрдиреЗ рднрд╛рдЧрд╛:

aws cloudwatch get-metric-statistics --namespace AWS/ApplicationELB \
    --metric-name RequestCount --statistics Sum --period 60 \
    --dimensions Name=LoadBalancer,Value=app/notice-alb/50dc6c495c0c9188 \
    --start-time 2026-09-21T00:00:00Z --end-time 2026-09-22T00:00:00Z

рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ row рдЪреЗ рдЦрд░реЗ bytes database рдордзреНрдпреЗ рдЕрд╕рддрд╛рдд тАФ рдЦрд▒реНрдпрд╛ data рдЕрд╕рд▓реЗрд▓реНрдпрд╛ рдкреНрд░рддреАрд╡рд░ рддреЗ рдореЛрдЬрд╛:

-- PostgreSQL: average stored size of one notice row, and the table + index size
SELECT avg(pg_column_size(n.*)) AS bytes_per_row FROM notices n;
SELECT pg_size_pretty(pg_total_relation_size('notices'));

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдЕрдВрджрд╛рдЬ design doc рдордзреНрдпреЗ рддреНрдпрд╛рдЪреНрдпрд╛ рдЧреГрд╣рд┐рддрдХрд╛рдВрд╕рд╣ рдареЗрд╡рд╛ ("рдкреНрд░рддреНрдпреЗрдХ рд╕реВрдЪрдиреЗрд▓рд╛ 100 reads, peak ├Ч3"). рдЦрд░рд╛ traffic рдЖрд▓рд╛ рдХреА рддреБрд▓рдирд╛ рдХрд░рд╛. рдЖрдард╡рдбрд╛ 2 рдордзреНрдпреЗ рд╕рд╛рдкрдбрд▓реЗрд▓реЗ рдЪреБрдХреАрдЪреЗ рдЧреГрд╣рд┐рддрдХ рд╕реНрд╡рд╕реНрдд рдЕрд╕рддреЗ; рдирд┐рдХрд╛рд▓рд╛рдЪреНрдпрд╛ рджрд┐рд╡рд╢реА рд╕рд╛рдкрдбрд▓реЗ, рддрд░ рддреЗ outage рдЕрд╕рддреЗ.

тПня╕П рдкреБрдвреЗ

рдЖрддрд╛ рдЖрдХрд╛рд░ рдХрд│рд▓рд╛. рдХреЛрдгреАрд╣реА code рд▓рд┐рд╣рд┐рдгреНрдпрд╛рдЖрдзреА, рдирд┐рдпреЛрдЬрди рдХрд╛рд░реНрдпрд╛рд▓рдп рдХрд░рд╛рд░ (contract) рд▓рд┐рд╣рд┐рддреЗ: app рдХреЛрдгрддреЗ requests рдкрд╛рдард╡рддреЛ рдЖрдгрд┐ server рдХрд╛рдп рдЙрддреНрддрд░ рджреЗрддреЛ.

git checkout lesson-03-api-design

ЁЯзо Lesson 02 тАФ Capacity estimation: the numbers that decide the shape

ЁЯУН You are here: Lesson 02 of 18 ┬╖ Previous: lesson-01-requirements ┬╖ Next: lesson-03-api-design


ЁЯУж What's in this branch

Lesson 01, plus the second step of the method: turn the sheet into rough numbers you can work out on paper in five minutes тАФ requests per second, the peak, storage over the years (with copies), bandwidth and servers. estimate() in design/blocks.py does the arithmetic; capacity() in design/demo.py runs it for the notice board.

ЁЯзТ Explain like I'm 5

Katrina says the new hall is for "a lot of children". Dipika asks Aishwarya, the office's number person, to make "a lot" into numbers.

Aishwarya does not need to be exact. She needs to know if the hall is the size of a classroom, a gym or a stadium. So she counts roughly:

Now Aishwarya can say: "It is a gym, not a stadium. Lots of readers, very few writers, and the register fits in one cupboard." That sentence is the answer. The exact digits are not.

ЁЯЧ║я╕П Diagram

flowchart LR
    u["ЁЯСк 5M parents<br/>1M notices/day"] --> w["тЬНя╕П writes<br/>1M ├╖ 86,400 тЙИ 11.6/s"]
    w --> r["ЁЯСА reads ├Ч100<br/>тЙИ 1,157/s"]
    r --> avg["ЁЯУК average<br/>тЙИ 1,169 req/s"]
    avg --> pk["тЫ░я╕П peak ├Ч3<br/>тЙИ 3,507 req/s"]
    pk --> sv["ЁЯЦея╕П servers<br/>3,507 ├╖ (500 ├Ч 60%) тЖТ 12"]
    w --> st["ЁЯТ╛ storage<br/>1M ├Ч 2 KB ├Ч 365 ├Ч 5 ├Ч 3 copies<br/>тЙИ 10.95 TB"]
    pk --> bw["ЁЯУ╢ bandwidth<br/>3,507 ├Ч 2 KB тЙИ 7 MB/s"]
    sv --> shape["ЁЯзн the SHAPE<br/>tiny writes ┬╖ heavy reads<br/>тЖТ cache the reads"]
    st --> shape

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/system-design/lesson-diagrams.html#l02

тЭУ What

ЁЯдФ Why

Because the numbers decide the shape. 11.6 writes a second is tiny: one database primary handles it easily, so you do not need sharding. 3,507 reads a second at the peak is real work, and 99% of it is reads, so you do want a cache (lesson 05). 11 TB in 5 years fits in one database cluster. Without the numbers people guess тАФ and guesses can be wrong in both directions: a design that fails on day one, or 40 microservices and a sharded database for a load one server could carry.

The answer is a shape, not a digit. Nobody will check that you said 12 servers and not 14. They will check that you saw "tiny writes, heavy reads" and put a cache in front of the reads.

ЁЯФз How (in this repo)

estimate(daily_users, actions_per_user, read_write_ratio, bytes_per_write, peak_factor=3, per_server_rps=500, years=5, replicas=3) in design/blocks.py:

  1. writes a day = users ├Ч actions each (5M ├Ч 0.2 = 1M notices)
  2. reads a day = writes ├Ч ratio (├Ч 100)
  3. average req/s = (reads + writes) ├╖ DAY (86,400)
  4. peak = average ├Ч peak factor
  5. servers = ceil(peak ├╖ (per_server_rps ├Ч 0.6))
  6. storage = writes a day ├Ч bytes ├Ч 365 ├Ч years ├Ч replicas
  7. egress = peak ├Ч bytes each

human() prints bytes as KB, MB, GB, TB.

ЁЯзк Try it

python3 design/demo.py capacity
python3 - <<'EOF'
import sys; sys.path.insert(0, "design"); from blocks import estimate, human, DAY
print(f"1M a day = {1_000_000 / DAY:.1f} per second ┬╖ 100M a day = {100_000_000 / DAY:,.0f} per second")
for users in (500_000, 5_000_000, 50_000_000):
    e = estimate(users, 0.2, 100, 2_000)
    print(f"{users:>10,} parents тЖТ peak {e['peak_rps']:>6,} req/s ┬╖ {e['servers']:>3} servers ┬╖ {e['storage_tb']:>6} TB")
for peak in (2, 3, 10):
    print(f"peak ├Ч{peak:<2} тЖТ {estimate(5_000_000, 0.2, 100, 2_000, peak_factor=peak)['servers']:>2} servers")
for copies in (1, 3):
    print(f"replicas={copies} тЖТ {estimate(5_000_000, 0.2, 100, 2_000, replicas=copies)['storage_tb']:>5} TB in 5 years")
print("one year of notices, one copy:", human(1_000_000 * 2_000 * 365))
EOF

тЬЕ Verify тАФ what you should see

capacity prints:

тФАтФА 5M parents ┬╖ 1M notices a day across all schools ┬╖ 100 reads per notice ┬╖ 2 KB each ┬╖ peak ├Ч3 ┬╖ 5 years ┬╖ 3 copies
   writes_per_s   11.6
   reads_per_s    1157.4
   avg_rps        1169
   peak_rps       3507
   servers        12
   storage_tb     10.95
   egress_mb_s    7.0
   rules of thumb: a day тЙИ 86,400 s тЙИ 10^5 ┬╖ 1M/day тЙИ 12/s ┬╖ 2 KB ├Ч 1M = 2.0 GB
   the answer is a SHAPE, not a digit: tiny writes, heavy reads, ~11 TB in 5 years тЖТ cache the reads; one database cluster is enough

Your snippet prints:

1M a day = 11.6 per second ┬╖ 100M a day = 1,157 per second
   500,000 parents тЖТ peak    351 req/s ┬╖   2 servers ┬╖   1.09 TB
 5,000,000 parents тЖТ peak  3,507 req/s ┬╖  12 servers ┬╖  10.95 TB
50,000,000 parents тЖТ peak 35,069 req/s ┬╖ 117 servers ┬╖  109.5 TB
peak ├Ч2  тЖТ  8 servers
peak ├Ч3  тЖТ 12 servers
peak ├Ч10 тЖТ 39 servers
replicas=1 тЖТ  3.65 TB in 5 years
replicas=3 тЖТ 10.95 TB in 5 years
one year of notices, one copy: 730.0 GB

ЁЯПБ What you just proved

Ten times the parents gives ten times everything тАФ the arithmetic is simple, and that is the point. At 50 million parents, 109.5 TB and 117 servers is a different shape: now the data may need splitting (the Database school) and the reads must be cached. A peak of ├Ч10 (results day) needs 39 servers, not 12. And the 3 copies triple the disk: 3.65 TB becomes 10.95 TB.

тЪая╕П Common mistakes

ЁЯПн In production

In real work the numbers come from measurements, not only from guesses. On a real account тАФ the real peak factor is in the load balancer's request count, minute by minute; divide the busiest minute by the daily average:

aws cloudwatch get-metric-statistics --namespace AWS/ApplicationELB \
    --metric-name RequestCount --statistics Sum --period 60 \
    --dimensions Name=LoadBalancer,Value=app/notice-alb/50dc6c495c0c9188 \
    --start-time 2026-09-21T00:00:00Z --end-time 2026-09-22T00:00:00Z

And the real bytes per row are in the database тАФ measure them on a copy with real data:

-- PostgreSQL: average stored size of one notice row, and the table + index size
SELECT avg(pg_column_size(n.*)) AS bytes_per_row FROM notices n;
SELECT pg_size_pretty(pg_total_relation_size('notices'));

ЁЯПн Why this matters in production: put the estimate in the design doc with its assumptions ("100 reads per notice, peak ├Ч3"). When real traffic arrives, compare. A wrong assumption found in week 2 is cheap; found on results day, it is an outage.

тПня╕П Next

Now we know the size. Before anyone writes code, the planning office writes the contract: which requests the app sends and what the server answers.

git checkout lesson-03-api-design
тЖР PreviousrequirementsNext тЖТapi design

This page is the lesson's README from the lesson-02-capacity branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.