Monolithe ou microservices : comment bien choisir

1. Aperçu général

La question « faut-il choisir monolithe ou microservices » surgit lors de presque chaque conception de système. Cet article ne propose pas une réponse unique valable pour tous, mais vous guide à travers des critères concrets : la taille de l’équipe, la cadence de déploiement, la complexité opérationnelle et le coût de communication entre services. À la fin, vous aurez une checklist pour évaluer où votre système devrait se positionner sur l’axe monolithe – monolithe modulaire – microservices.

2. Monolithe : simple mais souvent sous-estimé

Un monolithe signifie que tous les métiers (Commandes, Clients, Paiements, Inventaire…) s’exécutent dans un seul processus, une seule base de code, un seul déploiement.

@RestController
@RequestMapping("/orders")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
        Order order = orderService.create(request);
        return ResponseEntity.ok(OrderResponse.from(order));
    }
}

@Service
public class OrderService {

    private final InventoryService inventoryService;
    private final PaymentService paymentService;

    public OrderService(InventoryService inventoryService, PaymentService paymentService) {
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
    }

    public Order create(OrderRequest request) {
        inventoryService.reserve(request.productId(), request.quantity());
        paymentService.charge(request.customerId(), request.amount());
        return new Order(request); // on suppose que le constructeur sauvegarde en BD
    }
}

OrderService appelle directement InventoryService et PaymentService par un appel de méthode au sein de la même JVM. Pas de réseau, pas de sérialisation, les transactions peuvent être encapsulées dans un unique @Transactional. C’est le plus grand avantage du monolithe : la cohérence est facile à garantir, le débogage est facile, il suffit de déployer une seule fois.

Les inconvénients apparaissent quand l’équipe grandit : tout le monde modifie la même base de code, le temps de compilation augmente, le risque qu’un petit changement dans un module entraîne une recompilation de l’application entière augmente également.

Note : Un monolithe n’est pas synonyme de « code chaotique ». Un monolithe bien écrit, avec des modules clairement séparés (organisation par domaine métier, pas de dépendances circulaires), fonctionne très bien pour des systèmes de plusieurs millions d’utilisateurs.

3. Microservices : découper pour scaler indépendamment, au prix d’une complexité opérationnelle accrue

Les microservices séparent Order, Inventory, Payment en services distincts, communiquant par le réseau (REST, gRPC, file de messages).

@Service
public class OrderService {

    private final RestClient restClient;

    public OrderService(RestClient restClient) {
        this.restClient = restClient;
    }

    public Order create(OrderRequest request) {
        // Appel du service Inventory via HTTP
        restClient.post()
                .uri("http://inventory-service/reservations")
                .body(new ReservationRequest(request.productId(), request.quantity()))
                .retrieve()
                .toBodilessEntity();

        // Appel du service Payment via HTTP
        restClient.post()
                .uri("http://payment-service/charges")
                .body(new ChargeRequest(request.customerId(), request.amount()))
                .retrieve()
                .toBodilessEntity();

        return new Order(request);
    }
}

Par rapport à la version monolithe de la section 2, la différence clé se situe à restClient.post(...) : l’appel de méthode en-processus s’est transformé en appel HTTP via le réseau. Les conséquences :

  • Chaque appel peut échouer en raison d’un timeout réseau, d’une panne de service, d’une erreur DNS — des erreurs qui n’existent pas dans un monolithe.
  • Il n’y a plus de transaction ACID traversant les services. Vous devez gérer cela avec le pattern saga ou l’eventual consistency.
  • Vous avez besoin d’une service discovery, d’un load-balancer, du distributed tracing pour déboguer en cas de problème.

3.1. Coût caché : les transactions distribuées

@Service
public class OrderSagaOrchestrator {

    public void createOrder(OrderRequest request) {
        try {
            inventoryClient.reserve(request.productId(), request.quantity());
            paymentClient.charge(request.customerId(), request.amount());
        } catch (PaymentFailedException e) {
            // Vous devez écrire manuellement l'étape de compensation pour rollback
            inventoryClient.release(request.productId(), request.quantity());
            throw e;
        }
    }
}

Dans un monolithe, le rollback se fait automatiquement via @Transactional. Dans les microservices, vous devez écrire vous-même la logique de compensation (release après un échec de reserve). C’est du code qui n’existe pas dans la version monolithe mais qui devient obligatoire dans les microservices pour éviter l’incohérence des données.

4. Critères de décision : ne pas juste copier ce que font les grandes entreprises

4.1. Taille de l’équipe et loi de Conway

La loi de Conway : la structure d’un système reflète la structure organisationnelle qui l’a conçu. Si vous avez 5 développeurs et les répartissez en 5 services, chacun gère un service seul — personne pour relire son code, et vous perdez le bénéfice d’avoir plusieurs paires d’yeux sur un bug.

Seuils de référence pratiques :

1–15 dev, 1 équipe           -> Monolithe (modulaire)
15–50 dev, 2–5 équipes       -> Monolithe modulaire ou extraction de 2-3 grands services
50+ dev, plusieurs équipes   -> Microservices selon les bounded contexts

4.2. La cadence de déploiement est-elle vraiment un goulot d’étranglement ?

Si votre équipe déploie le monolithe tous les quinze jours sans conflits de fusion majeurs, découper les services pour « déployer indépendamment » ne crée pas de valeur — cela ajoute juste du coût opérationnel (CI/CD séparé, monitoring séparé, versioning d’API).

Les microservices valent le coup quand : l’équipe A a besoin de déployer 10 fois par jour pour l’évolution du paiement, tandis que l’équipe B déploie l’inventaire une fois par semaine. Les découper permet à A de ne pas attendre B.

4.3. La résilience requise de votre infrastructure

# docker-compose.yml - illustration de la complexité minimale d'infrastructure pour les microservices
services:
  order-service:
    image: order-service:1.0
    depends_on:
      - inventory-service
      - payment-service
      - service-registry
  inventory-service:
    image: inventory-service:1.0
  payment-service:
    image: payment-service:1.0
  service-registry:
    image: eureka-server:1.0
  api-gateway:
    image: spring-cloud-gateway:1.0

Avec juste 3 services métier, vous devez déjà ajouter service-registry et api-gateway. Multipliez par 20 services dans un vrai système, et le nombre de composants augmente exponentiellement — cela exige une équipe DevOps capable de gérer Kubernetes, du centralized logging (ELK), et du distributed tracing (Jaeger/Zipkin).

Note : Si votre entreprise n’a personne maîtrisant l’orchestration de conteneurs et la stack d’observabilité, les microservices transformeront chaque incident en quête « chercher une aiguille dans une botte de foin » en feuilletant les logs de plusieurs services.

5. Monolithe modulaire : une option intermédiaire trop souvent oubliée

Le monolithe modulaire est un monolithe où le code est organisé en modules indépendants, communiquant via des interfaces bien définies, sans dépendances circulaires de base de données.

// Package: com.nonladev.shop.order
public interface InventoryPort {
    void reserve(String productId, int quantity);
}

// Package: com.nonladev.shop.inventory
@Service
public class InventoryService implements InventoryPort {
    @Override
    public void reserve(String productId, int quantity) {
        // logique métier, accès à la BD propre au module inventory
    }
}

OrderService ne dépend que de l’interface InventoryPort, il ne connaît pas l’implémentation du module inventory. De cette façon, si plus tard vous avez besoin de découper inventory en service distinct, il vous suffit de remplacer l’implémentation de InventoryPort par un client HTTP — le code appelant dans OrderService ne change pas.

C’est exactement la stratégie « commencer en monolithe, découper progressivement quand c’est nécessaire » que des grands systèmes (Shopify, GitHub dans ses débuts) ont appliquée avec succès.

6. Tests : mesurer le coût de latence en passant d’appels en-processus à des appels réseau

L’exemple de test ci-dessous illustre la différence de latence — un facteur qu’il faut mesurer concrètement avant de décider de découper un service.

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <scope>test</scope>
</dependency>
class OrderServiceLatencyTest {

    @Test
    void inProcessCallShouldBeFasterThanRemoteCall() {
        InventoryPort inProcess = (productId, qty) -> { /* no-op */ };
        long startInProcess = System.nanoTime();
        inProcess.reserve("P-1", 1);
        long inProcessDuration = System.nanoTime() - startInProcess;

        InventoryPort remoteSimulated = (productId, qty) -> {
            try {
                Thread.sleep(20); // simule un aller-retour réseau d'environ 20ms
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };
        long startRemote = System.nanoTime();
        remoteSimulated.reserve("P-1", 1);
        long remoteDuration = System.nanoTime() - startRemote;

        assertThat(remoteDuration).isGreaterThan(inProcessDuration);
    }
}

Ce test ne mesure pas la vraie performance de votre infrastructure, mais il illustre le principe fondamental : à chaque fois qu’une Order appelle Inventory via le réseau, vous ajoutez quelques dizaines de millisecondes. Si une requête a besoin d’appeler une chaîne de 5 services, les latences s’additionnent et peuvent dépasser votre SLA — c’est un coût souvent oublié quand on dessine l’architecture sur papier.

7. Quand NE PAS choisir les microservices

  • Équipe de moins de 10 personnes : le coût opérationnel (CI/CD, monitoring, on-call) pour plusieurs services dépasse les bénéfices de la séparation.
  • Domaine métier pas encore stable : si le métier évolue constamment (startup en phase de recherche de product-market fit), les limites entre services seront fausses constamment, entraînant des fusions et divisions répétées — doublement de travail.
  • Pas d’infrastructure d’observabilité : sans distributed tracing, sans centralized logging, déboguer les erreurs traversant plusieurs services devient quasi impossible.
  • Besoin d’une forte cohérence transactionnelle (par exemple, core banking) : garder les choses dans une limite transactionnelle unique (monolithe) est plus sûr que d’implémenter des sagas complexes.

À l’inverse, les microservices valent l’investissement quand : plusieurs équipes indépendantes, besoin de scaler certaines parties séparément (par exemple, le service de recherche demande plus de CPU que le service de commandes), ou besoin de technologies différentes selon le domaine (Node.js pour du chat temps-réel, Java pour la facturation).

8. Conclusion

Il n’existe pas d’architecture « juste » en absolu — seulement une architecture adaptée à la taille actuelle de votre équipe, votre cadence de déploiement et votre capacité opérationnelle. Si vous n’êtes pas sûr, commencez avec un monolithe modulaire : maintenez des interfaces claires entre les domaines, évitez les dépendances circulaires de bases de données, pour que si vous devez vraiment découper plus tard (du fait de la scalabilité ou de la croissance de l’équipe), vous n’ayez qu’à changer l’implémentation, pas tout réécrire. Les microservices sont un outil pour résoudre des problèmes organisationnels et d’échelle, pas une fin en soi.

microservicesmonolithsystem-designspring-bootarchitecture