Arhitectura datelor și a sistemelor, de la zero Lecția 3 / 80

Modelul C4: context, container, component, code

O convenție de diagramare cu patru niveluri de zoom care se potrivește cu conversațiile reale despre sisteme.

Dacă ai fost vreodată într-o ședință în care cineva merge la tablă, desenează o căsuță etichetată „sistemul”, apoi o căsuță mai mică în interior etichetată „API”, apoi încă zece căsuțe în jur, iar o oră mai târziu nimeni nu se mai înțelege asupra a ce trebuiau să discute, ai experimentat problema centrală a diagramelor de arhitectură. Diagramele nu sunt de fapt problema. Lipsa unei convenții comune pentru la ce nivel de zoom ești este problema.

Vorbim despre sistem ca un singur lucru într-un ecosistem mai larg? Sau despre unitățile deployabile din interiorul sistemului? Sau despre modulele din interiorul uneia dintre acele unități? Sau despre cod? Fiecare dintre astea e o conversație utilă. Amestecarea lor în aceeași diagramă e modul în care obții o tablă cu optzeci de căsuțe pe care nimeni nu le poate citi.

Modelul C4 al lui Simon Brown rezolvă asta. Este, în funcție de cum numeri, fie o contribuție extrem de modestă la ingineria software, fie o revoluție subapreciată. E o convenție. Patru niveluri standard de zoom pentru a vorbi despre un sistem. Fiecare nivel are un domeniu clar, un set clar de elemente legitime și o întrebare clară la care răspunde. Când echipa cade de acord să folosească convenția, conversațiile devin mai rapide, diagramele devin mai clare, iar tabla nu mai arată ca un Jackson Pollock.

Lecția asta trece prin cele patru niveluri, cu un exemplu lucrat pentru un mic produs SaaS și câteva opinii despre când să nu folosești C4.

Cele patru niveluri

C4 vine de la Context, Container, Component, Code. Fiecare e un nivel de zoom. Te muți de la cel mai larg (Context) la cel mai detaliat (Code), iar la fiecare nivel arăți doar elementele potrivite pentru acel zoom.

Nivelul 1: System Context. Sistemul pe care îl construiești, situat în lumea mai largă. Arată sistemul ca o singură căsuță, utilizatorii umani („actorii”) și sistemele externe de care sistemul tău depinde sau cu care se integrează. Audiența e largă: oricine din business, tehnic sau nu, ar trebui să poată citi această diagramă. Întrebarea la care răspunde e „ce e chestia asta și cu ce vorbește?”

Nivelul 2: Container. Face zoom în sistem din nivelul 1. Arată unitățile deployabile sau rulabile din interior: web frontend-ul, serviciul de API, baza de date, message broker-ul, procesul worker, cache-ul. „Container” aici e un termen Brown care precede Docker cu ani și se referă la „orice lucru care găzduiește cod sau date”, nu specific la un container Linux, deși în 2026 majoritatea containerelor în acest sens rulează în interiorul containerelor reale în sensul Docker. Audiența e tehnică: dezvoltatori, SRE, arhitecți. Întrebarea la care răspunde e „ce rulează unde și cum vorbesc lucrurile alea între ele?”

Nivelul 3: Component. Face zoom într-un container din nivelul 2. Arată modulele majore, serviciile sau grupările logice din interiorul acelui container. Pentru un serviciu de API, componentele ar putea fi auth, orders, products, billing. Pentru un frontend, componentele ar putea fi stratul de routing, contextul de auth, stratul de data fetching, design system-ul. Audiența e dezvoltatorii care lucrează la acel container. Întrebarea la care răspunde e „cum e organizat intern acest container?”

Nivelul 4: Code. Face zoom într-o componentă din nivelul 3 și arată clasele, interfețele sau funcțiile reale. Diagramă UML de clase, în esență, scoped la o singură componentă. Audiența e oricine implementează componenta. Întrebarea la care răspunde e „care sunt obiectele de cod efective implicate?”

Trucul lui C4 e că desenezi nivelurile de care ai nevoie și le sari pe cele de care nu. Majoritatea conversațiilor utile au loc la nivelurile 1, 2 și 3. Nivelul 4 e rareori merită desenat pentru că în momentul în care ești la nivelul de clasă, e aproape întotdeauna mai rapid să citești codul decât să menții o diagramă care se învechește în momentul în care cineva refactorizează. Brown însuși zice că nivelul 4 e opțional și majoritatea echipelor îl sar.

Celălalt aspect util: fiecare nivel îți permite să ai o conversație la acel nivel fără să tragi celelalte niveluri în ea. Când product owner-ul întreabă „sistemul se integrează cu Stripe?” arăți spre diagrama Context. Nu explici și că Stripe e apelat din bean-ul Spring BillingService din pachetul payments al containerului api. Nu le pasă. Vor să știe dacă Stripe e în peisaj. Diagrama Context spune da.

Exemplu lucrat: un mic produs SaaS

Hai să desenăm toate cele trei niveluri utile pentru un produs SaaS mic ipotetic. Hai să-i spunem ChartSmith, o unealtă online pentru desenat grafice. Utilizatorii se înregistrează, se loghează, construiesc grafice, le salvează și le exportă. Graficele sunt stocate într-o bază de date, exportate via un worker de background, facturate lunar prin Stripe, iar email-urile de confirmare ies prin SendGrid. E un frontend React, un API Node, o bază de date Postgres, un cache Redis și un worker Python de export în spatele unei job queue.

Acel paragraf e sistemul în engleză simplă (ei bine, română aici). Acum să vedem cum arată fiecare nivel C4.

Nivelul 1: System Context

Diagrama Context arată ChartSmith ca o singură căsuță, oamenii care îl folosesc și sistemele externe cu care se integrează.

flowchart LR
    user(["ChartSmith user"])
    admin(["ChartSmith admin"])
    chartsmith["<b>ChartSmith</b><br/><i>Web app for building<br/>and exporting charts</i>"]
    stripe["<b>Stripe</b><br/><i>Subscription payments</i>"]
    sendgrid["<b>SendGrid</b><br/><i>Transactional email</i>"]
    s3["<b>AWS S3</b><br/><i>Export storage</i>"]

    user --> chartsmith
    admin --> chartsmith
    chartsmith --> stripe
    chartsmith --> sendgrid
    chartsmith --> s3

    classDef person fill:#e6f4f1,stroke:#0d9488,color:#0c1419
    classDef sys fill:#0d9488,stroke:#0d9488,color:#ffffff
    classDef ext fill:#fff5e6,stroke:#c89200,color:#5a3e00
    class user,admin person
    class chartsmith sys
    class stripe,sendgrid,s3 ext

Observă ce nu e pe această diagramă. Nu se menționează baza de date, sau cache-ul, sau dacă API-ul e REST sau GraphQL, sau dacă frontend-ul e React. Acele detalii aparțin nivelului 2. Diagrama Context e pentru conversația „aici e cutia, aici e lumea, aici sunt liniile dintre ele”. E diagrama pe care o pui pe prima pagină a unui design doc, cea pe care o arăți unui stakeholder non-tehnic și cea care ar trebui să fie citibilă în cincisprezece secunde.

Actorii sunt oamenii. Căsuța sistemului e ce construim. Sistemele externe sunt dependențe pe care nu le controlăm. Trei dependențe externe pentru un mic SaaS e normal; un sistem real tipic are între trei și treizeci, în funcție de cât de matur e produsul.

Nivelul 2: Container

Facem zoom în căsuța ChartSmith și arătăm ce e înăuntru.

flowchart TB
    user(["ChartSmith user"])

    subgraph chartsmith ["ChartSmith"]
        direction TB
        spa["<b>Web app</b><br/><i>React, Vite</i>"]
        api["<b>API</b><br/><i>Node.js, Express</i>"]
        worker["<b>Export worker</b><br/><i>Python</i>"]
        db[("<b>Database</b><br/><i>PostgreSQL</i>")]
        cache[("<b>Cache</b><br/><i>Redis</i>")]
        queue["<b>Job queue</b><br/><i>Redis + BullMQ</i>"]
    end

    subgraph external ["External services"]
        direction TB
        stripe["<b>Stripe</b>"]
        sendgrid["<b>SendGrid</b>"]
        s3["<b>AWS S3</b>"]
    end

    user --> spa
    spa --> api
    api --> db
    api --> cache
    api --> queue
    queue --> worker
    api --> stripe
    api --> sendgrid
    worker --> s3

    classDef person fill:#e6f4f1,stroke:#0d9488,color:#0c1419
    classDef ext fill:#fff5e6,stroke:#c89200,color:#5a3e00
    classDef boundary fill:transparent,stroke:#0d9488,stroke-dasharray: 5 5
    class user person
    class stripe,sendgrid,s3 ext
    class chartsmith,external boundary

Asta e diagrama pe care dezvoltatorii și SRE-ii se ceartă. Arată unitățile deployabile reale, alegerile tehnologice pentru fiecare și protocoalele dintre ele. Șase containere e un sistem mic. Un SaaS de dimensiune medie va avea zece până la cincisprezece containere pe această diagramă. Un sistem enterprise mare va avea atât de multe încât îl împarți în mai multe diagrame Container, una per sub-sistem logic, și asta e ok.

Câteva convenții care merită notate. Bazele de date primesc forma de cilindru (ContainerDb în sintaxa Mermaid C4). Fiecare container are o etichetă, o tehnologie și o descriere de o linie. Fiecare săgeată e etichetată cu ce face și ce protocol folosește. Acele etichete par exagerate prima dată când desenezi diagrama și sunt esențiale a treia oară când o privești șase luni mai târziu întrebându-te de ce naiba API-ul vorbește cu Redis.

Diagrama Container e și locul unde poți citi topologia de deployment. Fiecare container din diagramă devine unul sau mai multe procese în producție. Web app-ul e servit dintr-un CDN. API-ul rulează ca o flotă de procese Node în spatele unui load balancer. Baza de date e un Postgres gestionat. Cache-ul e un Redis gestionat. Worker-ul e o flotă separată de procese Python. Nimic din astea nu e în diagramă explicit, dar totul e implicat de care căsuțe există.

Nivelul 3: Component

Facem zoom în containerul API și arătăm modulele majore din interior.

flowchart TB
    spa["Web app"]

    subgraph api ["API container"]
        direction TB
        router["<b>HTTP router</b><br/><i>Express, auth middleware</i>"]

        subgraph features ["Feature components"]
            direction LR
            auth["<b>Auth</b>"]
            charts["<b>Charts</b>"]
            exports["<b>Exports</b>"]
            billing["<b>Billing</b>"]
            notifications["<b>Notifications</b>"]
        end

        data["<b>Data access</b><br/><i>Postgres + Redis repos</i>"]
    end

    storage[("DB / Cache / Queue")]
    extsvc["Stripe / SendGrid"]

    spa --> router
    router --> auth
    router --> charts
    router --> exports
    router --> billing
    auth --> data
    charts --> data
    exports --> data
    billing --> data
    billing --> notifications
    auth --> notifications
    data --> storage
    exports --> storage
    notifications --> extsvc
    billing --> extsvc

    classDef ext fill:#fff5e6,stroke:#c89200,color:#5a3e00
    classDef external fill:#f0f4f8,stroke:#5a6a73,color:#0c1419
    classDef boundary fill:transparent,stroke:#0d9488,stroke-dasharray: 5 5
    class extsvc ext
    class spa,storage external
    class api,features boundary

Diagrama Component arată organizarea internă a API-ului. Șapte componente, fiecare cu o responsabilitate clară. Granițele se mapează pe foldere în repository, mai mult sau mai puțin. Dezvoltatorii noi care se alătură echipei API se uită la această diagramă înainte să se uite la cod, pentru că le spune unde să găsească lucrurile. „Unde e apelat Stripe?” Componenta Billing. „Unde e hashing-ul de parolă?” Componenta Auth. „Unde sunt query-urile SQL?” Componenta Data access, și doar acolo.

Convenția care își merită aici banii e că componentele ar trebui să aibă single responsibilities și ar trebui să comunice prin interfețe clare. Dacă desenezi diagrama Component și descoperi că fiecare componentă vorbește cu fiecare altă componentă, ăsta e un semn că containerul tău și-a pierdut structura internă, iar diagrama face muncă utilă scoțând asta la suprafață.

În mod tipic ai desena o diagramă Component pentru fiecare container major care e suficient de complex încât să o merite. Web app-ul ar putea primi propria diagramă Component arătând structura frontend-ului. Worker-ul de export ar putea primi una arătând pipeline-ul de rendering. Baza de date nu are nevoie de una. Componentele din interiorul unei baze de date nu prea există într-un mod despre care C4 vorbește.

Nivelul 4: Code, și de ce de obicei îl sărim

Nivelul 4 ar face zoom într-o componentă (să zicem componenta Auth) și ar arăta clasele reale: UserRepository, SessionService, PasswordHasher, LoginController și așa mai departe, cu relațiile dintre ele. Teritoriu de diagramă UML de clase.

În practică aproape nimeni nu desenează astea și aproape nimeni nu ar trebui. Motivul e că diagramele de nivel 4 sunt expirate în ziua în care le desenezi. Structurile de clase se schimbă în fiecare săptămână pe măsură ce echipa refactorizează. Menținerea unei diagrame UML de clase desenate manual e o taxă pe care nimeni nu se oferă voluntar să o plătească, iar diagramele de clase generate de IDE sunt corecte dar rareori utile pentru că arată fiecare clasă fără curatorialitate.

Excepția e când documentezi o bucată de cod deosebit de subtilă: o mașină de stări, un parser, un pattern care i-a luat cuiva trei zile să-l descifreze și i-ar lua același trei zile unui mentenancer viitor să-l re-deducă fără ajutor. În acele cazuri, o diagramă de nivel 4 își merită banii. Altfel, codul tău e diagrama de nivel 4. Citește-l.

Ghidarea proprie a lui Brown e că nivelurile 1 până la 3 acoperă aproximativ 95% din conversațiile arhitecturale utile. Eu am descoperit că asta e aproximativ corect.

Când să nu folosești C4

C4 nu e unealta potrivită pentru fiecare situație. Câteva cazuri în care convenția e exagerată sau pur și simplu greșită.

Sisteme foarte mici. O singură funcție Lambda susținută de DynamoDB înseamnă două căsuțe. Nu ai nevoie de o diagramă Context, o diagramă Container și o diagramă Component pentru două căsuțe. O singură schiță e suficientă.

Prototipuri foarte timpurii. Când încerci să decizi dacă o idee e măcar fezabilă, desenarea de diagrame C4 formale te încetinește. O schiță de pe spatele unui plic cu căsuțe scrise de mână e mai bună. C4 începe să-și merite banii odată ce sistemul are formă și sunt stakeholder-i cu care să comunici.

Diagrame specializate care au propriile lor convenții. Sequence diagrams, mașini de stări, diagrame de topologie de rețea, ERD-uri, diagrame de deployment, diagrame de dataflow, fluxuri de proces BPMN: toate astea au propriile lor convenții stabilite și C4 nu e un înlocuitor pentru ele. C4 acoperă structura. Nu acoperă comportament, forme de date sau procese. Folosește unealta potrivită pentru întrebare.

Echipe care au deja o convenție diferită. Dacă organizația ta a standardizat pe diagrame de componente UML 2, sau ArchiMate, sau o convenție internă pe care toată lumea o citește fluent, trecerea la C4 e mai ales cost social. Valoarea lui C4 e în mare parte că toată lumea din echipă îl folosește la fel, iar orice convenție consistentă bate o lipsă de convenție.

Regula generală: folosește C4 când explici un sistem non-trivial unor oameni care nu au fost în cameră când a fost proiectat. Sări peste el când audiența și artefactul nu justifică formalitatea.

Tooling

Câteva unelte care randează C4 bine în 2026.

Mermaid, în-markdown și suportat în majoritatea sistemelor moderne de documentație. Diagramele din lecția asta sunt Mermaid C4. Suportă C4Context, C4Container, C4Component și C4Dynamic. Render-ul e suficient de bun pentru majoritatea utilizărilor, cu margini brute ocazionale în layout. Marele avantaj: diagramele trăiesc în același git repo cu codul, version-controlled și revizuite în pull request-uri.

Structurizr, propria unealtă a lui Brown. Funcționalitatea definitorie e că scrii modelul o singură dată într-un DSL și generezi toate nivelurile C4 din aceeași sursă. Dacă schimbi un container, toate diagramele care îl includ se actualizează automat. Există o versiune găzduită și o imagine Docker self-hosted. E cea mai „C4-nativă” unealtă care există, și dacă urmează să investești profund în C4, merită s-o cunoști.

diagrams.net (anterior draw.io), cu stencil-urile C4 activate. Funcționează bine dacă deja folosești draw.io pentru orice altceva și vrei un editor vizual rapid. Mai puțin riguros decât Structurizr pentru că diagramele sunt desenate manual și modelul nu e impus nicăieri, dar bara de intrare e mai joasă.

PlantUML cu C4-PlantUML. Stabilit de mult timp, bazat pe text, se integrează bine cu toolchain-uri mai vechi. Puțin mai verbos decât Mermaid dar mai bogat în funcționalități în unele privințe. Folosit pe scară largă în setări enterprise care au investit deja în PlantUML pentru sequence diagrams și ERD-uri.

Pentru majoritatea echipelor aș recomanda să începi cu Mermaid pentru diagrame care trăiesc într-un repo de documentație (sweet spot-ul de „version-controlled, revizuibil, ceremonie scăzută”) și să avansezi la Structurizr dacă și când diagramele încep să simtă suficient de mult o entitate încât să le gestionezi ca un model în loc de un set de imagini devine merituos.

Diagramă de creat: Un poster de tip „C4 zoom levels” overview. Patru dreptunghiuri imbricate, fiecare etichetat cu nivelul (1 Context, 2 Container, 3 Component, 4 Code) și un subtitlu scurt (Sistemul în lume, Containerele din interiorul sistemului, Componentele din interiorul unui container, Clasele din interiorul unei componente). În dreapta fiecărui nivel, o etichetă „audiență” pe o linie: business stakeholders, developers și SREs, echipa care construiește containerul, implementatorul componentei. Folosește o culoare consistentă pentru fiecare nivel (sugerat: navy, teal, amber, slate). Titlu sus: „C4 model: four zoom levels for system diagrams.” Atribuirea sursei jos: „After Simon Brown, c4model.com.”

Cu ce ar trebui să rămâi din lecția asta

C4 e o convenție cu patru niveluri de zoom: Context (sistemul în lume), Container (unități deployabile), Component (module din interiorul unui container), Code (rareori desenat). Majoritatea diagramelor utile trăiesc la nivelurile 1 până la 3. Fiecare nivel are o audiență clară și răspunde la o întrebare clară. Cea mai mare victorie practică a folosirii C4 e că conversațiile nu derivă între niveluri de zoom, care e modul de eșec ce produce table ilizibile.

Folosește Mermaid în repository-ul tău de documentație ca cea mai puțin rezistentă cale. Folosește Structurizr dacă vrei o singură sursă de adevăr pentru toate diagramele. Sări peste C4 cu totul pentru sisteme foarte mici, prototipuri foarte timpurii sau diagrame specializate care au propriile convenții.

Lecția următoare, ne mutăm de la desenarea sistemelor la a gândi la ce rulează de fapt sub ele. Lecția 4 se uită la mașina singulară: OS-ul, modelul de proces, threads, async, file I/O și ce poate face o singură cutie înainte să ai nevoie de a doua. Maxima „scale up înainte să scale out” funcționează doar dacă știi ce-ți cumpără efectiv scaling up-ul, și acolo o să mergem.

Referințe

Caută