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

Polyglot persistence: când să amesteci

Când aplicația ta beneficiază de mai multe baze de date, când una e suficientă și costul operațional al rulării a patru data stores în loc de unul.

Asta e ultima lecție din Modulul 3. În ultimele opt lecții am parcurs categoriile majore de data store: relațional, document, key-value, columnar, graf, time-series, vectorial. Fiecare categorie rezolvă o clasă de query pe care celelalte o tratează stângaci. Fiecare categorie are un ecosistem sănătos de produse. Și fiecare dintre ele, luată singură, e tentantă să o adaugi în stack-ul tău în momentul în care întâlnești un workload pe care l-ar trata bine.

Lecția asta e răspunsul la „ar trebui să adaug încă o bază de date?”, iar răspunsul e, surprinzător de des, „nu, dar citește mai departe.” Termenul pentru pattern-ul de design al rulării mai multor data stores specializate într-o singură aplicație e polyglot persistence, și a petrecut ultimii cincisprezece ani oscilând între la modă și regretat. Unde se află în 2026 și cum să decizi dacă e potrivit pentru tine, asta e tema.

De unde a venit termenul

Martin Fowler a inventat „polyglot persistence” în 2011 într-un scurt post bliki (https://martinfowler.com/bliki/PolyglotPersistence.html). Argumentul era direct: bazele de date relaționale fuseseră default-ul universal timp de treizeci de ani, dar ascensiunea store-urilor specializate însemna că o aplicație putea să aleagă uneltele potrivite pentru fiecare subset al datelor sale. Folosește baza de date relațională pentru date tranzacționale, un key-value store pentru sesiuni, un motor de căutare pentru query-uri full-text, o bază de date graf pentru relații, un column store pentru analiză. Fiecare parte a sistemului folosește stocarea care se potrivește, iar întregul se compune în ceva mai performant decât ar putea fi orice store unic.

Diagrama din postul lui Fowler a devenit iconică: o aplicație de e-commerce cu șapte cutii diferite, fiecare etichetată cu o bază de date diferită, toate conectate la același strat de aplicație. Părea elegantă. Părea ce voia 2011 să fie viitorul.

Anii care au urmat ne-au învățat că diagrama îi lipsea cea mai importantă cutie: echipa. Fiecare store suplimentar e încă un lucru pe care echipa ta trebuie să-l învețe, monitorizeze, facă backup, upgradeze și debugheze la trei dimineața, iar acele costuri nu se adună liniar. Se compun.

Promisiunea: o unealtă perfectă pentru fiecare treabă

Cazul pentru polyglot persistence e cazul pe care l-am făcut implicit pe parcursul Modulului 3. Pattern-urile de query sunt diferite. O traversare de graf e stângace în SQL. Căutarea full-text e lentă fără un inverted index. Agregările pe time series vor un column store. Sesiunile vor un lookup key-value in-memory cu latență mică. Căutarea vectorială vrea HNSW. Dacă construiești toate astea peste o singură bază de date relațională, faci lucruri pentru care baza de date nu e optimizată și într-un final atingi un plafon de performanță pe care unealta potrivită l-ar fi ridicat ușor.

Într-o lume perfectă, ai avea o bază de date per pattern de query, fiecare reglată pentru treaba ei, fiecare scalând independent. Aplicația ar fi un strat subțire care rutează fiecare cerere către store-ul potrivit. Cerințe noi ar însemna o nouă bază de date. Arhitectura s-ar compune frumos.

Acea lume există, în slide deck. În producție, are costuri pe care slide deck-ul nu le arată.

Realitatea: fiecare store îți dublează suprafața operațională

Fiecare bază de date pe care o adaugi e un angajament de un ordin de mărime, nu unul procentual. Achiziționezi, cu noul store: o strategie de backup și restore pe care cineva a testat-o efectiv; o poveste de monitorizare cu metrici, praguri, dashboard-uri și reguli de pager; un plan de upgrade, pentru că versiunile majore sparg lucruri și baza de date pe care ai adoptat-o la versiunea 5 va fi la versiunea 9 în trei ani; o povară de antrenament on-call, pentru că atunci când baza de date se comportă urât la 3 dimineața cineva trebuie s-o cunoască suficient de bine ca s-o diagnosticheze; o poveste de disaster recovery cu RTO și RPO măsurate; și o problemă de sincronizare, pentru că datele care trăiesc în două locuri se desincronizează.

Euristica brută pe care o folosesc e: fiecare nou sistem de persistență dublează aproximativ suprafața on-call pentru echipa care îl deține. Două baze de date înseamnă de două ori muncă mai multă decât una. Trei înseamnă de patru ori. Exponentul nu e precis dar direcția e. O echipă mică cu trei baze de date face mai multă muncă operațională decât aceeași echipă cu una, iar acea muncă e luată din timpul pe care altfel l-ar petrece livrând funcționalități.

Poziția default 2026: începe cu un singur Postgres

Ce s-a schimbat de când Fowler și-a scris postul e cât de mult poate face o singură bază de date relațională modernă. Postgres în 2026 nu e Postgres-ul din 2011. Aceeași instanță care servește workload-ul tău tranzacțional poate gestiona competent și:

  • Căutare full-text, via tsvector, tsquery și indexuri GIN. Performanța e bună până la milioane de documente, iar căutarea e tranzacțională cu datele subiacente, ceea ce Elasticsearch nu poate oferi.
  • Stocare de documente, via jsonb. Operatorii (->, ->>, @>, ?) și indexurile GIN pe căi JSON îți dau o bază de date document în interiorul Postgres, cu schema relațională pentru părțile pe care le vrei structurate și JSON pentru părțile care variază.
  • Căutare vectorială, via extensia pgvector acoperită în lecția precedentă: indexuri HNSW, distanțe cosine și L2 și dot-product, query-uri hibride care combină similaritatea vectorială cu filtre relaționale.
  • Time-series, via TimescaleDB sau hypertables native partiționate. Hypertables, compresie, agregate continue, politici de retenție. Competitiv cu store-urile dedicate din lecția 22 pentru majoritatea workload-urilor.
  • Traversare de graf, via extensia Apache AGE, care adaugă query-uri în stil Cypher peste tabele Postgres. Pentru cazuri mai simple, CTE-urile recursive (WITH RECURSIVE) te duc majoritatea drumului.
  • Query-uri geografice, via PostGIS: baza de date GIS open-source de standard auriu, rulând ca extensie Postgres.

Acea listă înseamnă că, în 2026, întrebarea „ar trebui să adaug un motor de căutare, o bază de date vectorială, o bază de date time-series și o bază de date graf” are același răspuns pentru multe echipe: nu încă. Rulează workload-urile pe Postgres mai întâi. Când depășești Postgres pentru unul dintre ele specific, extrage doar pe acela. Restul rămân unde sunt. E aceeași lecție ca tema „începe mai simplu” din Modulul 1, aplicată infrastructurii de date: default-urile lui 2026 sunt mult mai capabile decât default-urile lui 2011, iar prescripția arhitecturală trebuie să se actualizeze corespunzător.

Pattern-uri unde polyglot e inevitabil

Există workload-uri unde Postgres genuin nu e suficient și a doua bază de date își câștigă locul.

Căutare la scară. tsvector din Postgres e bun până la un punct. Peste zeci de milioane de documente, cu ranking de relevanță complex, navigare cu fațete, autocomplete, analizatori multi-limbă și latență p99 de milisecunde, Elasticsearch (sau OpenSearch) trage înainte. Dacă aplicația ta e centrată pe căutare, Elasticsearch e corect. Dacă căutarea e o funcționalitate secundară, Postgres e corect.

Caching la scară. Redis e răspunsul corect pentru chei fierbinți, stocare de sesiuni, leaderboards, pub/sub, rate limiting: oriunde ai nevoie de acces de o cifră de milisecunde la date mici cu TTL-uri și nu vrei ca fiecare citire să lovească primary-ul. Redis servește un ordin de mărime mai multe cereri pe secundă decât Postgres pe același hardware și îți dă primitivele potrivite. Adăugarea Redis ca al doilea store e mișcarea polyglot cea mai comună și cea mai justificată.

Analiză la scară. Rularea de query-uri grele de agregare pe Postgres-ul tău tranzacțional face workload-ul OLTP să sufere. Trimite datele într-un column store (BigQuery, Snowflake, ClickHouse, DuckDB pentru scări mai mici) și rulează analiza acolo. Baza de date tranzacțională se întoarce la treaba ei. Lecția 19 a acoperit acest caz în întregime.

Event sourcing. Când log-ul de evenimente e el însuși sursa adevărului, Kafka (sau un log similar) e sistemul de înregistrare. Baza de date relațională devine o vedere derivată, reconstruită din log când e nevoie. Modulul 4, lecția 42 reia asta.

În fiecare caz, a doua bază de date face ceva ce Postgres genuin nu poate face bine, iar costul operațional e plătit de bună voie pentru că alternativa e mai proastă.

Problema păstrării lor în sincron

Odată ce datele trăiesc în două locuri, deții o problemă de sincronizare. Utilizatorul își schimbă email-ul în primary; cache-ul, indexul de căutare și warehouse-ul de analiză toate au valoarea veche. Fiecare trebuie actualizat, fiecare actualizare poate eșua, fiecare sistem are propria fereastră de consistență. Modulul 4, lecția 46 acoperă pattern-urile de soluție (change data capture și pattern-ul outbox) în profunzime. Cele două forme care merită numite acum:

  • Dual writes: când aplicația scrie în primary, scrie și în cache, indexul de căutare și coada de analiză. Ușor de implementat, ușor de greșit și aproape întotdeauna se termină cu secondary-urile desincronizându-se. Evită cu excepția celor mai simple cache-uri cu TTL-uri scurte.
  • Change data capture (CDC): primary-ul emite un stream cu fiecare schimbare (replicare logică Postgres, Debezium sau similar) iar consumatorii downstream îl citesc și își actualizează store-urile. Primary-ul rămâne sursa adevărului, secondary-urile sunt derivate, iar sincronizarea e idempotentă (lecția 16).

Dacă adopți un al doilea store și răspunsul la „cum rămâne în sincron” e „vom face dual-write,” bugetează timp pentru rescriere. Vei ajunge la CDC.

Un exemplu rezolvat: o aplicație e-commerce cu trei store-uri

Scenariu concret. O platformă e-commerce cu trei store-uri alese din motivele potrivite:

  • Postgres ca sistem primar de înregistrare: comenzi, produse, utilizatori, inventar, plăți. ACID pe lucrurile care au nevoie.
  • Redis ca cache: stocare de sesiuni, pagini de produs fierbinți, contoare de rate-limit, pub/sub pentru actualizări în timp real între procese.
  • Elasticsearch ca index de căutare al catalogului: catalogul complet de produse cu analizatori multi-limbă, navigare cu fațete, autocomplete, toleranță la greșeli.
flowchart LR
    User[User request] --> App[Application]
    App -->|writes| PG[(Postgres - source of truth)]
    App -->|reads hot keys| R[(Redis - cache)]
    App -->|search queries| ES[(Elasticsearch - search)]
    PG -->|CDC stream| ES
    PG -.->|invalidate| R

Scrierile merg în Postgres. Un stream CDC alimentează Elasticsearch, care rămâne actual în câteva secunde. Redis e populat lazy la citire, cu invalidare la scriere în Postgres. Citirile de date tranzacționale merg la Postgres, paginile fierbinți trec prin Redis, query-urile de căutare merg la Elasticsearch.

Avantajele: fiecare store face ce face bine. Căutarea în catalog e rapidă și bogată, lookup-urile de sesiuni sunt sub-milisecundă, scrierile tranzacționale sunt ACID, niciunul dintre store-uri nu e abuzat. Dezavantajele: trei baze de date, trei rotații on-call, trei strategii de backup, trei dashboard-uri de monitorizare. Când rezultatele de căutare arată învechite echipa trebuie să decidă dacă bug-ul e în Postgres, în pipeline-ul CDC sau în Elasticsearch. Când Redis cade echipa trebuie să știe dacă fallback-ul la Postgres poate absorbi încărcarea. Pipeline-ul CDC e el însuși un sistem de operat.

Trei moduri de eșec care merită numite. Cache stampede: intrarea de cache a unui item popular expiră, o mie de cereri concurente nu o găsesc, toate lovesc Postgres, Postgres cade. Mitighează cu single-flight. Lag CDC: stream-ul către Elasticsearch rămâne în urmă, căutarea arată produse șterse sau ascunde unele noi. Monitorizează și alertează pe lag. Cache-write split-brain: scrierea în Postgres reușește dar invalidarea cache-ului eșuează, cache-ul servește date învechite până la TTL. TTL-uri scurte și tolerează învechirea.

Asta e o arhitectură polyglot rezonabilă. Fiecare store își câștigă locul. Aplicația e semnificativ mai bună decât pe Postgres singur, dar doar pentru că workload-ul cere genuin specializarea și echipa a investit în maturitatea operațională pentru a rula trei sisteme.

Recomandarea de încheiere

Instinctul de a adăuga încă o bază de date aproape întotdeauna se simte productiv. Noul store va rezolva problema specifică din fața ta astăzi. Beneficiile sunt concrete. Costurile sunt abstracte și nu apar până mai târziu, când rulezi patru sisteme de backup și trei rotații on-call iar echipa ta face mai multă muncă operațională decât muncă de funcționalități.

Fii sceptic față de „ar trebui să adăugăm baza de date X.” Răspunsul default e „am epuizat Postgres mai întâi?” Uită-te la jsonb, la pgvector, la TimescaleDB, la PostGIS, la AGE. De cele mai multe ori, unul dintre ele face treaba suficient de bine încât a doua bază de date să fie inutilă. Când răspunsul e genuin „nu, Postgres nu poate face asta la scara noastră,” atunci adaugă al doilea store, cu ochii deschiși. Plătește costul de bună voie pentru că workload-ul îl cere, nu pentru că diagrama părea elegantă.

Echipele pe care le-am văzut reușind cu polyglot persistence sunt cele care au ajuns acolo cu reticență. Fiecare store a fost adăugat pentru că o durere măsurată i-a împins la el. Echipele pe care le-am văzut chinuindu-se sunt cele care au început cu diagrama elegantă și apoi au petrecut doi ani operând-o.

Despre ce a fost Modulul 3

Acum opt lecții am început Modulul 3 cu modelul relațional și întrebarea ce-l face default-ul. Am parcurs alternativele: documente, key-value, columnar, graf, time-series, vectorial. Fiecare rezolvă o problemă pe care modelul relațional o tratează stângaci, iar fiecare costă ceva în schimb. Arcul modulului a fost despre a-ți da vocabularul de a evalua orice alegere de bază de date, inclusiv pe cea pe care deja o ai.

Trei lucruri de reținut. Primul, „baza de date potrivită” e o funcție de pattern-uri de query și constrângeri operaționale, nu de modă. Al doilea, Postgres în 2026 face mult mai mult decât sugerează reputația sa din 2011, iar default-ul „folosește Postgres până ai măsurat că nu poți” e corect mai des decât sugerează diagramele. Al treilea, fiecare bază de date suplimentară e un cost real: polyglot persistence e o unealtă de folosit cu reticență și deliberat, nu un scop în sine.

Modulul 3 e gata. Modulul 4 se întoarce la întrebarea care a fost amânată tot modulul: cum scalezi orizontal oricare dintre aceste alegeri de bază de date, când plafonul de single-server e bottleneck-ul. Replicare, partiționare, sharding, modelele de consistență din Modulul 2 aplicate layout-urilor reale de stocare. Pattern-urile sunt aceleași indiferent de baza de date pe care ai ales-o și sunt fundamentul următor.

Citări și lecturi suplimentare

  • Martin Fowler, „PolyglotPersistence”, 2011, https://martinfowler.com/bliki/PolyglotPersistence.html (consultat 2026-05-01). Postul original care a numit pattern-ul.
  • Pramod Sadalage și Martin Fowler, „NoSQL Distilled” (Addison-Wesley, 2012). Tratarea în format de carte a argumentului polyglot, scrisă când era cazul optimist.
  • Documentația TimescaleDB, https://docs.timescale.com/ (consultat 2026-05-01). Referința pentru calea time-series-on-Postgres.
  • Documentația Apache AGE, https://age.apache.org/age-manual/master/index.html (consultat 2026-05-01). Extensia Cypher-on-Postgres.
  • Documentația PostGIS, https://postgis.net/documentation/ (consultat 2026-05-01). Extensia spațială care a fost standardul auriu timp de două decenii.
  • pgvector, https://github.com/pgvector/pgvector (consultat 2026-05-01). Extensia de căutare vectorială acoperită în lecția 23.
  • Documentația Postgres, „Full Text Search”, https://www.postgresql.org/docs/current/textsearch.html (consultat 2026-05-01). Referința pentru mașinăria tsvector/tsquery.
Caută