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

Architectural Decision Records (ADR-uri)

Cum capturezi deciziile cu alternativele și consecințele lor. Formatul Michael Nygard pe care s-au pus de acord majoritatea echipelor.

Peste șase luni, cineva din echipa ta o să arate cu degetul spre o bucată din sistem și o să întrebe: „de ce folosește serviciul de auth JWT în loc de sesiuni?” Sau: „de ce suntem pe Postgres și nu pe MongoDB?” Sau: „de ce e coada Redis și nu RabbitMQ, când deja aveam RabbitMQ în alt serviciu?”

Răspunsul cinstit, în majoritatea echipelor, e: nimeni nu-și mai amintește. Persoana care a luat decizia a plecat în 2024. A fost un thread lung pe Slack, dar a ieșit din free tier. A fost o ședință, dar notițele sunt în OneNote-ul privat al cuiva. A fost o pagină de wiki, dar a fost editată ultima oară acum doi ani și face referire la o companie care între timp și-a schimbat numele de două ori. Raționamentul s-a pierdut. Ce rămâne e decizia, stând în producție, făcându-și treaba, fără ca nimeni să mai poată explica de ce a fost alegerea corectă.

Asta e problema pe care o rezolvă ADR-urile. Architectural Decision Records sunt documente scurte, în text simplu, scrise în momentul deciziei, care capturează ce s-a decis, care erau alternativele și care vor fi consecințele. Trăiesc în repository, lângă codul pe care îl descriu. Nu sunt șterse când devin perimate; sunt marcate ca superseded, iar noul ADR face referință înapoi la cel vechi. Făcute bine, ADR-urile devin arhiva arheologică a sistemului tău: un inginer viitor le poate citi în ordine și înțelege cum a ajuns arhitectura unde e azi.

Lecția asta e despre formatul care a câștigat (al lui Michael Nygard, din 2011), ce să pui într-un ADR, când să scrii unul și cum arată ADR-urile publice reale. E scurtă pe teorie și lungă pe „uite ce faci concret marți dimineața când trebuie luată o decizie.”

Formatul Nygard

În noiembrie 2011, Michael Nygard, autorul Release It! și consultant de mult timp pentru companii care existau de suficient timp ca să-și regrete arhitecturile, a scris o postare pe blog intitulată Documenting Architecture Decisions. Postarea are sub 1.000 de cuvinte. Propunea un format cu cinci secțiuni:

  1. Title. O frază nominală scurtă. „Folosim Postgres pentru serviciul de comenzi.”
  2. Status. Proposed, accepted, deprecated sau superseded by ADR-0017.
  3. Context. Care e situația, care sunt forțele în joc, de ce e nevoie de o decizie.
  4. Decision. Ce o să facem, la diateza activă. „Vom folosi Postgres.”
  5. Consequences. Ce devine mai ușor, ce devine mai greu, la ce ne-am angajat.

Atât. Întregul format e explicat în 30 de rânduri în postarea originală. Aproape orice template ADR pe care-l vezi azi în sălbăticie e formatul ăsta cu variații cosmetice: cineva a adăugat o secțiune „Alternatives Considered”, altcineva a împărțit Consequences în „Positive” și „Negative”, altcineva a adăugat un footer „Related decisions”. Scheletul e al lui Nygard.

Formatul funcționează datorită a ceea ce omite deliberat. Nu există rezumat executiv. Nu există matrice de risc. Nu există RACI chart. Nu există roadmap. Un ADR nu e un document de proiect; e un document de decizie. Înregistrează o singură decizie, doar una, în aproximativ una sau două pagini de Markdown obișnuit. Orice e mai mare e un alt artefact.

Status

Statusul e un singur cuvânt sau o frază scurtă. Ciclul de viață e:

  • Proposed: cineva a scris ADR-ul, e supus revizuirii, echipa nu s-a angajat încă.
  • Accepted: echipa a fost de acord; decizia e în vigoare.
  • Deprecated: decizia nu mai e respectată, dar încă nu există o înlocuitoare.
  • Superseded by ADR-NNNN: un ADR mai nou îl înlocuiește pe acesta. ADR-ul vechi rămâne în repo; doar actualizezi linia de status și adaugi o referință înapoi.

Punctul crucial e că ADR-urile sunt append-only. Nu ștergi ADR-uri superseded. Nu le editezi pe loc ca să reflecte noua decizie. Scrii un ADR nou, le legi pe cele două și-l lași pe cel vechi acolo ca înregistrare. Peste șase luni, când cineva întreabă „stai, nu foloseam Postgres?”, poate citi ADR-0007 („Use Postgres for the order service”, Superseded by ADR-0034), apoi ADR-0034 („Move order service to DynamoDB”), și vede tot arcul.

Context

Secțiunea de context e acolo unde stă cea mai mare parte a valorii. Descrie lumea la momentul în care s-a luat decizia: ce construiam, care erau constrângerile, la ce ne angajaserăm deja în altă parte, de ce ne era frică. Tu cel din viitor nu-ți vei aminti nimic din asta. Tu cel din viitor vei vedea sistemul de producție așa cum stă și nu vei avea nicio idee ce trade-off era navigat.

Secțiunile bune de context numesc forțele explicit. „Ne așteptăm la aproximativ 50 de scrieri pe secundă și 500 de citiri pe secundă în primul an, scalând la poate de 10 ori mai mult până în anul trei. Avem un inginer cu experiență profundă pe Postgres și niciunul care să fi rulat MongoDB în producție. Echipa de compliance cere să putem indica audit logs la nivel de rând.”

Decision

Diateză activă, timp prezent, un paragraf. „Vom folosi Postgres 16, găzduit pe RDS, cu un primary unic și o read replica într-o altă availability zone.”

Nu „Postgres pare o alegere bună.” Nu „echipa înclină spre Postgres.” Secțiunea de decizie se angajează.

Consequences

Consecințele sunt secțiunea cinstită. Fiecare decizie închide niște uși și deschide altele. A explicita consecințele negative nu e pesimism; e singura cale prin care următoarea persoană poate evalua dacă trade-off-ul mai e valid.

„Ne angajăm să rulăm Postgres în producție, ceea ce înseamnă că inginerii noștri de on-call trebuie să învețe preocupări operaționale Postgres: vacuum, replication lag, connection pooling. Renunțăm la flexibilitatea per-document a unui document store; dacă avem nevoie să adăugăm câmpuri frecvent, vom plăti pentru migrări. Beneficiem de garanții tranzacționale puternice și de un limbaj de query pe care echipa îl știe deja.”

Unde trăiesc ADR-urile

ADR-urile merg în repository, lângă codul pe care îl descriu. Calea convențională e docs/adr/ sau docs/architecture/decisions/. Numele de fișiere sunt numerotate: 0001-record-architecture-decisions.md, 0002-use-postgres-for-orders.md, 0003-jwt-for-auth.md. Primul ADR e aproape întotdeauna cel care spune „o să folosim ADR-uri”, ceea ce sună recursiv, dar e cu adevărat util: angajează echipa la practica respectivă și le spune contributorilor viitori unde să se uite.

Markdown obișnuit. Nu e nevoie de unelte speciale. Există un mic instrument numit adr-tools, un shell script care creează ADR-uri noi dintr-un template și gestionează numerotarea, și există echivalente specifice pentru Node, Python și Go. Folosește unul dacă-ți place, sau doar copiază fișierul ADR precedent și incrementează numărul. Formatul e ieftin ca pământul, intenționat.

PR-urile care introduc schimbări arhitecturale semnificative ar trebui să includă ADR-ul în același commit. A revizui ADR-ul înseamnă a revizui designul. Dacă echipa nu e de acord cu decizia, ADR-ul e revizuit înainte ca codul să intre. Asta e una dintre puținele practici de documentație care chiar țin documentația sincronizată cu codul, pentru că documentația e parte din code review.

Când să scrii un ADR

Regula generală care funcționează în practică: scrie un ADR pentru orice decizie care a avut nevoie de un thread Slack ca să se decidă. Dacă doi ingineri rezonabili pot fi în dezaverdere, iar echipa a trebuit să discute asta mai mult de 15 minute, decizia merită înregistrată. Dacă răspunsul e evident pentru toată lumea, sari peste.

Lucruri care de obicei merită un ADR:

  • Alegerea bazei de date, a cozii, a cache-ului sau a oricărei infrastructuri cu un ciclu lung de înlocuire.
  • Modelul de autentificare și autorizare (sesiuni vs JWT vs alegerea fluxului OAuth).
  • Decizii de delimitare a serviciilor: „acesta e un serviciu” vs „acestea sunt două servicii”.
  • Contracte API publice: REST vs gRPC vs GraphQL.
  • Preocupări transversale: format de logging, strategie de tracing, convenții de error-handling.
  • Decizii de build și deploy: monorepo vs polyrepo, provider de CI, container registry.

Lucruri care nu:

  • Nume de variabile, organizarea fișierelor în interiorul unui serviciu, alegeri de formatare.
  • Alegeri de biblioteci mici care sunt ușor de schimbat (care parser JSON, care bibliotecă de date).
  • Orice ce e capturat de un linter sau de un code review.

Riscul într-o echipă tânără e să scrie prea multe; riscul într-o echipă matură e să scrie prea puține. Ambele eșuează la fel: nimeni nu le mai citește. Țintește între cinci și douăzeci de ADR-uri în primul an al unui proiect, crescând încet după aia.

ADR-uri publice reale

Mai multe proiecte mari își publică ADR-urile public. Merită citite atât ca exemple de format, cât și ca ferestre spre cum raționează echipele serioase despre trade-off-uri.

Backstage, platforma open-source de developer-portal de la Spotify, are peste 30 de ADR-uri publice care acoperă decizii precum „use Lerna for monorepo management” (mai târziu superseded), „expose the React component for breadcrumbs” și „version-control plugin metadata in YAML.” Formatul e exact al lui Nygard, cu adăugiri ușoare. Citește ADR-001 („Architecture Decisions”) și ADR-005 („Catalog Core Entities”) ca să vezi stilul casei.

Arc42 menține un site comunitar cu un catalog mare de exemple și template-uri de ADR-uri, inclusiv alternative la formatul Nygard (formatul MADR adaugă o secțiune „Alternatives Considered” ca bloc de prim rang, pe care aș recomanda-o). E cel mai apropiat lucru pe care comunitatea ADR îl are de o referință canonică.

ThoughtWorks Technology Radar nu e un repository de ADR-uri, dar e cel mai apropiat document public la scară mare de „lucruri pe care practicienii noștri cred că ar trebui să le adopți, să le testezi, să le evaluezi sau să le ții în loc.” E în esență fluxul colectiv de ADR-uri al industriei pentru decizii de tooling, și e actualizat de două ori pe an.

Dacă citești un singur ADR extern înainte să-l scrii pe primul al tău, citește ADR-001 al Backstage. E scurt, e angajamentul proiectului de a ține ADR-uri și arată formatul folosit la furie.

Un exemplu lucrat

Iată cum arată un ADR pentru o echipă fictivă care tocmai a decis să folosească Postgres pentru serviciul lor de comenzi. Ăsta e întregul fișier; nu există altă documentație a deciziei.

# ADR-0007: Use Postgres for the order service

## Status

Accepted, 2025-10-28.

## Context

The order service stores customer orders, line items, payments, and
fulfilment status. Expected load in year one is ~50 writes/sec and
~500 reads/sec, growing to roughly 10x that by year three. The data
is highly relational: orders have many line items, line items
reference products and pricing, payments reference orders and
refund chains.

The team has three engineers with deep Postgres experience and
nobody with MongoDB in production. Our finance partners require
row-level audit logs and ACID guarantees on payment state
transitions. The platform already runs Postgres for the user
service, so we have a managed RDS pattern, backup tooling, and
on-call runbooks for it.

We considered Postgres, MongoDB, and DynamoDB.

## Decision

We will use Postgres 16 on AWS RDS for the order service. One
primary in eu-central-1a, one read replica in eu-central-1b for
analytics queries and failover. PgBouncer in front for connection
pooling. Schema migrations via Flyway, run on deploy.

## Consequences

Positive:
- Strong transactional guarantees on payment state changes.
- Reuse of existing operational tooling, backups, alerts.
- Team can debug query plans on day one.
- SQL is a known quantity for analytics and ad-hoc queries.

Negative:
- We commit to schema migrations for every new field. We will
  pay this cost on every release.
- Single-region primary; cross-region writes are not supported.
  If we ever need EU + US write locality, we will need to revisit.
- We are now responsible for index design. A bad query at 10x
  load will hurt.

Alternatives considered:
- MongoDB: rejected. Team has no operational experience, and the
  schema flexibility benefit is not worth the audit-log cost.
- DynamoDB: rejected. Single-table design would constrain the
  analytics team, and we would still need a separate relational
  store for reporting.

Ăsta e tot artefactul. Un singur fișier markdown, în docs/adr/0007-use-postgres-for-orders.md, commit-uit alături de prima migrare care creează tabelele de comenzi. Peste doi ani, când cineva întreabă de ce comenzile sunt pe Postgres, ăsta e răspunsul.

Ciclul de viață

Iată ciclul de viață al ADR-urilor ca diagramă:

flowchart LR
    A[Proposed] --> B[Accepted]
    A --> X[Rejected]
    B --> C[Deprecated]
    B --> D[Superseded by ADR-NNNN]
    C --> D

Majoritatea ADR-urilor trec de la Proposed la Accepted într-un singur PR review și apoi stau în Accepted pentru totdeauna, făcându-și treaba. Un număr mic ajung superseded pe măsură ce sistemul crește. Câteva sunt deprecate fără înlocuire, de obicei pentru că lucrul despre care s-a luat decizia nu mai există.

Cimitirul de ADR-uri superseded e partea pe care majoritatea echipelor o găsesc ne-intuitivă. Instinctul e să-l ștergi pe cel vechi pentru că e „greșit acum.” Rezistă instinctului. ADR-ul vechi e înregistrarea istorică a motivului pentru care echipa gândea diferit în 2024, iar lanțul de supersesiuni e o hartă a felului în care a evoluat arhitectura. Când un inginer nou întreabă „de ce suntem așa?”, lanțul ăla e răspunsul.

Ce nu sunt ADR-urile

ADR-urile nu sunt specificații. Nu descriu în detaliu cum e construit serviciul de comenzi; descriu o decizie despre el. Designul complet trăiește în cod, în docs API, în runbooks. ADR-ul capturează doar alegerea care a contat la momentul respectiv.

ADR-urile nu sunt contracte. Echipa are voie să se răzgândească și să înlocuiască un ADR anterior cu unul nou. Ușurința formatului e ce face asta practic: a rescrie un design doc de 50 de pagini când se schimbă baza de date e de neimaginat, dar a scrie ADR-0034 care îl supersede pe ADR-0007 e o muncă de marți după-amiază.

ADR-urile nu sunt planuri de proiect. Nu au programe, owneri sau bugete. Dacă ADR-urile tale capătă astfel de secțiuni, le transformi încet în altceva, iar formatul va înceta să mai funcționeze.

Începând de mâine

Iată kit-ul de start pentru o echipă care n-a scris niciodată un ADR:

  1. Creează docs/adr/ în repo-ul principal.
  2. Copiază template-ul Nygard în 0001-record-architecture-decisions.md și adaptează-l: „vom înregistra deciziile arhitecturale folosind ADR-uri în formatul Nygard.”
  3. Următoarea dată când izbucnește o ceartă arhitecturală reală pe Slack, scrie 0002-<orice-a-fost>.md și pune-l în thread.
  4. Adaugă un checkbox la PR template-ul tău: „Dacă acest PR ia o decizie arhitecturală, există un ADR?”

Ăsta e tot onboarding-ul. Formatul e suficient de mic încât o echipă îl adoptă într-o după-amiază, iar după un an de utilizare arhiva de decizii devine unul dintre documentele cele mai citite din repository.

Lecția următoare, ne mutăm de la „cum să înregistrezi deciziile” la „de ce fiecare decizie e un trade-off și care apar cel mai des în meseria asta.”

Caută