Several systems that have to work together, including when one of them goes down.
We design and operate platforms made of several services, with a message bus, capped retries, idempotency keys, circuit breakers, and releases that roll back on their own. Every pattern below runs in a system of ours, not in a diagram.
Construit dejaOperăm patru platforme proprii cu arhitecturi diferite și le putem deschide pe toate. Cifrele sunt măsurate azi cu comenzi, nu preluate din documentație: aichat are 28 de directoare de serviciu, 235 de modele de date pe patru scheme și o magistrală de mesaje cu 297 de cozi și 17.525 de legături; Kallina are 472 de funcții edge, 559 de migrări și 28 de sarcini programate în bază; MEGA CRM are un backend de 9.516 linii cu 99 de rute și 179 de politici de acces pe rând; Taskin rulează 21 de sarcini programate fără niciun Docker. Rezerva pe care o spunem noi: în două locuri documentația proprie a rămas în urma codului — o afirmație despre numărul de linii era depășită cu 16%, iar o diagramă de infrastructură descria un server pe care nu mai rulăm nimic. Am folosit măsurătoarea, nu documentul.
„Arhitectură complexă” nu înseamnă multe cutii pe un desen. Înseamnă că știi ce se întâmplă când una dintre ele nu răspunde. Un sistem cu trei servicii care se apelează sincron, fără plafon de reîncercări și fără idempotență, e mai fragil decât un monolit — pentru că fiecare legătură nouă e un mod nou de eșec, iar modurile de eșec se înmulțesc mai repede decât funcționalitățile.
Lucrarea noastră pornește de la separarea responsabilităților și se termină la ce se vede când ceva cedează. Într-una din platformele proprii, mesajele nu trec direct între servicii, ci printr-o magistrală cu 297 de cozi, 9 schimburi și 17.525 de legături, dintre care 278 de cozi sunt generate per client, nu scrise de mână. Opt cozi au schimb de scrisori moarte și opt au timp de viață pe mesaj de cinci minute — adică un mesaj care nu poate fi procesat ajunge undeva unde poate fi văzut, nu dispare.
Tiparele pe care le punem sunt puține și se repetă: reîncercări cu plafon scris în cod, chei de idempotență care fac a doua livrare a aceluiași eveniment inofensivă, un întrerupător care oprește apelurile către un serviciu care cade repetat, o poartă de consum înaintea acțiunilor care costă bani, și alerte cu prag de repetiție ca o defecțiune intermitentă să nu îngroape restul.
Ultima parte, cea care se uită: publicarea. Una dintre platforme se publică prin sincronizare de fișiere, fără Docker, cu verificarea că pagina servită conține exact identificatorul pachetului tocmai publicat — nu doar că serverul răspunde 200 — și cu revenire automată la versiunea precedentă dacă verificarea pică. Regula a apărut după un incident real în care o verificare superficială a anulat o publicare bună.
What it covers
The work, piece by piece
Separăm serviciile după ce se poate strica independent
Într-o platformă proprie sunt 28 de directoare de serviciu, dintre care 16 au punct de intrare propriu, iar pe producție rulează cinci procese sub un manager de procese. Fiecare canal de comunicare are propriul serviciu și propriul port, tocmai ca o defecțiune la unul să nu oprească restul. Datele sunt modelate pe patru scheme separate, cu 235 de modele în total — separarea nu e doar la nivel de proces, e și la nivel de schemă.
Punem o magistrală de mesaje, nu apeluri sincrone lanț
297 de cozi, 9 schimburi, 17.525 de legături. Denumirea cozilor e per client și, în unele cazuri, per fir de discuție — adică topologia se generează, nu se scrie de mână. Există și un schimb cu livrare întârziată, pentru lucrurile care trebuie să se întâmple mai târziu, nu acum. Opt cozi au schimb de scrisori moarte configurat și opt au timp de viață pe mesaj de cinci minute.
Reîncercări cu plafon, nu la infinit
Trei plafoane diferite pentru trei situații diferite, toate scrise în cod: cinci reîncercări pentru mesajele programate, cu contorul persistat, nu ținut în memorie; trei reîncercări la nivel de coadă, numărate într-un antet al mesajului; și trei reîncercări cu creștere exponențială a pauzei pentru trimiterea de e-mail, pe două căi de furnizor. O reîncercare fără plafon nu e reziliență, e o buclă.
Chei de idempotență, ca a doua livrare să nu strice nimic
Pentru evenimentele de la terți folosim o inserare care eșuează dacă evenimentul a mai fost văzut: încălcarea de unicitate a bazei de date se tratează ca „duplicat”, nu ca eroare, alături de o verificare de prospețime a momentului. Pentru plăți, cheia e un marcaj pe tranzacția de credit, deci retrimiterea aceluiași webhook nu creditează de două ori. Pentru mesaje, dedublarea se face pe identificator, cu timp de viață și curățare.
Un întrerupător de circuit pe integrarea fragilă
Integrarea care depinde de sesiunea unui site terț are un întrerupător real, nu doar o notă în documentație: după zece eșecuri consecutive, apelurile se opresc două minute. Fără el, un site care nu răspunde transformă un canal căzut într-o platformă lentă, pentru că toată lumea așteaptă în cerere.
Poartă de consum înaintea acțiunilor care costă
Într-una din platforme, orice acțiune plătită trece printr-o funcție de verificare cu patru trepte, fiecare cu propriul motiv întors în clar: cont suspendat, limită lunară de conversații atinsă, plafon zilnic de cheltuială depășit, sold insuficient. La excepție întoarce „nepermis”, nu „permis” — poarta se închide, nu se deschide, când ceva nu merge. În spate stă un registru de evenimente de consum cu cost pe eveniment, defalcat pe furnizor, model și modalitate.
Limitare de rată care supraviețuiește repornirii
Fereastră glisantă ținută în bază de date, cu o hartă în memorie doar ca scurtătură în interiorul unei invocări. Motivul e scris chiar în capul fișierului: procesele care servesc cererile sunt de scurtă durată, deci memoria lor nu poate fi sursa de adevăr. Cincisprezece funcții o folosesc, iar cota unui furnizor extern are propriul limitator, separat.
Observabilitate care distinge „a răspuns” de „a funcționat”
Un răspuns 200 nu înseamnă că sarcina a reușit. Învelișul cu care rulăm sarcinile programate parcurge recursiv răspunsul după liste de eșecuri sau erori și tratează „200 cu eșecuri” ca rezultat distinct. Alertele au prag de repetiție — zece minute într-un sistem, două ore în altul — iar marcajul se șterge la prima reușită, ca o defecțiune revenită să poată alerta din nou imediat.
Publicare cu verificare și revenire automată
Publicarea verifică nu doar că adresa răspunde 200, ci că pagina servită conține exact identificatorul pachetului tocmai publicat; altfel readuce versiunea precedentă. Pentru serviciul de fundal, publicarea compară arborele pe sume de control și sare peste repornire dacă nimic nu s-a schimbat — o repornire inutilă omoară o buclă aflată în lucru.
What it looks like
The route, step by step.
01
Desenăm harta modurilor de eșec, nu a cutiilor
Pentru fiecare legătură între două sisteme: ce se întâmplă dacă celălalt e lent, dacă e căzut, dacă răspunde de două ori, dacă răspunde greșit. Livrăm: lista legăturilor cu comportamentul așteptat la fiecare din cele patru situații, plus decizia sincron/asincron pentru fiecare.
02
Punem magistrala și contractele de mesaj
Cozi, schimburi, scrisori moarte, timp de viață pe mesaj, și cheia de idempotență pentru fiecare tip de eveniment. Livrăm: topologia, contractele de mesaj și comportamentul documentat la retrimitere.
03
Adăugăm porțile: rată, consum, întrerupător
Limitarea de rată pe fereastră glisantă în bază, poarta de consum înaintea acțiunilor care costă și întrerupătorul pe integrările din afara controlului nostru. Livrăm: pragurile setate, motivele întoarse în clar și testele pentru fiecare treaptă de refuz.
04
Construim publicarea și revenirea
Publicare cu verificare de conținut, nu doar de cod de răspuns, versiune precedentă păstrată alături și revenire automată. Livrăm: procedura de publicare, procedura de revenire exersată cel puțin o dată, și sondele de sănătate — una superficială pentru „trăiește”, una adâncă pentru „chiar merge”.
05
Predăm cu documentația verificată față de cod
Documentul se verifică față de măsurătoare înainte de predare, pentru că documentația care rămâne în urmă e mai periculoasă decât lipsa ei. Livrăm: diagrama, procedurile și lista explicită a locurilor unde documentul și codul au fost găsite în dezacord, cu ce am corectat.
Sistemele nu se apelează în lanț: între ele stă o magistrală cu cozi per client, schimburi separate pe tip de trafic și scrisori moarte pentru ce nu se poate procesa. În jurul ei, porțile — limitare de rată, poartă de consum, întrerupător de circuit — și sondele de sănătate, una superficială și una adâncă.
Datele
What we touch, where it lives, and how long it stays
The questions anyone with a data protection officer asks — asked here before they do.
Unde stau datele, per platformă
Nu există un răspuns unic și e bine că nu există: o platformă folosește MySQL pe server propriu, două folosesc PostgreSQL prin Supabase — una găzduită, una instalată pe infrastructura noastră — iar una ține conținutul ca fișiere statice, fără bază de date deloc. Alegerea se face pe cerințe, nu pe obișnuință.
Accesul se aplică în bază, nu doar în aplicație
Într-un sistem intern al nostru, securitatea la nivel de rând e pornită pe 43 de tabele, prin 90 de instrucțiuni, cu 179 de politici scrise. Regula pe care o urmărim: dacă un apel ocolește aplicația și lovește baza direct, tot nu trebuie să vadă rândurile altcuiva.
Registrul de consum
Un rând per eveniment cu tip, furnizor, cantități separate pe modalitate — secunde de convorbire, tokeni de intrare, tokeni de ieșire, caractere — plus cost în valută cu șase zecimale și creditele scăzute. Din el ies plafonul zilnic de cheltuială, costul pe apel și proiecția de consum.
Migrările sunt istoricul, nu documentația
559 de migrări într-o platformă, 80 într-alta. Schema se schimbă prin migrări versionate, deci starea bazei se poate reconstrui și se poate citi cronologic. Când documentul și migrarea nu sunt de acord, migrarea are dreptate.
Sarcinile programate stau în bază sau în cron, nu în proces
28 de sarcini programate rulează în interiorul bazei de date într-o platformă; într-alta, 21 de sarcini rulează prin cron de sistem. Motivul e scris în fișier: cronometrele din interiorul unui serviciu se resetează la fiecare repornire, deci o buclă la trei ore dintr-un serviciu care se republică nu se declanșează niciodată.
A case
A message topology that generates itself for every client
Situația
O platformă cu mai mulți clienți, fiecare cu propriile canale de comunicare, propriile fire de discuție și propriile notificări. Varianta naivă — o coadă comună și un filtru pe identificatorul clientului — face ca un client cu volum mare să blocheze restul, iar o eroare pe un fir să oprească coada pentru toți.
Ce am construit
Topologia se generează per client, nu se scrie de mână: din 297 de cozi, 278 sunt de tip „notificări pentru un anumit cont”, iar unele coboară până la nivelul unui singur fir de discuție. Peste ele, nouă schimburi separă tipurile de trafic — notificări, tokenuri, webhookuri, canale — și există un schimb cu livrare întârziată pentru ce trebuie să se întâmple mai târziu. Opt cozi au schimb de scrisori moarte, opt au timp de viață pe mesaj de cinci minute. Regulile de orchestrare se pot reîncărca la cald, printr-un canal de publicare, fără repornirea consumatorilor.
Ce a ieșit
Un client cu volum mare nu întârzie ceilalți clienți, iar un mesaj care nu poate fi procesat ajunge într-un loc unde poate fi văzut și reluat, în loc să dispară sau să blocheze coada. Numărul de legături din magistrală — 17.525 — arată exact de ce topologia trebuie generată: nimeni nu întreține manual așa ceva.
Ce nu spune cazul
Prețul e operațional: o magistrală de dimensiunea asta are nevoie de monitorizare proprie și de un plan pentru cozile abandonate, altfel crește la nesfârșit. Iar generarea per client presupune că ștergerea unui client șterge și topologia lui — dacă pasul ăsta lipsește, se acumulează cozi moarte.
Întrebări
What people ask us before they call
De unde știu că nu-mi vindeți complexitate de care n-am nevoie?
Pentru că prima recomandare pe care o dăm des e să nu separați. Fiecare serviciu nou e un mod de eșec nou, iar modurile de eșec se înmulțesc mai repede decât funcționalitățile. Un exemplu din propriile noastre sisteme: backendul unuia dintre ele e un singur fișier de 9.516 linii cu 99 de rute. Nu e elegant și o spunem, dar se publică într-un pas și se depanează într-un loc. Separarea se face când există un motiv măsurabil — un serviciu care trebuie scalat separat, o echipă separată, un ritm de eliberare separat.
Ce se întâmplă când un sistem din lanț nu răspunde?
Depinde de ce am decis împreună la etapa de hartă, și asta e ideea. În sistemele noastre: mesajul intră în coadă și se reîncearcă de un număr plafonat de ori, apoi ajunge în schimbul de scrisori moarte, unde poate fi văzut; integrarea instabilă are un întrerupător care după zece eșecuri consecutive se oprește două minute în loc să țină cererile în așteptare; iar acțiunile care costă bani sunt oprite de o poartă care, la excepție, refuză, nu permite.
Cum evitați ca același eveniment să fie procesat de două ori?
Cu o cheie de idempotență, nu cu o verificare „am mai văzut asta?” care are cursă de date. Concret: inserăm o linie cu identificatorul evenimentului și, dacă baza refuză pentru încălcarea unicității, tratăm codul de eroare respectiv ca semnal de duplicat, nu ca defecțiune. Pentru plăți, marcajul stă pe tranzacția de credit, deci retrimiterea aceluiași webhook nu creditează de două ori.
Folosiți Docker sau nu?
Amândouă, și alegerea o argumentăm de fiecare dată. Un sistem rulează în container, dar fără volume montate, ceea ce înseamnă că publicarea e o copiere în container plus repornire — un `docker rm` acolo pierde starea, și e scris în documentul de operare exact așa. O altă platformă nu are niciun Docker: publicarea e sincronizare de fișiere, iar scripturile de publicare sunt instalate manual cu drepturi de administrator și expuse fluxului automat prin exact două căi permise. Motivul e scris în cod: un script pe care fluxul îl poate rescrie e un script prin care fluxul poate escalada.
Cum știți că o sarcină programată chiar a rulat?
Din experiență proastă. Un raport zilnic al nostru a fost mort unsprezece zile, între 7 și 17 august, în timp ce sarcina programată se declanșa în fiecare zi — comanda folosită ieșea în tăcere la eroare și nu scria nimic. De atunci fiecare sarcină rulează într-un înveliș care parcurge răspunsul după liste de eșecuri, tratează „200 cu eșecuri” ca rezultat distinct, are timp maxim de execuție și scrie o linie structurată pe rulare. Alertele au prag de repetiție, iar marcajul se șterge la prima reușită.
Cât de mult contează abstractizarea pe furnizor?
Mult, dacă furnizorul e într-o zonă care se mișcă repede. Pentru voce avem trei punți separate — câte una pentru fiecare furnizor de vorbire în timp real — care implementează aceeași interfață. Puntea face conversia audio între centrala telefonică și furnizor, pe un port dedicat, cu serverul de sănătate legat doar pe interfața locală, nu expus. Schimbarea furnizorului devine o decizie, nu o rescriere.
Documentația voastră e la zi?
Nu peste tot, și preferăm să spunem unde. La pregătirea acestei pagini am măsurat două sisteme și am găsit documentul propriu în urmă: o afirmație despre dimensiunea unui fișier era mai mică cu circa 16% decât realitatea, iar o diagramă de infrastructură descria un server de pe care ne mutasem. Regula pe care o aplicăm și pe care o cerem în proiecte: când documentul și măsurătoarea nu sunt de acord, măsurătoarea are dreptate, iar documentul se corectează în același pas.
Ce nu faceți?
Nu promitem obiective de disponibilitate fără măsurătoare — un procent de tip „99,9%” cere date de teren pe o perioadă, iar unde nu le avem nu-l afirmăm. Nu proiectăm sisteme pe care nu le putem opera sau preda: dacă rezultatul e o arhitectură pe care echipa ta nu o poate întreține, e o arhitectură greșită. Și nu facem certificări de conformitate — putem construi controalele, nu putem emite certificatul.
Pe ce se sprijină fiecare afirmație de mai sus (23 surse)
Magistrală de mesaje cu 297 de cozi, 9 schimburi, 17.525 de legături; 8 cozi cu schimb de scrisori moarte, 8 cu timp de viață de 300.000 ms; există un schimb cu livrare întârziată; 278 din 297 de cozi sunt per contdefinitions.json:export al brokerului, numărat prin analiză JSON la 06.09.2026
Reîncărcare la cald a regulilor de orchestrare printr-un canal de publicare, fără repornirea consumatorilormain.py:26-44 (reguli), 47-57 (canal de actualizare)
28 de directoare de serviciu, 16 cu punct de intrare propriu, 5 procese pe producția curentă; 235 de modele de date pe patru scheme (96 + 49 + 48 + 42) (numărat cu ls și grep la 06.09.2026):lista proceselor curente în CLAUDE.md:10
Plafoane de reîncercare: 5 pentru mesajele programate cu contor persistat; 3 la nivel de coadă, numărate într-un antet; 3 cu creștere exponențială pentru e-mail, pe două căi de furnizorschedule.service.js:125,129:instagram/comments.worker.js:385-386; utils/emailService.js:89,121-127,162-165
Întrerupător de circuit pe integrarea cu sesiune de site terț: 10 eșecuri consecutive → 2 minute de pauzătrei9.service.js:14 (definire), 158 și 255 (aplicare)
Idempotență pe plăți: marcaj pe tranzacția de credit, deci retrimiterea aceluiași webhook nu creditează de două oristripe.service.js:388-415
Dedublare de mesaje pe identificator, cu timp de viață și curățaremessage_utils.py:135-149
Două sonde de sănătate distincte: una superficială („trăiește”) și una adâncă („chiar merge”)index.js:313 și 325
Cheie de idempotență pe evenimente de la terți: inserare care tratează încălcarea de unicitate a bazei drept „duplicat”, plus verificare de prospețime a momentuluiwebhook-replay.ts:9-10 (prospețime), 28-35 (unicitate → duplicat)
Poartă de consum cu patru trepte, fiecare cu motiv întors în clar; la excepție întoarce „nepermis”index.ts:67 (suspendat), 77 (limită de conversații), 88 (plafon zilnic), 131-158 (sold), 177 (refuz la excepție)
Registru de evenimente de consum cu cost pe eveniment, defalcat pe furnizor, model și modalitate20260312100000_create_usage_events.sql:5-30
Limitare de rată pe fereastră glisantă ținută în bază, cu memoria doar ca scurtătură; motivul e scris în cap: procesele care servesc cererile sunt de scurtă duratărate-limiter.ts:3-7 (motivul), 11-20 (schema), 37-40 (răspuns)
Alerte cu prag de repetiție de 10 minute; eșecul canalului de alertare e înghițit ca să nu doboare apelantulalerting.ts:2-4 (înghițire), 22-30 (prag de repetiție)
472 de funcții edge (468 cu punct de intrare), 559 de migrări, 28 de sarcini programate în bază, 38 de module comune, 26 de funcții de sănătate/alertare, 8 servicii în compoziția de containere (numărat cu ls și grep la 06.09.2026):supabase/functions/, supabase/migrations/, docker-compose.yml
Trei punți vocale separate care implementează aceeași interfață de furnizor; puntea face conversia audio pe port dedicat, cu serverul de sănătate legat doar pe interfața localăelevenlabs-bridge.mjs:19-20 (porturi), 1522-1523 (conversie), 1560 (sănătate pe interfața locală); punți paralele în openai-realtime-bridge/ și gemini-bridge/
Backend într-un singur fișier de 9.516 linii cu 99 de rute; 80 de migrări; securitate la nivel de rând pornită prin 90 de instrucțiuni pe 43 de tabele, cu 179 de politici (ls | wc -l):numărat la 06.09.2026; documentul propriu spunea ~8.200 de linii — depășit cu circa 16%
Excluderea mutuală a sarcinilor de campanie prin fișiere de blocare; sarcini programate pe ore de lucruexecute-campaign.md:44-51 (orar), 56 (fișiere de blocare), 21-29 (lista de plase de siguranță)
Publicare fără Docker, prin sincronizare de fișiere; scripturile sunt instalate manual cu drepturi de administrator și expuse fluxului prin exact două căi permise, pentru că un script rescriptibil de flux e o cale de escaladaredeploy-web.sh:4-8 (motivul), 10-12 (fără Docker)
Verificarea de publicare cere ca pagina servită să conțină exact identificatorul pachetului publicat, nu doar un cod 200, altfel readuce versiunea precedentă; regula vine dintr-un incident realdeploy-web.sh:30-34 (incidentul), 37-42 (verificarea și revenirea)
Publicarea serviciului de fundal compară arborele pe sume de control și sare peste repornire dacă nimic nu s-a schimbat, pentru că o repornire omoară o buclă în lucrudeploy-collector.sh:9-10 (motivul), 19-23 (comparația)
Un raport zilnic a fost mort 11 zile (7–17 august) pentru că o comandă ieșea în tăcere la eroare; de atunci fiecare sarcină rulează într-un înveliș care tratează „200 cu eșecuri” ca rezultat distinct, cu timp maxim de execuție și prag de repetiție pe alertetaskin-run.sh:4-8 (incidentul), 27 (timp maxim), 68-106 (200 cu eșecuri), 51-59 și 112 (prag de repetiție și ștergerea marcajului la reușită)
Sarcinile se mută din cronometre interne în cron de sistem pentru că cronometrele se resetează la fiecare republicare; 21 de sarcini programate, 38 de module de buclătaskin-loops.cron:3-5 (motivul), 15-147 (sarcinile), 39-45 (cele trei convertite)
O diagramă proprie de infrastructură descria un server pe care nu mai rulăm nimic; documentul curent al proiectului o marchează explicit ca istoricăCLAUDE.md:3-5; nu am folosit datele de infrastructură din docs/architecture/README.md
Citările interne arată numele fișierului și linia. Calea completă rămâne în depozitul nostru; o putem parcurge împreună, la cerere.