Aplicațiile software au evoluat enorm în ultimele decenii, atât ca scop, cât și ca structură. De la algoritmi simpli, care rezolvau probleme punctuale - cum a fost cel folosit pentru spargerea codului Enigma în Al Doilea Război Mondial - am ajuns la sisteme complexe, distribuite, care susțin industrii întregi. La început, software-ul era utilizat în special în scopuri militare sau medicale, dar treptat a pătruns în finanțe, business și în viața noastră de zi cu zi. Astăzi, software-ul este omniprezent și ne așteptăm ca aplicațiile să funcționeze instant, fără să ne intereseze ce se întâmplă în spate.
Pentru a răspunde acestor așteptări tot mai ridicate, dezvoltatorii au fost nevoiți să optimizeze nu doar viteza și consumul de resurse, ci și modul în care este scris și organizat codul. De aici și tranziția de la aplicații monolitice la arhitecturi moderne bazate pe microservicii.
În cele ce urmează, vom defini și compara cele două metode de a crea aplicații. Vom analiza avantajele și dezavantajele fiecăreia, dar și când se justifică alegerea uneia în defavoarea celeilalte.
Un monolit scris în Java pornește, de regulă, de la un fișier JAR/WAR care conține tot: controllere REST, servicii de business, repository-uri JPA, batchuri, job Schedule, pagini Thymeleaf sau orice alt generator de HTML, template-uri e-mail și poate chiar module de raportare și generare de date cum ar fi Jasper.
Figura. 1 Model monolit
Este important să nu confundăm o aplicație modulară cu una bazată pe microservicii. Faptul că o aplicație este un monolit nu înseamnă că nu poate fi bine structurată pe module. Înseamnă doar că toate aceste module fac parte din aceeași aplicație mai mare, dar cu limitarea că nu poți face release la un modul independent.
Când aplicația este mică, avem avantaje clare:
Simplitate de dezvoltare - un singur code-base, un singur build Gradle/Maven, un singur set de profile Spring.
Debugging - se poate pune un breakpoint și se vede automat tot callstackul într-un singur IDE.
Tranzacții locale - consistența este garantată prin persistența datelor într-un singur context ACID.
Pe măsură ce codul și echipa cresc, aceleași trăsături se transformă în obstacole:
Scalare negranulară - un modul CPU- intens (de exemplu, un generator PDF de facturi) consumă resurse și forțează replicarea întregului proces.
Fereastră de deploy lungă - un fix de UI (de exemplu se urgentează schimbarea unui element grafic) necesită build, test integrat și restart complet, cu downtime destul de lung.
Conflicte de echipă - 10-15 developeri editează clase comune, apar merge-uri care sunt necontrolabile și, bineînțeles, echipa de QA (dacă există) va trebui să facă regresii neașteptate.
Technical-debt masiv - upgrade-ul la Spring Boot major, schimbarea JDK-ului sau trecerea la un alt driver de bază de date implică retestarea tuturor componentelor deodată.
După câțiva ani, monolitul atinge maximul complexității: onboardingul unui nou coleg durează săptămâni, timpii de build devin enormi (chiar de ordinul orelor dacă se execută suite de teste de integrare), iar fiecare release este un eveniment stresant pentru echipa de development sau cea de devops, întrucât toate modulele sunt repornite.
Un microserviciu este un proces autonom, înglobând un subset clar al domeniului de business (de ex., gestionare utilizatori, procesare plăți, căutare, generare de rapoarte). Dacă un monolit a fost modularizat corect, atunci cam fiecare modul poate fi înțeles ca un microserviciu, cu avantajul ca în cazul microserviciilor, aceste module devin acum independente, având propriul ciclu de viață și development.
Figura. 2 Model microserviciu
Izolare a codului - code-base separat pentru fiecare serviciu, pipeline-uri CI/CD specializate, timezone și configurații proprii. Dacă avem 100 de microservicii, este de așteptat să avem 100+ Git-uri.
Bază de date per serviciu - fiecare microserviciu deține schema și modelul său, evitând cuplarea (coupling) la nivel de bază de date, fie ea SQL sau NoSQL. Este important de menționat diferența între a avea o schemă pentru fiecare serviciu față de a avea câte o bază de date pentru fiecare serviciu. O bază de date poate fi împărțită de mai multe microservicii diferite, dar niciodată o schemă nu o să fie refolosită de mai multe microservicii diferite.
Comunicare explicită - HTTP/REST, gRPC sau event streaming (Kafka, RabbitMQ, Pulsar). Toate aceste modalități de comunicare ajută la decuplarea tehnologiilor folosite de microservicii. Nu există nici o restricție în afară de cea a comunicării a protocolului ales.
Scalare selectivă - doar anumite servicii primesc resurse; cost total mai mic. De exemplu, în perioadele de sărbători, se dorește creșterea serviciilor ce permit comenzi online sau căutare de produse. Nu ar avea sens să scalăm identic și servicii care țin de rapoarte fiscale.
Domeniu specific - fiecare microserviciu are propriul domeniu de execuție. De exemplu, un microserviciu de user profile nu va avea niciodată modele de produse sau facturi.
Viteză de livrare - echipe mici, responsabile end-to-end pot rula zeci de release-uri pe zi, atâta timp cât se respectă contractul dintre servicii.
Rezistență la erori - o defecțiune într-un serviciu degradează parțial sistemul, nu îl doboară complet.
Diversitate tehnologică - un microserviciu poate fi rescris în Python sau Go fără a atinge restul platformei care poate fi scrisă în Java.
Fără single-point-of-failure - adică dacă un microserviciu pică, nu pică toată aplicația. Ba mai mult se pot adăuga circuit-breaker pentru a nu bloca serviciile care depind de servicii care fie sunt picate, fie sunt supraaglomerate. O astfel de modalitate se implementează folosind Resilience4j sau Hystrix. Acest lucru poate reduce consistent erorile 5xx.
Versionare API - se păstrează două versiuni live cel puțin un ciclu de release.
Complexitate operațională - Dacă în cazul monolitului aveam loggingul și metricile centralizate, în cazul microserviciilor acest lucru se complică și este nevoie de aplicații sau librării adiționale pentru a gestiona acest lucru. De exemplu, ELK Stack pentru logging sau MicroMeter sau Prometheus pentru performanță.
Observabilitate distribuită - fără trace-id propagat (Spring Cloud Sleuth + Zipkin/Jaeger) rezolvarea incidentelor durează ore sau chiar e imposibilă.
Delay de rețea sau supra-comunicare - prea multe servicii extrase ridică latența și factura cloud (mai ales la REST). Dacă în cazul monolit aveam comunicare directă, între module prin simple chemări de metode, în cazul microserviciilor se poate ajunge la o latență ridicată prin mutarea datelor între microservicii. De aceea, trebuie să existe un echilibru între ce trebuie extras ca serviciu separat și ce trebuie să rămână unit.
Consistență a datelor - tranzacțiile distribuite trebuie gestionate cu Saga, Outbox sau Event Sourcing.
Over-engineering - un start-up cu 3 developeri și 100 utilizatori poate cheltui mai mult timp pe DevOps decât pe produs.
Figura 3 Configurație aplicație monolit
Monolitul scalează pe verticală și anume se adaugă CPU, RAM, disc mai rapid, eventual se folosește clustering. Este simplu, dar vine cu limitare fizică (un singur nod foarte puternic devine scump și ineficient) și un punct unic de eșec.
În contrast microserviciile scalează pe orizontală prin replicarea containerelor individuale. Se pot mări anumite servicii de la 2 la 12 instanțe când CPU > 80 %, și doar pe acelea care necesită să fie clonate, evitând utilizarea de resurse incorect.
Migrarea de la monolit la microservicii
Dacă se dorește migrarea de la un monolit existent la o arhitectură pe bază de microservicii, de regulă, există mai multe direcții care pot contribui la succesul acestui proces. De obicei, migrarea se face incremental, pentru a evita pierderile de date sau perioadele de nefuncționare (downtime). În plus, este important ca aplicația să poată fi testată între release-uri. Echipa de QA ar fi copleșită dacă ar trebui să testeze totul abia la final.
Se pot folosi următoarele principii:
Strangler Pattern - se traduce prin eliminarea treptată a funcționalităților din monolit și mutarea lor către un microserviciu. Practic, se scot, bucată cu bucată, funcționalități din modulele din monolit și se mută în servici standalone.
Modularizare și extragere - nu orice modul are sens singur. De aceea, se face o strategie astfel încât să se determine dacă decuplarea modulelor se poate realiza, sau se extrag mai multe module într-un serviciu separat. Totodată, înainte de a extrage microservicii, este bine ca aplicația monolit să fie deja multi-modală, folosind de exemplu module maven.
Backend for Frontend - adăugarea de servicii translatoare și agregatoare. Acest lucru înseamnă crearea unor servicii terțe care să pregătească datele din microservicii pentru anumiți clienți, cum ar fi platforme mobile (Android sau iOS) sau aplicații web (platforme React sau Angular). Acest lucru ajută enorm în crearea de rute optime, trafic de rețea scăzut și decuplarea client de server. De exemplu, se pot adăuga preprocesări de date sau agregare din mai multe servicii, astfel datele pot părea atomice în clienți, fără să fie necesar să se facă mai multe apeluri către microservicii.
Reutilizare și decuplare a bazelor de date - acest lucru înseamnă ca la început microserviciul va folosi aceeași schemă ca și monolitul pentru a preveni duplicarea datelor. Bineînțeles că monolitul treptat nu va mai avea voie să scrie date, ulterior datele fiind mutate cu totul într-o nouă schemă specifică serviciului.
Spring Boot 3.x + Spring Cloud - suport nativ pentru observabilitate (Micrometer), configurații externe (Config Server), gateway robust.
Micronaut / Quarkus - ideale pentru imagini native GraalVM și timpi de boot sub 0.1 s (cu condiția să se folosească atent tehnologii bazate pe AOP).
Contract Testing - Spring Cloud Contract sau Pact folosite obligatoriu când avem de a face cu servicii de ordinul zecilor. Se rulează stuburi WireMock atât în CI, cât și în testele locale.
Secret Management - Se folosesc tehnologii HashiCorp Vault sau AWS KMS. Nu se pun parole în plain text în application.yml.
Retry & Timeout - Obligatoriu dacă există apeluri sincrone (de obicei REST) se pune time-out \< 3 s pentru apeluri interne și retry-uri cu back-off exponențial.
Caching - dacă datele se modifică rar și pot cache-ui. Chiar dacă sunt date importante, acestea se pot invalida cu ușurință.
Situația inițială: monolit Spring Boot 2.0, PostgreSQL, thymeleaf pentru generate de HTML, deploy pe VPS singular Hetzner. TTFB > 900 ms până la 9000ms în sezonul estival. S-a observat punct blocant în căutarile de produse.
Ținte: TTFB < 200 ms, deploy fără downtime, autoscaling.
Strategie: Strangler Pattern. S-au extras următoarele microservicii:
search-service: Spring Boot, Solr client - din teste folosind jMeter s-a observat un răspuns de 50ms pentru o încărcare de 1000 utilizatori concurenți. Serviciu poate fi replicat 3x în funcție de necesitate, întrucât datele sunt doar spre citire.
User-management-service (management pur de utilizatori, generare de JWT). Doar utilizatorii înregistrați au acces în această zonă.
Product-management-service (business relevant pentru management de produs). Doar utilizatorii înregistrați au acces în această zonă.
Product-order-service - management de comenzi de produse.
E-mail-service - Spring Boot, cu configurare de email și SMTP.
De notat că toate serviciile au fost create și puse în containere Docker, astfel totul s-a putut automatiza. De altfel, s-a adăugat NGINX ca reverse proxy.
S-a adăugat comunicare asincronă prin cozi RabbitMQ. De exemplu, produsele sunt indexate în SOLR prin evenimente venite dinspre product service către search service.
TTFB majoritar de 100-150 ms.
Cost mai ridicat pentru cloud, dar nici un blocaj la rulare, plus zero downtime la release-uri (blue-green deployment).
Figura 3 Configurație aplicație monolit
Monolitul rămâne o opțiune excelentă pentru proiecte mici, echipe restrânse și MVP-uri ce trebuie validate rapid. În schimb, microserviciile își justifică investiția atunci când există mai multe echipe autonome, traficul variază semnificativ, iar livrarea trebuie să se facă în ore, nu în săptămâni. Ecosistemul Java susține ambele abordări: Spring Modulith ajută la construirea unor monoliți coerent modularizați, în timp ce Spring Cloud, Micronaut și Quarkus oferă suport enterprise pentru microservicii.
Totuși, odată cu această flexibilitate vine și o responsabilitate crescută: observabilitate, pipeline-uri CI/CD robuste, teste contractuale și o bună guvernanță a codului. Migrarea către microservicii trebuie să fie ghidată de nevoi reale, nu de tendințe. Iar tranziția trebuie făcută pas cu pas, măsurând constant impactul și reducând complexitatea acolo unde nu aduce valoare de business.