À proposExpertiseProjetsParcoursBlogContact
Discuter
À proposExpertiseProjetsParcoursBlogContactDiscuter

Yao David Logan

Software Engineer fullstack spécialisé en SaaS, automatisation métier et plateformes web/mobile scalables.

NavigationExpertiseProjetsParcoursBlogContact
LiensGitHubLinkedInEmail
© 2026 Yao David Logan. Tous droits réservés.
Microservices Node.js sans dette — REX d'une plateforme fintech multi-canal
Retour au blog

Tech

Xin@

Microservices Node.js sans dette — REX d'une plateforme fintech multi-canal

YDLYao David Logan16 min de lecture18 septembre 2026

CatégorieTech

Lecture16 min de lecture

Publié18 septembre 2026

Vues0

Partages0

Commentaires0

Sommaire
  1. 011. Le contexte
  2. 022. Symptômes qui m'auraient fait reculer
  3. 033. L'approche
  4. 044. Cas concret : un webhook qui retentait 200 fois
  5. 055. PostgreSQL + Redis — qui fait quoi
  6. 066. Le WebSocket de l'admin
  7. 077. Migrations sans downtime
  8. 088. Pièges à éviter
  9. 099. Le bilan
  10. 1010. Conclusion

Retour d'expérience sur l'architecture microservices Node.js d'une plateforme d'investissement financier en production : trois services, PostgreSQL + Redis, webhooks idempotents, zéro downtime.

Construire une plateforme fintech multi-canal en microservices Node.js, sans tomber dans le piège du "microservice pour le microservice". Retour d'expérience sur Nexus, sept mois de production.

1. Le contexte

Nexus Corporation voulait une plateforme d'investissement financier servant simultanément un site web public, une application mobile cross-platform, un dashboard admin et un blog technique — le tout sur un backend unifié. Le piège classique aurait été d'empiler des microservices par mimétisme. La promesse marketing "microservices" cache souvent un monolithe distribué : pénible à débugger, lent à déployer, fragile sous charge.

J'ai pris le pari inverse : commencer par un monolithe modulaire bien découpé, puis extraire en microservices uniquement les domaines qui le justifient — paiement, scoring, sessions. Trois critères m'ont guidé : besoin de scaler indépendamment, contrainte de sécurité différente, vitesse de déploiement spécifique.

Le résultat : trois services (auth/RBAC, investissement, paiement) derrière une API gateway Node.js, partageant PostgreSQL pour le relationnel et Redis pour le cache et les sessions volatiles. Les surfaces (web Next.js 14, app mobile React Native, admin) consomment la même API.

2. Symptômes qui m'auraient fait reculer

Avant de découper, j'ai listé les signaux qu'on observait sur des architectures concurrentes :

  • Déploiements monolithiques de 12+ minutes qui bloquent les hotfixes
  • Pics de charge sur les routes paiement qui font tomber l'authentification
  • Webhooks de passerelle paiement qui retentent en boucle parce que le service est indisponible
  • Migration de schéma qui locke la table pendant 4 minutes en production
  • Logs noyés dans le bruit, impossible d'isoler une transaction

Aucun de ces symptômes n'était lié au monolithe en lui-même. Tous étaient liés à un mauvais découpage de domaines. Mais le découpage en services force la discipline.

3. L'approche

3.1 Le service gateway

Le gateway est délibérément mince. Il fait trois choses : router, authentifier, journaliser. Pas de logique métier. Pas d'accès direct à la base. Sa seule autonomie : refuser une requête mal formée, rate-limiter, et émettre un trace ID propagé à tous les services downstream.

TypeScript
1// src/middlewares/traceContext.ts
2import { randomUUID } from "node:crypto";
3import type { Request, Response, NextFunction } from "express";
4 
5export function traceContext(req: Request, res: Response, next: NextFunction) {
6 const traceId = req.header("x-trace-id") ?? randomUUID();
7 res.setHeader("x-trace-id", traceId);
8 // Disponible dans tous les services downstream via le client interne
9 req.app.locals.traceId = traceId;
10 next();
11}

3.2 Le service auth & RBAC

Séparé pour deux raisons : sa surface de sécurité diffère du reste (rotation de secrets, audit obligatoire), et il doit scaler indépendamment quand les sessions explosent (typiquement à l'ouverture du marché).

Le contrat est étroit : un endpoint POST /sessions qui retourne un JWT court (15 min) + un refresh token long (7 jours, stocké hashé en DB). Les rôles sont matérialisés dans le claim JWT, vérifiés à chaque requête downstream sans rappel.

3.3 Le service paiement

C'est le plus instructif. Il intègre plusieurs passerelles (carte, mobile money, virement), chacune avec son propre format de webhook et sa propre logique d'idempotence. Le service expose une API unifiée : createPayment, confirmPayment, refundPayment. À l'intérieur, un adapter pattern isole chaque passerelle dans son propre module.

TypeScript
1// src/services/payment/PaymentGateway.ts
2export interface PaymentGateway {
3 readonly name: string;
4 initiate(input: PaymentInitiateInput): Promise<PaymentSession>;
5 confirm(sessionId: string, evidence: WebhookPayload): Promise<PaymentResult>;
6 refund(transactionId: string, amount: number): Promise<RefundResult>;
7}

Chaque passerelle implémente l'interface. Le service paiement choisit la bonne via un router basé sur le moyen demandé par le client. Les webhooks sont reçus sur des routes dédiées par passerelle (/webhooks/cb, /webhooks/mtn, /webhooks/orange) puis dispatchés via un job idempotent.

4. Cas concret : un webhook qui retentait 200 fois

Premier mois en production, un webhook d'une passerelle locale a tenté 200 fois la même notification. Le bug venait d'une accusation de réception 200 OK trop tardive : nous traitions le webhook en synchrone (vérification base + mise à jour + notification utilisateur), et certaines transactions prenaient 8 secondes. La passerelle considérait l'envoi comme échoué et rejouait.

La correction : accuser réception immédiatement et déléguer le traitement à un job en arrière-plan, idempotent par event_id.

TypeScript
1// src/controllers/payment/webhookHandler.ts
2export async function handleGatewayWebhook(req: Request, res: Response) {
3 const payload = req.body as WebhookPayload;
4 
5 // 1. Idempotence — refuse les rejeux silencieusement
6 const exists = await WebhookEvent.findOne({
7 where: { gatewayId: payload.gateway, eventId: payload.id },
8 });
9 if (exists) return res.status(200).json({ ok: true, deduped: true });
10 
11 // 2. Persiste l'event AVANT tout traitement
12 await WebhookEvent.create({
13 gatewayId: payload.gateway,
14 eventId: payload.id,
15 rawBody: payload,
16 status: "received",
17 });
18 
19 // 3. Accuse réception immédiatement
20 res.status(200).json({ ok: true });
21 
22 // 4. Traitement asynchrone via la queue
23 await paymentQueue.add("process-webhook", { eventId: payload.id });
24}

Trois lignes essentielles : persister avant de traiter, accuser réception tôt, traiter en queue idempotente. Les retries de la passerelle deviennent inoffensifs.

5. PostgreSQL + Redis — qui fait quoi

PostgreSQL stocke ce qui doit survivre : transactions, comptes, soldes, journaux d'audit. Toutes les opérations financières sont en transactions ACID avec niveau d'isolation SERIALIZABLE sur les comptes.

Redis stocke ce qui peut disparaître : sessions JWT révoquées (TTL 15 min), rate-limit par IP, cache de prix temps réel (TTL 2 secondes), et un publish/subscribe pour pousser les flux WebSocket à l'admin.

Règle de pouce : si la donnée doit survivre à un redémarrage, elle est dans Postgres. Si elle peut être recalculée en moins de 5 secondes, Redis suffit.

6. Le WebSocket de l'admin

Le dashboard admin doit afficher les transactions en temps réel. La solution naïve — polling toutes les 2 secondes — saturerait la base. À la place : Redis Pub/Sub côté serveur, Socket.IO côté client. Chaque service publie ses événements (payment.confirmed, user.kyc.validated, investment.created) sur un canal Redis. Le gateway s'abonne et pousse aux clients admins authentifiés.

TypeScript
1// src/services/realtime/eventBus.ts
2import { redis } from "../../config/redis.js";
3 
4type RealtimeEvent = "payment.confirmed" | "user.kyc.validated" | "investment.created";
5 
6export async function publishEvent<T>(event: RealtimeEvent, payload: T) {
7 await redis.publish(`events:${event}`, JSON.stringify({ event, payload, at: Date.now() }));
8}

Côté admin : un seul socket multiplexé par utilisateur, filtrage des événements selon le rôle, déduplication par hash sur les 30 dernières secondes.

7. Migrations sans downtime

Les migrations de schéma sur une plateforme financière en production sont à haut risque. Trois règles non négociables :

  1. Backward compatible d'abord — toute migration doit pouvoir tourner sans casser la version précédente du code. Ajouter une colonne nullable, jamais renommer.
  2. Pas de ALTER TABLE qui locke — sur Postgres, ADD COLUMN ... DEFAULT rewrite la table entière (sauf en 11+). Toujours préférer ADD COLUMN nullable puis UPDATE par lot.
  3. Un seul changement par migration — sinon le rollback partiel est ingérable.
SQL
1-- ✅ Bon : ajoute la colonne nullable, sera remplie progressivement
2ALTER TABLE transactions ADD COLUMN risk_score INTEGER NULL;
3 
4-- ❌ Mauvais : locke la table pour rewrite
5ALTER TABLE transactions ADD COLUMN risk_score INTEGER NOT NULL DEFAULT 0;

8. Pièges à éviter

PiègeSymptômeCorrection
Microservice prématuréLatence inter-service > latence DBReculer vers un monolithe modulaire
Pas d'idempotence sur webhooksDoublons de transactionsTable webhook_events + queue idempotente
Auth dans chaque serviceLogique dupliquée, drift de règlesService auth centralisé, JWT avec claims
Logs sans trace IDImpossible d'isoler une requêteTrace ID propagé en header inter-service
Schemas partagés via DBCouplage caché, déploiements groupésSchemas privés par service, contracts JSON
Pas de circuit breakerUne passerelle lente fait tomber toutTimeout + circuit breaker par adapter

9. Le bilan

Sept mois plus tard, la plateforme tourne avec un uptime mesuré à 99,9 %, une latence p95 sur les routes critiques sous 200 ms, et zéro incident de déploiement bloquant en production. Le découpage en trois services s'est révélé suffisant — je n'ai pas eu besoin d'aller plus loin.

Le vrai gain n'est pas technique. C'est la clarté d'intention : chaque service a un propriétaire métier, une responsabilité étroite, un cycle de release indépendant. L'équipe peut livrer du paiement sans craindre de casser l'authentification.

10. Conclusion

Les microservices ne sont pas un objectif. Ce sont un outil pour résoudre des problèmes spécifiques : scalabilité différenciée, contraintes de sécurité distinctes, cadence de release indépendante. Si vous n'avez aucun de ces trois problèmes, restez sur un monolithe modulaire — il est dix fois plus rapide à livrer et cinq fois plus facile à débugger.

Le bon découpage de domaines compte plus que le nombre de services. Une plateforme bien structurée en trois services bat n'importe quelle architecture distribuée bricolée en quinze.

Commentaires

Réactions des lecteurs

Aucun commentaire pour l'instant

Aucun spam — l'email sert uniquement à vérifier votre identité.

·

Soyez le premier à réagir à cet article.

←PrécédentPaiement multi-passerelles et chiffrement E2E — le pattern qui ne casse pasTech · 18 min de lecture
Prochain échange

Transformer cette lecture en décision produit.

Si ce sujet ressemble à un problème réel dans votre produit, je peux intervenir sur le diagnostic, l'architecture, le backend, l'interface et les automatisations qui rendent une plateforme exploitable.

Format
CDI, freelance, mission longue
Focus
SaaS, API, back-office, automatisation
Discuter du sujetTélécharger le CV

Newsletter

Recevoir les prochaines notes techniques.

Une sélection courte sur SaaS, architecture backend, automatisation métier et qualité produit. Pas de bruit, seulement des idées applicables.