System Design Cases

7-krokový framework + plně rozpracovaný URL shortener.

Tady se sbíhá všechno z předchozích kapitol. System design znamená navrhnout, jak z jednotlivých dílů (databáze, cache, fronty, služby) poskládat celý systém, který zvládne daný úkol i zátěž. Není to o znalosti jedné věci nazpaměť — je to o kombinování trade-offů podle konkrétního zadání. Tohle je finále, kde se ukáže, jestli předchozí témata umíš použít dohromady.


Pár pojmů na úvod

  • System design = návrh celého systému z vyšší perspektivy (jaké díly, jak je propojit).
  • QPS (queries per second) = kolik dotazů/requestů systém zvládá za sekundu. Základní míra zátěže.
  • Read-heavy / write-heavy = systém, kde se hlavně čte, vs. systém, kde se hlavně zapisuje. Tenhle poměr určuje skoro celý návrh.
  • Funkční požadavky = co systém má dělat (např. „zkrátit URL"). Nefunkční = jak dobře (jak rychle, pro kolik uživatelů, jak spolehlivě).
  • Back-of-envelope odhad = hrubý výpočet „na koleni" (na zadní stranu obálky), abys získal řád.

Univerzální postup (drž se ho pokaždé)

Největší chyba začátečníka je hned začít kreslit boxy. Místo toho jdi po krocích:

1. Ujasni si požadavky (neskákej rovnou na řešení!)

  • Funkční: co přesně má systém umět?
  • Nefunkční: kolik uživatelů? Hlavně se čte, nebo zapisuje? Jak rychlá odezva? Jak moc vadí chvilkově zastaralá data?
  • Co je mimo rozsah — vymezit, co řešit nebudeš, je půlka úspěchu.

2. Hrubý odhad rozsahu (back-of-envelope)

  • Kolik requestů za sekundu (zvlášť čtení a zápis)? Kolik dat denně? Kolik za rok?
  • Z toho hned poznáš, jestli stačí jeden server, nebo potřebuješ cache a víc strojů. Poměr čtení ku zápisu určuje tvar řešení: hodně čtení → cache + read replicas; hodně zápisu → rozdělení dat + fronty.

3. API (smlouva)

  • Načrtni pár hlavních endpointů — tím se ujasní, co systém vlastně dělá (API Design).

4. Datový model a úložiště

  • Jaké entity a vztahy? SQL, nebo NoSQL a proč (Databáze)?

5. Hrubý návrh (high-level)

  • Teď teprve boxy a šipky: klient → load balancer → služby → cache → databáze → fronty. Projdi, kudy tečou data při hlavních scénářích.

6. Zoom do míst, kde to bolí

  • Tady aplikuješ patterny z předchozích kapitol: indexy, cache, rozdělení dat, idempotenci, fronty.

7. Úzká hrdla, selhání, škálování

  • Kde to praskne? Co když přijde 10× víc provozu? Co se stane, když spadne jedna část?

Žádný „jediný správný" návrh neexistuje. Hodnotí se, jestli vidíš trade-offy a umíš svoje volby obhájit. „Záleží na…" je naprosto správný začátek odpovědi.


✍️ Vzorový případ: Zkracovač URL (jako TinyURL)

1. Požadavky

  • Funkční: z dlouhé URL udělej krátkou; po zadání krátké přesměruj (redirect) na původní dlouhou.
  • Nefunkční: hlavně se čte (přesměrování je mnohem víc než vytváření, řekněme 100:1). Přesměrování musí být bleskové a nesmí vypadnout.

2. Odhad

  • Řekněme 100 milionů nových URL měsíčně → asi 40 zápisů za sekundu (řádově), a při poměru 100:1 tedy ~4000 čtení za sekundu. Hodně čtení → bude potřeba cache!

3. API

POST /urls         { url }            → { shortCode }
GET  /{shortCode}  → přesměrování (redirect) na původní URL

4. Datový model

Stačí tabulka, kde ke krátkému kódu najdeš dlouhou URL:

short_code (klíč) | long_url | created_at | owner_id

Hledáš vždy podle krátkého kódu → sedí na jednoduché vyhledávání podle klíče (indexovaný Postgres nebo úložiště klíč-hodnota).

5. Jak vyrobit krátký kód (jádro úlohy)

  • Z hashe: spočítáš otisk URL a zkrátíš ho — hrozí ale kolize (dvě URL vyjdou stejně), musíš řešit.
  • Z počítadla + Base62: vezmeš rostoucí číslo (ID) a zakóduješ ho do znaků 0–9, a–z, A–Z (tomu se říká Base62). Na 7 znaků se vejde ~3,5 bilionu kombinací. Bez kolizí, ale kódy jsou uhodnutelné (jdou popořadě) → enumeration: někdo projde /aaaa1, /aaaa2… a vyscrapuje cizí (i privátní) odkazy. Obrana: nesekvenční ID (zamíchaný/šifrovaný čítač, Hashids).
  • Volba je trade-off: kolize vs. uhodnutelnost. Vyber jednu a obhaj proč.

Na co se na pohovoru ptají skoro vždy: expirace/TTL odkazů (a co to udělá se storage odhadem) a custom alias (uživatel si zvolí kód → musíš řešit kolizi na úrovni vstupu, ne hashe).

6. Zoom na cestu čtení (tam žije výkon)

  • Hodně čtení → cache je středobod (Caching). Hodně populární odkaz = hot key, řeš ho kopiemi klíče nebo lokální cache.

7. Škálování a selhání

  • Databáze: read replicas (kopie pro čtení) hodně pomůžou, protože jde hlavně o čtení.
  • Výpadek cache → požadavky spadnou na databázi; systém to musí ustát (zpomalit, ne zkolabovat).
  • Počítání kliků (analytika)nedělej synchronně v cestě přesměrování! Pošli o kliku zprávu do fronty a zpracuj ji vedle (Messaging). Přesměrování musí být bleskové.

Všimni si, jak se v jednom příkladu potkalo: cache, read replicas, hot key, asynchronní fronta a generování ID. To je system design — poskládat dohromady všechno z předchozích kapitol.


✍️ Vzorový případ: Rate limiter (kontrast — write-heavy, latence-kritický)

1. Požadavky

  • Funkční: pro daný klíč (uživatel / IP / API klíč) povol nejvýš N requestů za okno (např. 100/min), jinak vrať 429.
  • Nefunkční: musí být velmi rychlý (běží před každým requestem — přidaná latence = daň pro všechny), přesný napříč instancemi a sám nesmí být single point of failure.

2. Odhad

  • Běží u každého requestu → QPS rate limiteru = QPS celé služby (klidně desetitisíce/s). Takže rozhodnutí musí stát zlomek ms a ideálně 1 levnou operaci. Dat je málo (počítadlo na klíč).

3. Jádro: algoritmus + kde držet stav

  • Algoritmus: token bucket (z Patternů) — zvládá nárazy, nejpoužívanější.
  • Stav (počítadla) nemůže být v paměti instance — limit by platil jen per instance, ne dohromady. Drží se centrálně v Redisu.

4. Zoom na pasti

  • Atomicita: „přečti počet → rozhodni → zapiš" má race condition. Musí to být jedna atomická operace (Lua skript v Redisu), jinak při souběhu pustíš víc, než smíš (Patterny).
  • Redis spadne: rate limiter nesmí položit celou službu. Rozhodni se předem: fail-open (při výpadku radši pusť) vs fail-closed (radši odmítej) — pro běžné API obvykle fail-open.
  • Vždy vrať Retry-After, ať klient ví, kdy to zkusit znovu.

Pointa kontrastu s URL shortenerem: tady nejde o storage ani cache, ale o atomicitu, latenci na kritické cestě a chování při výpadku závislosti. Jiné zadání → jiné těžiště.


✍️ Vzorový případ: News feed / timeline (kontrast — fan-out trade-off)

1. Požadavky

  • Funkční: uživatel vidí feed příspěvků lidí, které sleduje, seřazený (zjednodušíme na chronologicky).
  • Nefunkční: read-heavy (čtení feedu >> psaní příspěvků), feed se má načíst rychle.

2. Jádro úlohy: kdy feed sestavit?

Tohle je nejklasičtější senior trade-off v system designu — fan-out:

  • Fan-out on read (sestav při čtení): při otevření feedu se zeptáš „dej příspěvky všech, koho sleduju, seřaď". Jednoduché, zápis levný — ale čtení drahé (a feed je read-heavy → bolí).
  • Fan-out on write (rozešli při psaní): když někdo přidá příspěvek, hned ho zkopíruješ do feedů všech jeho sledujících (předpočítaný feed v cache). Čtení je pak bleskové (jen vytáhni hotový seznam) — ale zápis drahý.

3. Past: celebrity problem

Fan-out on write se rozbije u uživatele s 10 miliony sledujících — jeden jeho příspěvek znamená 10M zápisů. Proto se v praxi dělá hybrid: běžní uživatelé fan-out on write (předpočítaný feed), celebrity fan-out on read (jejich příspěvky se domíchají do feedu až při čtení).

Pointa: na otázku „jak navrhneš feed" není správná odpověď „takhle", ale „záleží na poměru sledujících — fan-out on write vs on read, a u celebrit hybrid". To je přesně ta schopnost vidět trade-off, kterou system design testuje.


✍️ Vzorový případ: Platební systém (kontrast — konzistence nade vše)

1. Požadavky

  • Funkční: zaplatit objednávku přes externí platební bránu (Stripe apod.), potvrdit, případně vrátit.
  • Nefunkční: žádná dvojitá ani ztracená platba, vše auditovatelné. Správnost je tu důležitější než výkon — radši pomalejší a jisté.

2. Jádro: tři pasti a jak je obejít

Platby spojují skoro všechny patterny z Patternů:

  • Dvojí stržení při retry. Klient/síť request zopakuje → idempotency key na POST /payments: server podle klíče pozná, že platbu už založil, a vrátí původní výsledek místo nové platby.
  • Dvojí zápis (zaplaceno v bráně, ale ne u tebe). Strhneš v bráně, pak spadneš dřív, než si to poznamenáš → peníze pryč, systém o nich neví. Řešení: stavový automat platby (pending → authorized → captured → failed), nikdy ne „rovnou hotovo", + outbox.
  • Operace přes víc služeb (objednávka → platba → sklad). Nejde jedna transakce → saga s kompenzacemi (selže sklad → vrať platbu).

3. Zoom na pasti, co junior nečeká

  • Brána potvrzuje asynchronně webhookem. Odpověď na POST ber jako „přijato ke zpracování", ne „zaplaceno". Skutečné potvrzení dorazí webhookem — ten ověř (podpis + replay) a zpracuj idempotentně.
  • Reconciliation (srovnání). Pravidelně porovnej svůj stav se stavem v bráně — síť i webhooky občas selžou a stavy se rozejdou. Bez tohohle ti tiše utíkají peníze.
  • Peníze jako celá čísla (haléře/centy) + měna (Correctness).
  • Audit log každé změny stavu (kdo, kdy, kolik) — u peněz povinnost, ne luxus.

Pointa kontrastu: u zkracovače URL šlo o škálu, tady o správnost. Návrh se netočí kolem QPS, ale kolem „jak zaručit, že se nikdy nestrhne dvakrát a nikdy se neztratí potvrzení" = idempotence + stavový automat + outbox + saga + reconciliation pohromadě.


✍️ Vzorový případ: Chat (kontrast — stateful a realtime)

1. Požadavky

  • Funkční: 1:1 i skupinové zprávy v reálném čase, historie, kdo je online (presence), doručení.
  • Nefunkční: nízká latence, perzistentní spojení (ne request/response na každou zprávu).

2. Jádro: stateful místo stateless

Na rozdíl od běžného API drží chat dlouhožijící obousměrné spojení (WebSocket z Realtime). Zpráva se uloží (historie) a rozešle příjemcům — a protože uživatelé jsou připojení na různých serverech, potřebuješ mezi nimi pub/sub backplane (Redis):

3. Zoom na pasti

  • Pořadí jen v rámci jedné konverzace — globální pořadí nepotřebuješ (a neškáluje). Řaď podle sekvence/času v dané konverzaci.
  • Doručení = at-least-once + ACK od klienta a dedup podle ID zprávy (při reconnectu může klient dostat duplikát).
  • Offline příjemce → ulož a pošli push notifikaci (Notifikace); doručíš při příštím připojení.
  • Presence (kdo je online) je distribuovaný stav přes všechny servery — drží se v Redisu, s expirací přes heartbeat.
  • Deploy → tisíce otevřených spojení řízeně rozpusti (connection draining), ne tvrdě zabít.

Pointa kontrastu: platby řešily správnost, feed škálu čtení — chat je o stavu a spojení (perzistentní WebSockety, backplane mezi servery, presence). Jiná osa problému.


📋 Další případy k procvičení

Pět případů už máš rozebraných výše (zkracovač URL, rate limiter, feed, platby, chat). Zkus si stejným postupem projít i tyhle — v závorce je hlavní téma, na které narazíš:

  • Notifikace(fronty, rate limit, retry, DLQ)
  • Úložiště souborů(rozdělení na kusy, metadata vs. samotný soubor, edge cache)
  • Našeptávač / vyhledávání(invertovaný index, prefixy, řazení výsledků, cache)
  • Rezervační systém(race condition na poslední místo, zámky, dočasná rezervace)

U každého začni požadavky + poměrem čtení ku zápisu — ten určí zbytek návrhu.


🛠️ Cvičení

  1. Projeď postup. Vezmi některý ještě nerozebraný případ (třeba našeptávač nebo notifikace) a projdi všech 7 kroků (požadavky → odhad → API → data → hrubý návrh → zoom → úzká hrdla). Nepřeskakuj na řešení.
  2. Hrubý odhad. Pro feed s 10 miliony uživatelů, každý 5 příspěvků denně, čtení 50× víc než zápis — odhadni QPS čtení a zápisu a denní objem dat. Co z toho plyne pro návrh?
  3. Poměr čtení/zápis. Pro tři systémy (zkracovač URL, banka, logovací služba) urči poměr a řekni, jak to změní návrh (cache + repliky vs. rozdělení dat + fronty).
  4. Dotáhni zkracovač URL. Doplň, jak bys počítal kliky, aniž bys zpomalil přesměrování (nápověda: asynchronní fronta).
  5. Rezervace. Pro rezervaci posledního místa navrhni, jak zabráníš dvojí rezervaci (zámek / atomický update / dočasná rezervace).
Náčrt řešení — rozbal, až si cvičení zkusíš sám
  1. Projeď postup — u našeptávače: požadavky (hledání podle prefixu, bleskové, silně read-heavy) → odhad → API (GET /suggest?q=) → data (prefixová struktura / invertovaný index) → cache populárních dotazů → úzké hrdlo je latence na každé písmeno. Pointa: postup je vždy stejný napříč zadáními. Pozor: nezačínej kreslením boxů, ale požadavky a poměrem čtení/zápis — ten určí zbytek.
  2. Hrubý odhad — 10M uživatelů × 5 příspěvků = 50M zápisů/den ÷ 86 400 ≈ ~580 zápisů/s; čtení 50× = ~29 000 čtení/s; objem dat = 50M × ~1 KB ≈ ~50 GB/den. Silně read-heavy → fan-out + cache. Pozor: počítej i špičku (×2–10).
  3. Poměr čtení/zápis — zkracovač URL: read-heavy (~100:1) → cache + repliky; banka: konzistence-kritická → silné transakce, s cache opatrně; logovací služba: write-heavy (append-only) → rozdělení dat + fronty/batch. Pozor: poměr určuje architekturu, ne to, co je zrovna „cool".
  4. Dotáhni zkracovač — přesměrování jen přečte z cache a hned vrátí redirect; o kliku pošli událost do fronty a agreguj na pozadí (Messaging). Pozor: nikdy nepiš do DB synchronně v cestě přesměrování — zpomalil bys to úplně zbytečně.
  5. Rezervace — atomický update UPDATE ... SET volna = volna - 1 WHERE volna > 0 (DB rozhodne nedělitelně), nebo pessimistic lock SELECT ... FOR UPDATE, nebo dočasná rezervace (zamluv místo na pár minut, po vypršení uvolni). Pozor: nikdy „přečti v aplikaci → rozhodni → zapiš" (klasický lost update).

🧠 Otázky & odpovědi

Proč začínat požadavky, a ne kreslením boxů?

Bez ujasnění funkčních i nefunkčních požadavků (kolik uživatelů, hlavně čtení nebo zápis, jaká odezva) navrhuješ naslepo — můžeš přeoptimalizovat na škálu, kterou nemáš, nebo minout to nejdůležitější. Požadavky určují celý zbytek: hodně čtení → cache + repliky, hodně zápisu → rozdělení dat + fronty. Vymezení rozsahu je půlka úspěchu.

K čemu je hrubý (back-of-envelope) odhad?

Rychlý odhad počtu requestů za sekundu a objemu dat ti řekne, jestli stačí jeden server, nebo potřebuješ cache, repliky či rozdělení dat — ještě než cokoli navrhneš. Hlavně poměr čtení ku zápisu určuje tvar řešení. Nejde o přesná čísla, ale o řád, který nasměruje rozhodnutí.

Proč u zkracovače URL nepočítat kliky synchronně?

Přesměrování musí být bleskové (čte se hodně, je to nejvytíženější cesta). Synchronní zápis počítadla kliků do databáze při každém přesměrování by ho zpomalil a databázi zatížil. Místo toho pošli o kliku zprávu do fronty a zpracuj ji asynchronně vedle — přesměrování odpoví hned, statistika se dopočítá zvlášť.

Jak vyřešíš rezervaci posledního místa bez dvojí rezervace?

Je to race condition na sdíleném zdroji. Možnosti: pessimistic lock (zamkni místo), atomický update (UPDATE ... SET volna_mista = volna_mista - 1 WHERE volna_mista > 0), nebo dočasná rezervace (zamluv místo na pár minut, po vypršení uvolni). Klíčové je, ať kontrolu a odečet udělá databáze v jednom atomickém kroku, ne aplikace ve dvou.

Existuje jedno správné řešení system design úlohy?

Ne. Hodnotí se, jestli vidíš trade-offy a umíš svoje volby obhájit konkrétním požadavkem — ne jestli jsi trefil „tu jedinou" architekturu. Dobrá odpověď často začíná „záleží na…" a pak vysvětlí, za jakých předpokladů volíš co. Špatná je dogmatické „použijeme X", protože je to populární.