Dans le cycle de vie d'une application à forte croissance, il arrive un moment où la direction et l'équipe technique envisagent de découper le “gros monolithe” en microservices pour régler les lenteurs. En tant qu'architecte, j'ai récemment bloqué cette transition pour une plateforme e-commerce d'envergure. Voici l'ADR (Architecture Decision Record) et les arguments techniques derrière ce choix.
Le mythe des microservices salvateurs
Les microservices résolvent des problèmes d'organisation humaine (quand vous avez 150 développeurs répartis en 15 Feature Teams) et non des problèmes de performance pure. Découper un système trop tôt introduit des complexités majeures :
- Transactions distribuées : Comment garantir la cohérence des données entre le service Commande et le service Facturation sans la complexité extrême du pattern Saga (compensation, requêtes asynchrones complexes) ?
- Latence réseau : Une requête utilisateur simple se transforme en cascade d'appels HTTP internes (ou gRPC), dégradant le temps de réponse global.
- Déploiement et Ops : Gérer 4 pipelines CI/CD, un orchestrateur Kubernetes, un service mesh, le traçage distribué, et la configuration des secrets multiplie la charge de maintenance par dix.
La solution : Le Monolithe Modulaire
Au lieu d'éclater l'infrastructure, nous avons restructuré le code au niveau applicatif pour découpler proprement les domaines métier. En PHP 8+, nous avons implémenté des domaines encapsulés à l'intérieur du framework Laravel.
Voici un exemple de structure modulaire stricte via les espaces de noms (Namespaces) :
// Structure de répertoires stricte app/ ├── Domain/ │ ├── Order/ │ │ ├── Contracts/ │ │ ├── Models/ │ │ └── Services/ │ └── Billing/ │ ├── Contracts/ │ ├── Services/ │ └── Listeners/ └── Providers/
Pour communiquer entre domaines de façon découplée, nous utilisons le système d'événements interne de Laravel au lieu d'appels HTTP internes. C'est l'équivalent d'un bus de messages interne (In-Memory Event Bus) :
namespace App\Domain\Order\Services;
use App\Domain\Order\Events\OrderPlaced;
use Illuminate\Support\Facades\DB;
class CreateOrderService {
public function execute(array $data) {
return DB::transaction(function () use ($data) {
$order = Order::create($data);
// Notification asynchrone interne
event(new OrderPlaced($order));
return $order;
});
}
}Comment nous avons résolu les problèmes de performance
Au lieu de distribuer l'application, nous avons ciblé les réels goulets d'étranglement :
- Optimisation SQL : Mise en place d'index composites sur les tables de recherche et réécriture des requêtes complexes avec des jointures optimisées ou des vues matérialisées.
- Déchargement asynchrone : Utilisation de files d'attente (Queues) gérées par Redis pour toutes les tâches lourdes (envoi de factures, rapports, appels API tiers).
- Mise en cache intelligente : Stratégie de cache à deux niveaux (Redis et cache en mémoire locale de la requête) pour les données d'inventaire peu mobiles.
Décision finale et bilan
Le monolithe modulaire nous a permis de conserver une base de code unique, des transactions de base de données ACID fiables, et des temps de déploiement en une seule étape (Single-Step Deployment). En production, le temps de réponse moyen est tombé sous les 80ms et la vélocité de l'équipe est restée maximale, prouvant qu'une architecture propre vaut mieux qu'une infrastructure sur-dimensionnée et complexe.
