System design la interviu: un cadru pas cu pas
Un cadru în șase pași pentru interviul de system design, aplicat pe un serviciu de scurtare a link-urilor, plus conceptele esențiale.
Interviul de system design te sperie probabil mai mult decât cel de algoritmică, pentru că nu are un singur răspuns corect. Tocmai asta e ideea: intervievatorul vrea să vadă cum gândești când problema e vagă, mare și plină de compromisuri. Ghidul acesta îți dă un cadru în șase pași, aplicat pe un exemplu complet, plus conceptele pe care trebuie să le stăpânești.
Ce evaluează, de fapt, intervievatorul
Un interviu de system design durează de obicei între 45 și 60 de minute și pornește de la o cerință scurtă, de exemplu „proiectează un serviciu de scurtare a link-urilor”. Nimeni nu se așteaptă să construiești un sistem real în acest timp. Se urmăresc, în schimb, câteva lucruri:
- Clarificarea problemei: pui întrebări înainte să desenezi?
- Structura: abordezi problema ordonat sau sari de la o idee la alta?
- Compromisurile: știi să explici de ce alegi o soluție și ce pierzi prin alegerea ei?
- Cunoștințele de bază: înțelegi baze de date, caching, cozi de mesaje, load balancing?
- Comunicarea: gândești cu voce tare și lucrezi cu intervievatorul, nu doar în fața lui?
Pentru pozițiile de junior, system design apare mai rar sau într-o formă simplificată. Pentru mid și senior, e aproape mereu o rundă separată.
Cadrul în șase pași
Iată o împărțire orientativă a timpului pentru un interviu de 45 de minute:
| Pas | Ce faci | Timp aproximativ |
|---|---|---|
| 1. Cerințe | Clarifici funcționalitățile și constrângerile | 5 min |
| 2. Estimări | Calcule rapide de trafic și stocare | 3–5 min |
| 3. API și date | Definești endpoint-urile și modelul de date | 5 min |
| 4. Design de ansamblu | Desenezi componentele principale și fluxul datelor | 10 min |
| 5. Aprofundare | Detaliezi 1–2 componente critice | 10–15 min |
| 6. Concluzii | Rezumi, discuți punctele slabe și extinderile | 3–5 min |
Exemplul pe care îl vom folosi: un serviciu de scurtare a link-urilor, de tipul celor care transformă un URL lung într-unul scurt.
Pasul 1: clarifică cerințele
Nu începe niciodată să desenezi imediat. Primele minute le petreci punând întrebări și notând răspunsurile.
Cerințe funcționale (ce face sistemul):
- Utilizatorul trimite un URL lung și primește un cod scurt.
- Accesarea link-ului scurt redirecționează către URL-ul original.
- Opțional: alias personalizat, expirare, statistici de accesare.
Cerințe non-funcționale (cum trebuie să se comporte):
- Disponibilitate ridicată: un link scurt care nu funcționează strică experiența oricui l-a primit.
- Latență mică la redirecționare.
- Codurile nu trebuie să fie ușor de ghicit în ordine.
Întrebări bune de pus: „Câți utilizatori estimăm?”, „Raportul dintre citiri și scrieri?”, „Link-urile expiră?”, „Avem nevoie de statistici în timp real?”. Stabilește explicit ce nu intră în discuție, de exemplu autentificarea sau panoul de administrare.
Pasul 2: estimări rapide
Estimările (back-of-the-envelope) te ajută să alegi tehnologiile potrivite. Ordinul de mărime contează, nu precizia. Presupunem, împreună cu intervievatorul:
- 100 de milioane de link-uri noi pe lună.
- Raport citiri/scrieri de 100:1.
- Datele se păstrează 5 ani, cu aproximativ 500 de bytes pe înregistrare.
Calculele:
O zi ≈ 86.400 s ≈ 10^5 s; o lună ≈ 2,6 milioane de secunde
Scrieri: 100.000.000 / 2.600.000 ≈ 40 scrieri/s
Citiri: 40 × 100 ≈ 4.000 citiri/s (cu vârfuri mai mari)
Înregistrări în 5 ani: 100 mil. × 12 × 5 = 6 miliarde
Stocare: 6 × 10^9 × 500 B = 3 × 10^12 B ≈ 3 TB
Concluzia contează mai mult decât cifrele: sistemul este dominat de citiri, deci caching-ul va fi esențial, iar 3 TB încap fără probleme într-o bază de date bine configurată, eventual partiționată.
Și lungimea codului: cu 62 de caractere posibile (a-z, A-Z, 0-9), un cod de 7 caractere oferă 62^7 ≈ 3,5 × 10^12 combinații (peste 3.500 de miliarde), cu mult peste cele 6 miliarde necesare.
Pasul 3: API și modelul de date
Definește contractul înainte de componente:
POST /api/v1/urls
Body: { "url": "https://exemplu.ro/un/drum/foarte/lung" }
Răspuns: 201 { "code": "aZ3k9Qx", "shortUrl": "https://scurt.ro/aZ3k9Qx" }
GET /{code}
Răspuns: 302 Location: https://exemplu.ro/un/drum/foarte/lung
Aici apare un compromis interesant: 301 sau 302? Un redirect 301 (permanent) poate fi păstrat în cache de browser, deci reduce încărcarea serverelor, dar nu mai vezi accesările repetate. Un 302 (temporar) trimite fiecare accesare prin serviciul tău, ceea ce permite statistici. Menționează compromisul și alege în funcție de cerințe.
Modelul de date este simplu:
CREATE TABLE urls (
code VARCHAR(10) PRIMARY KEY,
url_lung TEXT NOT NULL,
creat_la TIMESTAMP NOT NULL,
expira_la TIMESTAMP
);
Accesul se face aproape exclusiv după cheie (code), fără relații complexe, deci funcționează la fel de bine o bază de date relațională sau un key-value store. Spune asta explicit și justifică alegerea.
Pasul 4: designul de ansamblu
Desenează componentele principale și drumul unei cereri:
┌──────────────────┐
Client ──► Load ──► │ Servicii API │ ──► Cache (Redis)
balancer │ (stateless) │ │ miss
└──────────────────┘ ▼
│ Bază de date
▼ (replici de citire)
Generator de coduri
- Load balancer: distribuie cererile între mai multe instanțe.
- Servicii API stateless: nu păstrează stare locală, deci poți adăuga instanțe după nevoie (scalare orizontală).
- Cache: păstrează în memorie link-urile populare. La 4.000 de citiri pe secundă, majoritatea cererilor nu ar trebui să ajungă la baza de date.
- Bază de date cu replici de citire: scrierile merg la nodul principal, citirile se pot distribui pe replici.
- Generator de coduri: produce coduri unice, fără coliziuni.
Explică fluxul ambelor operații. Creare: API → generator → bază de date. Redirecționare: API → cache → (la miss) bază de date → populare cache → răspuns 302.
Pasul 5: aprofundare
Intervievatorul va alege de obicei una sau două zone. Pregătește-te pentru cele mai probabile.
Cum generezi codurile?
| Abordare | Cum funcționează | Avantaje | Dezavantaje |
|---|---|---|---|
| Hash pe URL | Primele 7 caractere dintr-un hash al URL-ului | Simplu; același URL dă același cod | Coliziuni care trebuie detectate și rezolvate |
| Contor + base62 | Un ID numeric unic, codat în base62 | Fără coliziuni | Coduri previzibile; contorul trebuie distribuit |
| Chei pregenerate | Un serviciu generează dinainte coduri aleatoare unice | Rapid la scriere, coduri imprevizibile | Încă o componentă de întreținut |
Pentru contor distribuit, fiecare server poate primi un interval de ID-uri (de exemplu, câte un milion odată), astfel încât să nu fie nevoie de coordonare la fiecare cerere.
Caching
Strategia naturală este cache-aside: aplicația caută întâi în cache, iar la miss citește din baza de date și populează cache-ul. Politica de evacuare LRU (Least Recently Used) păstrează link-urile populare. Discută și invalidarea: ce se întâmplă când un link expiră sau e șters?
Scalarea bazei de date
Când un singur nod nu mai ajunge, partiționezi datele (sharding) după code, de exemplu folosind consistent hashing, ca adăugarea unui nod nou să mute cât mai puține date.
Statistici fără să încetinești redirecționarea
Nu scrie sincron în baza de date la fiecare accesare. Publică un eveniment într-o coadă de mesaje (de exemplu Kafka sau RabbitMQ) și procesează statisticile asincron, separat de drumul critic.
Pasul 6: concluzii
În ultimele minute, rezumă designul în câteva propoziții și arată singur punctele slabe: ce se întâmplă dacă pică cache-ul, cum previi abuzul (rate limiting pe crearea de link-uri, verificarea URL-urilor malițioase), cum ai monitoriza sistemul. Intervievatorii apreciază candidații care își critică propriul design.
Conceptele pe care trebuie să le stăpânești
| Concept | Ce trebuie să știi |
|---|---|
| Scalare verticală vs. orizontală | O mașină mai puternică vs. mai multe mașini |
| Load balancing | Algoritmi (round robin, least connections), health checks |
| Caching | Cache-aside, write-through, TTL, invalidare, CDN |
| Replicare | Nod principal și replici, întârzierea replicării |
| Sharding | Chei de partiționare, hot spots, consistent hashing |
| Teorema CAP | La o partiție de rețea, alegi între consistență și disponibilitate |
| SQL vs. NoSQL | Tranzacții și relații vs. scalare și scheme flexibile |
| Cozi de mesaje | Procesare asincronă, decuplare, retry |
| Rate limiting | Token bucket, sliding window |
| Indexuri | Când accelerează citirile și cât costă la scriere |
Greșeli frecvente
- Desenezi înainte să clarifici cerințele și rezolvi altă problemă decât cea cerută.
- Over-engineering: microservicii, Kubernetes și trei baze de date pentru un sistem cu 40 de scrieri pe secundă.
- Buzzwords fără justificare: „pun Kafka aici” fără să spui de ce.
- Tăcerea: intervievatorul nu-ți poate evalua gândirea dacă nu o aude.
- Ignorarea indiciilor: când intervievatorul întreabă „ce se întâmplă dacă…?”, de obicei te îndreaptă spre o problemă reală din design.
Cum te pregătești
- Învață conceptele de bază, câte unul pe rând, cu exemple concrete.
- Exersează probleme clasice: scurtarea link-urilor, un feed de social media, un chat, un sistem de notificări, un rate limiter, un serviciu de stocare de fișiere.
- Rezolvă fiecare problemă cu voce tare și cu cronometrul pornit, urmând cei șase pași.
- Fă interviuri simulate cu un prieten sau un coleg. Feedback-ul extern e de neînlocuit.
- Citește despre arhitectura sistemelor reale: multe companii publică articole tehnice despre cum și-au scalat produsele. Un punct de plecare bun și gratuit este System Design Primer.
Pașii următori
- Parcurge roadmap-ul System Design pentru toate conceptele din tabelul de mai sus, în ordinea potrivită.
- Pregătește în paralel runda de algoritmică cu roadmap-ul DSA pentru interviuri.
- Exersează o problemă cu tutorul AI: cere-i să joace rolul intervievatorului, să-ți pună întrebări de clarificare și să-ți dea feedback pe design.