De ce Am Construit Asta
Fiecare tutorial de microservicii îți arată cum să construiești un serviciu. Puțini îți arată cum să construiești cinci care chiar comunică între ele, gestionează eșecurile elegant și se deployează în Kubernetes. Am vrut să construiesc imaginea completă - nu un demo de jucărie, ci un sistem cu formă de producție, cu comunicare reală event-driven, izolarea bazelor de date, health check-uri și CI/CD.
Rezultatul este un monorepo Turborepo cu cinci servicii NestJS, pachete partajate pentru DTO-uri și configurare, RabbitMQ pentru mesagerie asincronă, PostgreSQL (o bază de date per serviciu) și un deployment complet în Kubernetes.
Cele Cinci Servicii
API Gateway (Portul 3000)
Gateway-ul este singurul punct de intrare. Nu conține logică de business - rutează, autentifică și agregă.
ProxyService menține clienți Axios pentru fiecare serviciu din aval. Când un request ajunge la /auth/register, AuthController delegă către proxyService.forward('auth', req.method, path, req.body, req.headers). Gateway-ul elimină header-ele hop-by-hop, adaugă correlation ID-uri și redirecționează curat.
Agregarea health check-urilor a fost o îmbunătățire plăcută: endpoint-ul /health al gateway-ului apelează endpoint-ul /health al fiecărui serviciu din aval în paralel și raportează un status agregat: healthy (toate serviciile funcționale), degraded (unele inactive) sau unhealthy (toate inactive). Verificarea de readiness impune în mod specific ca serviciul de autentificare să fie funcțional - dacă utilizatorii nu se pot autentifica, sistemul nu este pregătit.
Auth Service (Portul 3001)
Gestionează înregistrarea, token-urile JWT, resetarea parolei și /auth/me. Folosește Passport.js cu o strategie JWT personalizată și Prisma pentru stocarea utilizatorilor.
Când un utilizator se înregistrează, serviciul publică auth.user.registered în RabbitMQ. User Service-ul preia aceasta și creează un profil. Email Service-ul trimite un email de bun venit. Analytics Service-ul urmărește înregistrarea. Auth Service-ul nu știe cine ascultă - și acesta este tocmai scopul.
User Service (Portul 3002)
Gestionează profilurile utilizatorilor, preferințele și adresele. Ascultă evenimentele auth.user.registered pentru a crea automat profiluri. Publică evenimentele user.profile.updated și user.deleted pentru consumatorii din aval.
Product Service (Portul 3003)
Catalogul de produse cu categorii, urmărirea inventarului și căutare. Publică evenimente pentru crearea, actualizarea, ștergerea produselor, modificări de inventar și alerte de stoc scăzut. LowStockAlertEvent este deosebit de util - permite unui serviciu de notificări să alerteze adminii când inventarul scade sub un prag.
Payment Service (Portul 3004)
Comenzi și procesarea plăților. Ascultă evenimentele de produse pentru a menține un cache local denormalizat de produse (astfel nu trebuie să apeleze sincron Product Service-ul pentru validarea comenzilor).
Comunicarea Event-Driven: Detaliile
Fiecare serviciu are propriul EventService care se conectează la RabbitMQ la inițializarea modulului, declară un topic exchange (microservices.events) și oferă metode de publicare tipizate.
Reziliența Conexiunii
Fiecare EventService implementează OnModuleInit și OnModuleDestroy pentru gestionarea ciclului de viață. Dacă RabbitMQ nu este disponibil la pornire, serviciul reîncearcă la fiecare 5 secunde (setTimeout(() => this.connect(), this.retryDelay)). Handler-ele de erori pentru conexiune și canal înregistrează eșecurile și setează un flag isConnected. Metoda ensureConnection() restabilește conexiunea înaintea oricărei încercări de publicare.
Asta înseamnă că serviciile pornesc și funcționează chiar dacă RabbitMQ este temporar inactiv - pur și simplu nu pot publica evenimente până când conexiunea se recuperează. Nicio prăbușire, nicio blocare la pornire.
Structura Evenimentelor
Am standardizat formatul payload-ului evenimentelor în toate serviciile:
eventType: cheia de rutare (ex:user.profile.updated)data: payload-ul evenimentului (tipizat puternic per eveniment)timestamp: ISO 8601service: numele serviciului care publicăversion: pentru compatibilitate viitoare
Mesajele sunt publicate ca buffere JSON persistente cu ID-uri unice de mesaj, asigurând că supraviețuiesc repornirilor RabbitMQ.
Convenția de Denumire
Toate evenimentele urmează {serviciu}.{entitate}.{acțiune} - auth.user.registered, product.inventory.low_stock, payment.order.status.updated. Acest lucru face pattern-urile cheilor de rutare triviale: un serviciu interesat de toate evenimentele utilizatorului se abonează la *.user.*.
Bază de Date Per Serviciu
Fiecare serviciu are propria schemă Prisma, propriul director de migrații și propriul connection string pentru baza de date. Auth service-ul stochează credențialele și token-urile. User service-ul stochează profilurile. Product service-ul stochează catalogul. Nu există nicio stare partajată a bazei de date.
Compromisul este real: dacă Payment Service-ul are nevoie de detalii despre produse pentru o comandă, nu poate face pur și simplu un JOIN față de tabela de produse. Fie ascultă evenimentele de produse și menține un cache local, fie face un apel HTTP sincron prin gateway. Am ales abordarea de materialized view bazată pe evenimente pentru datele frecvent accesate și apelurile HTTP pentru căutările rare.
Health Check-uri: Trei Niveluri
Fiecare serviciu expune trei endpoint-uri de health:
/health- verificare completă (conectivitate la baza de date, uptime, versiune)/health/ready- readiness probe (poate serviciul gestiona request-uri?)/health/live- liveness probe (este procesul în viață?)
HealthService-ul partajat din pachete oferă verificatori reutilizabili pentru PostgreSQL, Redis și RabbitMQ. Fiecare serviciu își compune propriul health check din aceste blocuri de construcție. Manifestele Kubernetes referențiază aceste endpoint-uri pentru probele de readiness și liveness ale pod-urilor.
Deployment Kubernetes
Directorul k8s/ conține manifeste gata pentru producție:
- Deployments pentru fiecare serviciu cu limite și request-uri de resurse
- Services pentru descoperire internă bazată pe DNS
- ConfigMaps și Secrets pentru configurarea mediului
- HorizontalPodAutoscaler pentru scalare bazată pe trafic
- Readiness/liveness probes care indică spre endpoint-urile de health
Docker: Build-uri Multi-Stage
Fiecare serviciu are un Dockerfile cu un build în trei etape: deps (instalarea dependențelor), builder (compilarea TypeScript, generarea clientului Prisma) și runner (imaginea de producție cu amprentă minimă). Pachetele partajate sunt construite mai întâi (@microservices/shared, @microservices/config), apoi serviciul în sine.
Pachete Partajate
Directorul packages/ conține două pachete:
@microservices/shared- DTO-uri comune, constante de tip eveniment (AUTH_EVENTS,USER_EVENTS,PRODUCT_EVENTS,PAYMENT_EVENTS), utilitare de health check și configurare Swagger.@microservices/config- factory de configurare a conexiunii RabbitMQ, utilitare de denumire a cozilor și helper-e pentru mediu.
A avea constante de tip eveniment într-un pachet partajat înseamnă că publisher-ul și consumer-ul sunt mereu de acord cu string-ul cheii de rutare. Fără greșeli de scriere, fără drift.
Ce Am Învățat
- Arhitectura event-driven este mai greu de debugat. Când ceva nu merge, eroarea poate fi în serviciul care publică, în configurarea exchange-ului, în cheia de rutare, în deserializarea consumer-ului sau în logica de business a consumer-ului. Logging-ul bun și correlation ID-urile sunt esențiale.
- Baza de date per serviciu merită complexitatea. Deployabilitatea independentă și izolarea eșecurilor justifică overhead-ul consistenței eventuale.
- Health check-urile nu sunt opționale. Într-o lume Kubernetes, readiness și liveness probe-urile reprezintă diferența dintre un sistem auto-vindecător și eșecuri în cascadă.
- Tooling-ul pentru monorepo contează. Graful de task-uri și caching-ul Turborepo au făcut posibilă dezvoltarea simultană a cinci servicii fără să îmi pierd mințile.
Codul sursă complet: github.com/ionutn0301/microservices-starter-kit