En bref
Une architecture microservices robuste ne se construit pas en un « big bang ». On migre progressivement avec le pattern Strangler Fig, on découpe selon les bounded contexts (DDD) avec une base de données par service, on privilégie les événements Kafka publiés de façon fiable (Outbox), on gère les transactions avec des Sagas, et on rend le tout observable et résilient. Le succès se mesure avec les indicateurs DORA, pas au nombre de services.
Microservice
Service autonome, déployable seul, qui porte une capacité métier complète et possède ses propres données.
Monolithe modulaire
Application déployée d’un bloc mais découpée en modules aux frontières claires, alignés sur les domaines métier.
Monolithe distribué
Anti-pattern : des services séparés mais si couplés qu’ils doivent être modifiés et déployés ensemble.
DDD (Domain-Driven Design)
Approche de conception qui aligne le code sur le langage et les règles du métier.
Bounded context
Frontière à l’intérieur de laquelle un modèle métier a un sens précis. En microservices : idéalement un service par contexte.
Event Storming
Atelier collaboratif avec le métier pour cartographier les événements d’un domaine et repérer les bounded contexts.
Strangler Fig
Pattern de migration progressive : une façade route le trafic, et les fonctionnalités sont extraites du monolithe une à une.
API Gateway
Point d’entrée unique des clients : routage, authentification, rate limiting et fallbacks.
Database per service
Règle selon laquelle chaque service possède sa propre base ; aucun autre service n’accède directement à ses tables.
Apache Kafka
Plateforme de streaming d’événements distribuée, utilisée pour la communication asynchrone entre services.
Clé de partition Kafka
Clé d’un message qui détermine sa partition ; les messages de même clé sont lus dans l’ordre.
Transactional Outbox
Pattern qui enregistre l’événement dans une table outbox, dans la même transaction que la donnée métier, avant de le publier.
CDC / Debezium
Change Data Capture : lecture du journal de la base pour publier les changements. Debezium est l’outil open source de référence.
At-least-once
Garantie de livraison : chaque message arrive au moins une fois, d’éventuels doublons sont possibles.
Consommateur idempotent
Consommateur qui produit le même résultat s’il reçoit plusieurs fois le même événement.
Saga
Suite de transactions locales coordonnées par événements, avec des compensations en cas d’échec, à la place d’une transaction distribuée.
Chorégraphie / orchestration
Deux styles de Saga : chaque service réagit aux événements (chorégraphie) ou un orchestrateur central pilote les étapes.
Compensation
Action métier qui annule l’effet d’une étape déjà réalisée (ex. rembourser un paiement).
2PC / XA
Validation en deux phases : protocole de transaction distribuée, à éviter entre microservices.
Circuit breaker
Coupe-circuit qui cesse temporairement d’appeler un service en échec, pour éviter les pannes en cascade.
Resilience4j
Bibliothèque Java de résilience : circuit breaker, retry, rate limiter, bulkhead, time limiter.
Rate limiting
Limitation du nombre de requêtes par client et par période, pour protéger les services.
Service mesh (Istio)
Couche d’infrastructure qui gère au niveau réseau le trafic, la sécurité mTLS et la résilience entre services.
OpenTelemetry
Standard ouvert de collecte des traces, métriques et logs, indépendant de l’outil de visualisation.
Tracing distribué
Suivi d’une requête à travers tous les services, grâce à un identifiant commun (traceId).
Indicateurs DORA
Quatre métriques de performance de livraison : fréquence de déploiement, délai de mise en production, taux d’échec des changements, temps de restauration.
Lag consumer
Retard d’un consommateur Kafka : nombre de messages publiés mais pas encore traités.
Tests de contrat
Tests qui vérifient qu’un fournisseur d’API respecte ce qu’attendent ses consommateurs (Pact, Spring Cloud Contract).
Probes liveness / readiness
Sondes Kubernetes : la première détecte un service bloqué à redémarrer, la seconde indique s’il peut recevoir du trafic.
Introduction
Passer d’un monolithe à des microservices est un tournant majeur dans la vie d’un système. Il ne s’agit pas de « découper en petits services », mais de construire une architecture distribuée résiliente, observable et maintenable par des équipes autonomes.
À l’intérieur de chaque service, l’architecture hexagonale avec Spring Boot garde le domaine métier indépendant de Kafka, de la base et du framework. Et quand la base d’un service grossit, le partitionnement PostgreSQL permet de la faire monter en charge sans redécouper le service.
Comme le souligne Sam Newman dans Building Microservices, les microservices ne sont pas un objectif en soi : ce sont un moyen de gagner en autonomie d’équipe, en vitesse de livraison et en capacité de montée en charge. Si ces bénéfices ne sont pas attendus, un monolithe modulaire reste souvent le meilleur choix.
Ce guide présente une démarche éprouvée, étape par étape, avec du code Spring Boot 3, Kafka et Spring Cloud Gateway.
Problématique observée
- Monolithe en croissance : chaque déploiement devient risqué et lent, les équipes se marchent dessus.
- Couplage fort : une petite modification dans un module impacte plusieurs autres.
- Scalabilité limitée : impossible de scaler uniquement le module critique (ex. le paiement pendant les soldes).
- Time-to-market allongé : plusieurs équipes doivent se synchroniser pour une release globale.
Tentatives ratées (leçons apprises)
- Découpage prématuré : extraire des services sans comprendre les domaines métier produit une explosion d’appels inter-services, un « monolithe distribué » pire que l’original.
- Microservices trop petits : un service par entité (Client, Adresse, Ligne de commande…) multiplie les coûts d’orchestration, de déploiement et de supervision.
- Base de données partagée : les services restent couplés par le schéma ; une migration SQL casse trois équipes.
- Mélange de protocoles sans standard : REST, gRPC et SOAP sans gouvernance créent une dette technique immédiate.
- Double écriture DB + Kafka : sauvegarder en base puis publier un message sans garantie atomique fait perdre des événements à la première panne.
Vue d’ensemble de l’architecture cible
Solution technique : la migration en 8 étapes
Étape 1 — Migrer progressivement avec le Strangler Fig
Plutôt qu’une réécriture complète, on place une façade (l’API Gateway) devant le monolithe, puis on extrait les fonctionnalités une à une. Chaque route basculée vers un nouveau service est un petit déploiement réversible. Le monolithe « s’étiole » jusqu’à disparaître, comme l’arbre étouffé par le figuier étrangleur qui a donné son nom au pattern (Martin Fowler).
Commencez par un contexte à forte valeur et faible couplage (souvent le paiement ou le catalogue), pas par le cœur le plus enchevêtré.
Étape 2 — Découper selon les bounded contexts (DDD)
Un service correspond à un bounded context, pas à une entité ou à une table :
- Identité (User Service) : profils, rôles, préférences.
- Commande (Order Service) : panier, commandes, statuts.
- Paiement (Payment Service) : autorisations, remboursements.
Règle non négociable : une base de données par service. Un service ne lit jamais les tables d’un autre ; il passe par son API ou consomme ses événements. Un atelier d’Event Storming avec le métier est le moyen le plus rapide d’identifier ces frontières.
Étape 3 — Choisir le bon mode de communication
| Besoin | Mode | Technologie | Exemple |
|---|---|---|---|
| Réponse immédiate à l’utilisateur | Synchrone | REST ou gRPC | Afficher le profil client |
| Notifier d’autres services d’un fait métier | Asynchrone | Kafka (événements) | OrderPlaced |
| Traitement long ou en lot | Asynchrone | Kafka ou RabbitMQ | Génération de factures |
| Transaction sur plusieurs services | Asynchrone | Saga + événements | Commande → paiement → stock |
Par défaut, privilégiez l’asynchrone entre services : un service indisponible ne bloque plus toute la chaîne. Gardez le synchrone pour les lectures dont l’utilisateur attend la réponse, toujours protégées par un timeout et un circuit breaker.
Étape 4 — Publier des événements fiables avec l’Outbox
Le piège classique : sauvegarder la commande en base, puis appeler kafkaTemplate.send(). Si l’application tombe entre les deux, la commande existe mais l’événement est perdu. Le Transactional Outbox résout ce problème : l’événement est écrit dans une table outbox dans la même transaction que la commande, puis un relais le publie dans Kafka.
La table outbox :
CREATE TABLE outbox (
id UUID PRIMARY KEY,
aggregate_id VARCHAR(64) NOT NULL,
event_type VARCHAR(128) NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ
);
Le service métier écrit la commande et l’événement dans une seule transaction :
@Service
public class OrderService {
private final OrderRepository orders;
private final OutboxRepository outbox;
public OrderService(OrderRepository orders, OutboxRepository outbox) {
this.orders = orders;
this.outbox = outbox;
}
@Transactional
public Order placeOrder(PlaceOrderCommand cmd) {
Order order = orders.save(Order.create(cmd));
outbox.save(OutboxEvent.of(order.getId(), "OrderPlaced",
new OrderPlaced(order.getId(), order.getCustomerId(), order.getTotal())));
return order;
}
}
Le relais publie les événements en attente. La clé Kafka est l’identifiant de la commande : tous les événements d’une même commande arrivent dans la même partition, donc dans l’ordre.
@Component
public class OutboxRelay {
private final OutboxRepository outbox;
private final KafkaTemplate<String, String> kafka;
public OutboxRelay(OutboxRepository outbox, KafkaTemplate<String, String> kafka) {
this.outbox = outbox;
this.kafka = kafka;
}
@Scheduled(fixedDelay = 500)
@Transactional
public void publishPending() {
// SELECT ... FOR UPDATE SKIP LOCKED : plusieurs instances sans doublon de traitement
for (OutboxEvent e : outbox.lockNextBatch(100)) {
ProducerRecord<String, String> record =
new ProducerRecord<>("orders.events", e.getAggregateId(), e.getPayload());
record.headers().add("eventId", e.getId().toString().getBytes(StandardCharsets.UTF_8));
record.headers().add("eventType", e.getEventType().getBytes(StandardCharsets.UTF_8));
kafka.send(record).join();
e.markPublished();
}
}
}
En production, Debezium (Change Data Capture) et son Outbox Event Router remplacent avantageusement le poller : pas de requêtes périodiques, latence plus faible.
Comme la livraison est « at-least-once », un consommateur peut recevoir deux fois le même événement. Il doit donc être idempotent :
@Component
public class OrderEventsListener {
private final ProcessedEventRepository processed;
private final PaymentService payments;
public OrderEventsListener(ProcessedEventRepository processed, PaymentService payments) {
this.processed = processed;
this.payments = payments;
}
@KafkaListener(topics = "orders.events", groupId = "payment-service")
@Transactional
public void on(ConsumerRecord<String, String> record) {
String eventId = new String(record.headers().lastHeader("eventId").value(), StandardCharsets.UTF_8);
if (processed.existsById(eventId)) {
return; // doublon : déjà traité
}
payments.authorize(record.value());
processed.save(new ProcessedEvent(eventId));
}
}
Étape 5 — Remplacer les transactions distribuées par une Saga
Pas de transaction XA/2PC entre services. Une Saga enchaîne des transactions locales ; si une étape échoue, des actions de compensation annulent les étapes précédentes. En chorégraphie, chaque service réagit aux événements des autres ; en orchestration, un orchestrateur central pilote les étapes (plus lisible au-delà de 4 ou 5 étapes).
Saga, Outbox, Circuit breaker : ce sont des design patterns appliqués à l’échelle d’un système distribué. Pour revoir les fondamentaux, lisez Design patterns : la base de tout développement logiciel robuste.
Étape 6 — Une API Gateway comme point d’entrée unique
La gateway centralise l’authentification (OAuth2/JWT), le routage, le rate limiting et les fallbacks. Avec Spring Cloud Gateway :
spring:
cloud:
gateway:
server:
webflux: # Spring Cloud 2025+ (avant : spring.cloud.gateway.routes)
routes:
- id: order-service
uri: http://order-service:8080
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
- name: CircuitBreaker
args:
name: ordersCB
fallbackUri: forward:/fallback/orders
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 50
redis-rate-limiter.burstCapacity: 100
À noter : depuis Spring Cloud 2025, les propriétés de la gateway sont préfixées par spring.cloud.gateway.server.webflux (ou webmvc). Le RequestRateLimiter nécessite Redis et un bean KeyResolver (par utilisateur ou par IP).
Étape 7 — Rendre chaque appel synchrone résilient
Tout appel réseau finira par échouer. Avec Resilience4j, on combine timeout, retry et circuit breaker, avec un fallback dégradé plutôt qu’une erreur en cascade :
@Service
public class UserProfileClient {
private final RestClient restClient;
private final ProfileCache cache;
public UserProfileClient(RestClient.Builder builder, ProfileCache cache) {
this.restClient = builder.baseUrl("http://user-service:8080").build();
this.cache = cache;
}
@CircuitBreaker(name = "userProfile", fallbackMethod = "cachedProfile")
@Retry(name = "userProfile")
public UserProfile fetch(UUID userId) {
return restClient.get().uri("/users/{id}", userId).retrieve().body(UserProfile.class);
}
private UserProfile cachedProfile(UUID userId, Throwable cause) {
return cache.getOrDefault(userId, UserProfile.anonymous(userId));
}
}
resilience4j:
circuitbreaker:
instances:
userProfile:
sliding-window-size: 20
failure-rate-threshold: 50
wait-duration-in-open-state: 10s
retry:
instances:
userProfile:
max-attempts: 3
wait-duration: 200ms
Sur Kubernetes, un service mesh comme Istio peut porter ces politiques au niveau réseau, sans code applicatif.
Étape 8 — Observabilité et déploiement
Sans observabilité, un système distribué est une boîte noire. Les trois piliers :
- Traces distribuées : OpenTelemetry (via Micrometer Tracing dans Spring Boot) exporte vers Jaeger ou Grafana Tempo ; chaque requête est suivie de la gateway jusqu’au consommateur Kafka.
- Métriques : Prometheus + Grafana (latence p95, taux d’erreur, lag des consumers Kafka).
- Logs centralisés : ELK ou Loki, toujours avec le
traceIdpour corréler.
# application.yml (Spring Boot 3.x)
management:
tracing:
sampling:
probability: 0.1
otlp:
tracing:
endpoint: http://otel-collector:4318/v1/traces
endpoints:
web:
exposure:
include: health,prometheus
Côté déploiement : une image Docker par service, Kubernetes pour le scaling et l’auto-réparation, des probes liveness/readiness (exposées par Spring Boot Actuator), et une pipeline CI/CD par service avec tests de contrat (Spring Cloud Contract ou Pact) pour livrer sans casser les consommateurs.
Mesurer les résultats : les indicateurs DORA
Le succès d’une migration ne se mesure pas au nombre de services, mais à la capacité de livrer vite et sans casse. Mesurez avant la migration pour avoir une base de comparaison.
| Indicateur DORA | Ce qu’il mesure | Ce que les microservices doivent améliorer |
|---|---|---|
| Fréquence de déploiement | Combien de mises en production par équipe | Chaque équipe déploie seule, à la demande |
| Délai de mise en production | Du commit à la production | Pipelines courtes, par service |
| Taux d’échec des changements | Part des déploiements qui causent un incident | Déploiements plus petits, donc moins risqués |
| Temps de restauration | Durée pour rétablir le service après incident | Panne isolée, rollback d’un seul service |
Complétez avec des métriques techniques par service : latence p95, taux d’erreur, disponibilité (SLO) et lag des consumers Kafka.
Bonnes pratiques à appliquer
- Gouvernance API : contrats versionnés et documentés (OpenAPI pour REST, AsyncAPI ou Schema Registry pour Kafka).
- Asynchrone par défaut entre services, synchrone uniquement pour les lectures temps réel.
- Jamais de transaction distribuée : Saga + Outbox + consommateurs idempotents.
- Une base par service, sans exception.
- CI/CD indépendante par service, avec tests de contrat.
- Taille des services : assez petits pour qu’une équipe les maîtrise, assez grands pour porter une capacité métier complète.
- Sécurité dès le départ : JWT validé à la gateway, mTLS entre services, secrets hors du code.
Quand ne PAS passer aux microservices ?
- Une seule petite équipe (moins de 10 développeurs) sur un seul produit.
- Un domaine métier encore flou, qui change chaque mois.
- Pas encore de CI/CD automatisée ni de supervision.
Dans ces cas, un monolithe modulaire bien découpé (modules alignés sur les bounded contexts) offre l’essentiel des bénéfices, et prépare une extraction future.
Références
- Sam Newman — Building Microservices, 2e édition (O’Reilly, 2021)
- Sam Newman — Monolith to Microservices (O’Reilly, 2019)
- Chris Richardson — Microservices Patterns (Manning) et microservices.io (Saga, Transactional Outbox)
- Michael T. Nygard — Release It!, 2e édition (patterns de résilience)
- Neal Ford, Mark Richards, Pramod Sadalage, Zhamak Dehghani — Software Architecture: The Hard Parts
- Vaughn Vernon — Domain-Driven Design Distilled
- Martin Fowler — Strangler Fig Application et Microservices Guide
- DORA — les quatre indicateurs clés
- Documentation Spring Cloud Gateway
FAQ
Peut-on faire des microservices sans DDD ?
Techniquement oui, mais sans bounded contexts clairs, on obtient presque toujours un « monolithe distribué » : des services qui s’appellent en permanence et doivent être déployés ensemble. C’est pire qu’un monolithe classique.
Comment gérer une transaction qui touche plusieurs services ?
Avec le pattern Saga : une suite de transactions locales coordonnées par des événements (chorégraphie) ou par un orchestrateur, avec des actions de compensation en cas d’échec. Combinez-le avec l’Outbox pour ne perdre aucun événement.
Pourquoi ne pas simplement appeler kafkaTemplate.send() après l’enregistrement en base ?
Parce que les deux opérations ne sont pas atomiques. Si l’application tombe entre l’écriture en base et l’envoi, l’événement est perdu. Le pattern Transactional Outbox écrit l’événement dans la même transaction que la donnée métier.
Les microservices sont-ils adaptés à toutes les entreprises ?
Non. Pour une startup ou une petite équipe, un monolithe modulaire est souvent préférable. Les microservices deviennent rentables quand plusieurs équipes doivent livrer indépendamment.
Par où commencer la migration d’un monolithe ?
Placez une API Gateway devant le monolithe (Strangler Fig), mesurez les indicateurs DORA, puis extrayez un premier contexte à forte valeur et faible couplage, comme le paiement ou le catalogue.
Quelle stack technique recommander ?
Backend : Spring Boot, Quarkus ou Micronaut. Communication : REST ou gRPC pour le synchrone, Kafka pour les événements. Infrastructure : Docker, Kubernetes, éventuellement Istio. Observabilité : OpenTelemetry, Prometheus, Grafana, Jaeger ou Tempo.
Pour aller plus loin
Kafka, Docker et Spring Boot sont au cœur de cette architecture. Nos formations Java, Spring Boot, Kafka et Docker vous font construire ces patterns pas à pas, cluster Kafka local inclus.




