Quand découper votre application : un guide de décision architecturale
La plus grande erreur architecturale des équipes n'est pas de choisir le mauvais pattern — c'est de choisir trop tôt. Voici le cadre de décision que j'utilise pour déterminer si un produit est prêt pour les microservices, et comment migrer sans tout casser en production.
L'erreur architecturale la plus coûteuse n'est pas de choisir les microservices plutôt qu'un monolithe. C'est de choisir l'un ou l'autre avant de comprendre suffisamment votre domaine pour savoir où se trouvent les frontières. Chez Fygurs, nous avons commencé avec un monolithe et l'avons découpé en quatre services. Voici le cadre de décision que j'aurais aimé avoir dès le départ — et ce que je changerais dans la séquence que nous avons suivie.
Pourquoi l'Architecture est Importante
L'architecture ne consiste pas à choisir le « meilleur » pattern. C'est choisir le bon pattern pour votre contexte. Une startup avec trois développeurs a des besoins différents d'une entreprise avec cinquante équipes. Une plateforme de trading en temps réel a des exigences différentes d'un système de gestion de contenu.
Le mauvais choix architectural entraîne :
- Complexité prématurée : Construire des microservices alors qu'un monolithe suffirait
- Goulots d'étranglement au scaling : Atteindre les limites du monolithe quand vous avez besoin d'un scaling indépendant
- Friction d'équipe : Une architecture qui ne correspond pas à la structure de votre organisation
- Dette technique : Des patterns qui luttent contre les frontières naturelles de votre domaine
L'Architecture Monolithique
Un monolithe est une unité déployable unique contenant toutes les fonctionnalités de l'application. L'ensemble du codebase vit dans un seul dépôt, compile en un seul artefact et se déploie comme un seul processus.
ARCHITECTURE MONOLITHIQUE
┌─────────────────────────────────────────────────────┐
│ Load Balancer │
└─────────────────────────┬───────────────────────────┘
│
┌─────────────────────────▼───────────────────────────┐
│ Application Monolithique │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Module │ │ Module │ │ Module │ │
│ │ Utilisateur │ │ Commandes │ │ Paiement │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ┌──────▼───────────────▼───────────────▼──────┐ │
│ │ Couche de Données Partagée │ │
│ └─────────────────────┬───────────────────────┘ │
└─────────────────────────┼───────────────────────────┘
│
┌─────────────────────────▼───────────────────────────┐
│ Base de Données Unique (PostgreSQL) │
└─────────────────────────────────────────────────────┘
Caractéristiques des Monolithes
- Codebase unique : Tout le code dans un seul dépôt, un seul processus de build
- Mémoire partagée : Les modules communiquent via des appels de fonction, pas par le réseau
- Base de données unique : Tous les modules partagent la même instance de base de données
- Déploiement atomique : Tout se déploie ensemble ou pas du tout
- Technologie unifiée : Un langage, un framework, un runtime
Avantages des Monolithes
Simplicité de Développement
Tout est au même endroit. Pas d'appels réseau entre services, pas de débogage distribué, pas de découverte de services. Vous pouvez tracer une requête de l'entrée HTTP jusqu'à la requête base de données en une seule session de débogage.
@Injectable()
export class OrderService {
constructor(
private userService: UserService,
private paymentService: PaymentService,
private inventoryService: InventoryService,
) {}
async createOrder(userId: string, items: OrderItem[]) {
const user = await this.userService.findById(userId);
const inventory = await this.inventoryService.checkAvailability(items);
const payment = await this.paymentService.processPayment(user, items);
return this.orderRepository.create({
userId,
items,
paymentId: payment.id,
});
}
}
Dans un monolithe, c'est une simple chaîne d'appels de fonction. Pas de latence réseau, pas de surcharge de sérialisation, pas de scénarios d'échec partiel.
Tests Plus Faciles
Les tests d'intégration s'exécutent contre une seule application. Pas besoin de démarrer plusieurs services, de mocker des APIs externes ou de gérer une configuration de données de test distribuée.
describe("OrderService", () => {
let orderService: OrderService;
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
orderService = module.get(OrderService);
});
it("should create order with payment", async () => {
const order = await orderService.createOrder("user-123", mockItems);
expect(order.paymentId).toBeDefined();
expect(order.status).toBe("CONFIRMED");
});
});
Déploiement Straightforward
Un seul artefact à builder, un seul déploiement à gérer, un seul rollback si ça tourne mal. Pas de coordination entre services, pas de problèmes de compatibilité de versions d'API.
Transactions ACID
Les transactions de base de données couvrent l'intégralité de l'opération. Si le paiement échoue, la commande n'existe pas. Pas de cohérence éventuelle, pas de transactions compensatoires, pas de patterns de saga distribués.
@Injectable()
export class OrderService {
constructor(private prisma: PrismaService) {}
async createOrderWithTransaction(userId: string, items: OrderItem[]) {
return this.prisma.$transaction(async (tx) => {
const inventory = await tx.inventory.decrementMany(items);
const payment = await tx.payment.create({ data: { userId, amount: total } });
const order = await tx.order.create({
data: { userId, items, paymentId: payment.id },
});
return order;
});
}
}
Défis des Monolithes
Limitations au Scaling
Vous scalez tout ou rien. Si le module de paiement a besoin d'une capacité 10x mais que la gestion des utilisateurs n'en a besoin que de 1x, vous déployez quand même 10 instances de l'application entière.
Lock-in Technologique
L'application entière utilise une seule stack. Si Python est meilleur pour vos fonctionnalités ML mais que votre monolithe est en Node.js, vous avez des options limitées.
Risque de Déploiement
Un bug dans n'importe quel module peut faire tomber toute l'application. Une fuite mémoire dans les avatars utilisateurs affecte le traitement des paiements.
Couplage d'Équipe
De grandes équipes travaillant sur un seul codebase entraînent des conflits de merge, une coordination de déploiement et des frontières de propriété floues.
Croissance des Temps de Build
À mesure que le codebase grandit, les temps de build et de test augmentent. Ce qui commençait comme des builds de 2 minutes devient des pipelines CI de 30 minutes.
L'Architecture Microservices
Les microservices décomposent une application en petits services indépendamment déployables. Chaque service possède son domaine, ses données et son cycle de vie de déploiement.
ARCHITECTURE MICROSERVICES
┌─────────────────────────────────────────────────────┐
│ Load Balancer │
└─────────────────────────┬───────────────────────────┘
│
┌─────────────────────────▼───────────────────────────┐
│ API Gateway │
│ • Authentification • Rate Limiting │
│ • Routage • Composition API │
└───────┬─────────────┬─────────────┬─────────────────┘
│ │ │
│ TCP │ TCP │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│Service │ │Service │ │Service │
│Utilisat.│ │Commandes│ │Paiement │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ DB │ │ DB │ │ DB │
│(Postgres)│ │(MongoDB)│ │Paiement │
└─────────┘ └─────────┘ └─────────┘
Caractéristiques des Microservices
- Indépendance des services : Chaque service est une codebase séparée, une unité déployable
- Base de données par service : Chaque service possède ses données, pas de bases partagées
- Communication réseau : Les services communiquent via HTTP, TCP ou files de messages
- Diversité technologique : Chaque service peut utiliser différents langages et frameworks
- Scaling indépendant : Scaler chaque service selon sa charge spécifique
Implémentation NestJS Microservices
Voici comment j'implémente des microservices avec NestJS en utilisant le transport TCP pour la communication synchrone.
Configuration de l'API Gateway
L'API Gateway est le point d'entrée unique exposé à internet. Il gère les requêtes HTTP et les route vers les microservices appropriés via TCP.
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));
const config = new DocumentBuilder()
.setTitle("Microservices API")
.setDescription("API Gateway pour architecture microservices")
.setVersion("1.0")
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup("api", app, document);
app.enableCors();
await app.listen(3001);
}
bootstrap();
Enregistrement des Clients Microservices
Le gateway doit savoir comment atteindre chaque microservice. L'utilisation de registerAsync assure que les variables d'environnement sont correctement chargées.
import { Module } from "@nestjs/common";
import { ClientsModule, Transport } from "@nestjs/microservices";
import { ConfigModule, ConfigService } from "@nestjs/config";
@Module({
imports: [
ConfigModule.forRoot(),
ClientsModule.registerAsync([
{
name: "USER_SERVICE",
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
transport: Transport.TCP,
options: {
host: configService.get("USER_SERVICE_HOST"),
port: configService.get("USER_SERVICE_PORT"),
},
}),
inject: [ConfigService],
},
{
name: "ORDER_SERVICE",
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
transport: Transport.TCP,
options: {
host: configService.get("ORDER_SERVICE_HOST"),
port: configService.get("ORDER_SERVICE_PORT"),
},
}),
inject: [ConfigService],
},
]),
],
})
export class AppModule {}
Communication du Service Gateway
Le service gateway utilise ClientProxy pour communiquer avec les microservices.
import { Injectable, Inject, HttpException, HttpStatus } from "@nestjs/common";
import { ClientProxy } from "@nestjs/microservices";
import { firstValueFrom, timeout, catchError } from "rxjs";
@Injectable()
export class OrderGatewayService {
constructor(
@Inject("ORDER_SERVICE") private orderService: ClientProxy,
@Inject("USER_SERVICE") private userService: ClientProxy,
) {}
async createOrder(userId: string, createOrderDto: CreateOrderDto) {
try {
const user = await firstValueFrom(
this.userService.send({ cmd: "findUser" }, { userId }).pipe(
timeout(5000),
catchError((error) => {
throw new HttpException(
error.message || "Service utilisateur indisponible",
error.status || HttpStatus.SERVICE_UNAVAILABLE,
);
}),
),
);
const result = await firstValueFrom(
this.orderService.send({ cmd: "createOrder" }, { user, ...createOrderDto }).pipe(
timeout(5000),
),
);
return result;
} catch (error) {
if (error instanceof HttpException) throw error;
throw new HttpException(
"Échec de communication avec le microservice",
HttpStatus.SERVICE_UNAVAILABLE,
);
}
}
}
Démarrage du Microservice
Chaque microservice écoute les messages TCP plutôt que les requêtes HTTP.
import { NestFactory } from "@nestjs/core";
import { MicroserviceOptions, Transport } from "@nestjs/microservices";
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.createMicroservice(
AppModule,
{
transport: Transport.TCP,
options: {
host: process.env.MS_HOST,
port: parseInt(process.env.MS_PORT, 10),
},
},
);
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
transform: true,
}));
await app.listen();
}
bootstrap();
Contrôleurs avec Patterns de Messages
Les contrôleurs de microservices utilisent @MessagePattern() à la place des décorateurs HTTP.
import { Controller } from "@nestjs/common";
import { MessagePattern, Payload, RpcException } from "@nestjs/microservices";
@Controller()
export class OrderController {
constructor(private readonly orderService: OrderService) {}
@MessagePattern({ cmd: "createOrder" })
async createOrder(@Payload() data: { user: User; items: OrderItem[] }) {
try {
return await this.orderService.create(data.user, data.items);
} catch (error) {
throw new RpcException({
status: error.status || 500,
message: error.message || "Échec de création de commande",
});
}
}
@MessagePattern({ cmd: "findOrder" })
async findOrder(@Payload() data: { id: string }) {
const order = await this.orderService.findOne(data.id);
if (!order) {
throw new RpcException({
status: 404,
message: "Commande introuvable",
});
}
return order;
}
}
Patterns de Communication
Les microservices communiquent via deux patterns principaux : synchrone et asynchrone.
Communication Synchrone (Requête-Réponse)
Utilisez send() quand l'appelant a besoin d'une réponse immédiate. Le gateway attend que le microservice traite et renvoie des données.
const user = await firstValueFrom(
this.userService.send({ cmd: "findUser" }, { userId }).pipe(
timeout(5000),
),
);
Utiliser pour : Authentification utilisateur, récupération de données, soumissions de formulaires, toute opération où l'utilisateur attend une réponse.
Communication Asynchrone (Événements)
Utilisez emit() quand l'appelant n'a pas besoin d'attendre. L'événement est émis et le service continue sans attendre le traitement.
@Injectable()
export class OrderService {
constructor(
@Inject("NOTIFICATION_SERVICE") private notificationClient: ClientProxy,
) {}
async createOrder(userId: string, items: OrderItem[]) {
const order = await this.orderRepository.create({ userId, items });
this.notificationClient.emit("order.created", {
orderId: order.id,
userId,
items,
});
return order;
}
}
Utiliser pour : Envoi d'emails, génération de rapports, tracking analytics, logs d'audit.
Handler d'Événement dans le Microservice
@Controller()
export class NotificationController {
@EventPattern("order.created")
async handleOrderCreated(@Payload() data: OrderCreatedEvent) {
await this.emailService.sendOrderConfirmation(data.userId, data.orderId);
}
}
Avantages des Microservices
Déploiement Indépendant
Déployez le service de paiement sans toucher à la gestion des utilisateurs. Livrez des fonctionnalités plus vite en supprimant la surcharge de coordination de déploiement.
Scaling Ciblé
Scalez les services indépendamment selon la demande. Le traitement des paiements a besoin de 20 instances pendant le Black Friday tandis que la gestion des utilisateurs reste à 2.
Liberté Technologique
Utilisez le bon outil pour chaque travail. Node.js pour les fonctionnalités temps réel, Python pour les pipelines ML, Go pour les services haute performance.
Isolation des Pannes
Un bug dans le service de recommandations ne plante pas le checkout. Les services tombent indépendamment, et le système se dégrade gracieusement.
Défis des Microservices
Complexité des Systèmes Distribués
Les appels réseau échouent. Les services ont des timeouts. Les échecs partiels créent des états incohérents. Vous avez besoin de patterns de résilience comme les timeouts, les retries et les circuit breakers.
Cohérence des Données
Plus de transactions ACID entre les services. Vous avez besoin de cohérence éventuelle, de patterns de saga et de transactions compensatoires.
Surcharge Opérationnelle
Plus de services signifie plus de déploiements, plus de monitoring, plus de logs à agréger, plus d'infrastructure à gérer.
Considérations de Production
Faire tourner des microservices en production nécessite une infrastructure supplémentaire par rapport à ce qu'un monolithe requiert.
Découverte de Services
Les services doivent se trouver mutuellement. Dans les environnements conteneurisés, utilisez le DNS Kubernetes ou des solutions de service mesh comme Istio.
Traçage Distribué
Une seule requête peut toucher 10 services. Des outils comme Jaeger ou Zipkin aident à tracer les requêtes à travers les frontières de services en utilisant des IDs de corrélation.
Logging Centralisé
Agréger les logs de dizaines de services dans une interface unique et consultable. Stack ELK (Elasticsearch, Logstash, Kibana) ou solutions cloud-native.
Health Checks
Chaque service a besoin d'endpoints de santé pour que l'orchestration de conteneurs sache quand redémarrer ou remplacer des instances.
import { Controller, Get } from "@nestjs/common";
import { HealthCheck, HealthCheckService, PrismaHealthIndicator } from "@nestjs/terminus";
@Controller("health")
export class HealthController {
constructor(
private health: HealthCheckService,
private prisma: PrismaHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.prisma.pingCheck("database"),
]);
}
}
Architecture Event-Driven
L'architecture event-driven est un pattern où les services communiquent via des événements plutôt que des appels directs. Elle est souvent combinée avec les microservices pour obtenir un couplage faible.
ARCHITECTURE EVENT-DRIVEN
┌─────────────┐ ┌─────────────────────┐ ┌─────────────┐
│ Service │────▶│ Message Broker │────▶│ Service │
│ Commandes │ │ (RabbitMQ) │ │Notifications│
└─────────────┘ └──────────┬──────────┘ └─────────────┘
│
┌──────────┴──────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Service │ │ Service │
│ Inventaire │ │ Analytics │
└─────────────┘ └─────────────┘
Quand Utiliser l'Event-Driven
- Services découplés : Les publishers n'ont pas besoin de connaître les subscribers
- Workflows async : Processus longue durée qui n'ont pas besoin de réponse immédiate
- Consommateurs multiples : Un événement doit déclencher des actions dans plusieurs services
- Pistes d'audit : Les événements fournissent un logging naturel de ce qui s'est passé
NestJS avec RabbitMQ
@Module({
imports: [
ClientsModule.register([
{
name: "EVENTS",
transport: Transport.RMQ,
options: {
urls: [process.env.RABBITMQ_URL],
queue: "events_queue",
queueOptions: { durable: true },
},
},
]),
],
})
export class EventsModule {}
Anti-Patterns Courants
Les microservices introduisent de la complexité. Ces anti-patterns transforment cette complexité en échec.
Le Monolithe Distribué
Des services qui doivent être déployés ensemble, partagent la même base de données ou ont un couplage runtime serré. Vous obtenez la complexité des microservices sans aucun des avantages.
Signes :
- Déployer un service nécessite de déployer les autres
- Les services partagent des tables de base de données
- Dépendances circulaires entre services
- La panne d'un service se propage à tous les autres
Base de Données Partagée
Plusieurs services lisant et écrivant dans les mêmes tables de base de données. Cela crée un couplage caché et rend le déploiement indépendant impossible.
ANTI-PATTERN : BASE DE DONNÉES PARTAGÉE
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Service │ │ Service │ │ Service │
│ A │ │ B │ │ C │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└────────────────┼────────────────┘
▼
┌─────────────────┐
│ Base de Données │ ← Les changements de schéma
│ Partagée │ cassent tous les services
└─────────────────┘
Chaînes d'Appels Synchrones
Le service A appelle B, qui appelle C, qui appelle D. La latence s'accumule, et tout échec casse toute la chaîne.
Solution : Utiliser des événements async là où c'est possible, ou agréger les données pour réduire la profondeur des appels.
Cadre de Décision : Monolithe vs Microservices
Choisir le Monolithe Quand
- Petite équipe (moins de 10 développeurs) : La surcharge de coordination des microservices n'est pas justifiée
- Nouveau produit/startup : Vous ne connaissez pas encore vos frontières de domaine
- Domaine simple : L'application n'a pas de frontières de services naturelles
- Forte cohérence requise : Les transactions ACID sont critiques pour votre métier
- Capacité DevOps limitée : Vous n'avez pas l'expertise en infrastructure pour les systèmes distribués
Choisir les Microservices Quand
- Grande équipe (plusieurs équipes) : Le déploiement indépendant permet l'autonomie des équipes
- Domaine bien compris : Des frontières claires existent entre les capacités métier
- Besoins de scaling différents : Des parties de votre système ont des patterns de charge très différents
- Diversité technologique nécessaire : Différents problèmes nécessitent différentes stacks technologiques
- Culture DevOps solide : Vous avez l'infrastructure et l'expertise pour opérer des systèmes distribués
Signes que vous Devez Découper le Monolithe
- La fréquence de déploiement est limitée par la surcharge de coordination
- Les équipes se marchent sur les pieds dans le code
- Un module a besoin de 10x les ressources tandis que les autres n'en ont besoin que de 1x
- Les temps de build dépassent 30 minutes
- Un bug dans une zone casse fréquemment des fonctionnalités sans rapport
La Matrice d'Évaluation
FACTEUR MONOLITHE MICROSERVICES ──────────────────────────────────────────────────────── Vitesse de Dév. Plus rapide Plus lent au début Complexité Déploiement Simple Complexe Flexibilité Scaling Limitée Élevée Liberté Technologique Faible Élevée Indépendance Équipe Faible Élevée Surcharge Opérationnelle Faible Élevée Cohérence Données Forte (ACID) Éventuelle Isolation des Pannes Aucune Élevée
Le Monolithe Modulaire : Un Juste Milieu
Avant de passer aux microservices, envisagez le monolithe modulaire. C'est un monolithe avec des frontières internes claires qui peuvent être extraites en services plus tard si nécessaire.
MONOLITHE MODULAIRE ┌─────────────────────────────────────────────────────┐ │ Application Monolithique │ │ ┌─────────────────────────────────────────────┐ │ │ │ Couche API │ │ │ └──────┬──────────────┬──────────────┬────────┘ │ │ │ │ │ │ │ ┌──────▼──────┐ ┌─────▼─────┐ ┌─────▼─────┐ │ │ │ Module │ │ Module │ │ Module │ │ │ │ Utilisateur │ │ Commandes │ │ Paiement │ │ │ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │ │ │ │Public │ │ │ │Public │ │ │ │Public │ │ │ │ │ │ API │ │ │ │ API │ │ │ │ API │ │ │ │ │ └───────┘ │ │ └───────┘ │ │ └───────┘ │ │ │ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │ │ │ │Private│ │ │ │Private│ │ │ │Private│ │ │ │ │ │ Impl │ │ │ │ Impl │ │ │ │ Impl │ │ │ │ │ └───────┘ │ │ └───────┘ │ │ └───────┘ │ │ │ └─────────────┘ └───────────┘ └───────────┘ │ └────────────────────────────────────────────────────┘
Principes du Monolithe Modulaire
- API publique uniquement : Les modules exposent une interface publique ; l'implémentation interne est privée
- Pas d'accès DB inter-modules : Chaque module possède ses tables ; les autres modules utilisent l'API publique
- Communication par interfaces : Les modules communiquent via des services exportés, pas des imports directs
- Schémas séparés : Les tables de base de données sont logiquement groupées par module
@Module({
imports: [
UserModule,
OrderModule,
PaymentModule,
],
})
export class AppModule {}
@Module({
controllers: [UserController],
providers: [UserService, UserRepository],
exports: [UserService],
})
export class UserModule {}
@Module({
imports: [UserModule],
controllers: [OrderController],
providers: [OrderService, OrderRepository],
exports: [OrderService],
})
export class OrderModule {}
Le monolithe modulaire vous offre la simplicité d'un monolithe avec la possibilité d'extraire des services plus tard. Quand un module a besoin d'un scaling indépendant, vous l'extrayez. Les frontières claires rendent l'extraction straightforward.
Patterns de Couche de Présentation
Ces patterns organisent le code au sein d'une seule application, en se concentrant sur la séparation de l'interface utilisateur et de la logique de présentation. Ils sont différents des patterns d'architecture applicative comme monolithe/microservices.
Architecture en Couches (N-Tier)
Le pattern d'architecture le plus courant. Il structure une application en couches horizontales, chacune responsable de préoccupations spécifiques.
ARCHITECTURE EN COUCHES ┌─────────────────────────────────┐ │ Couche Présentation │ Contrôleurs, Vues ├─────────────────────────────────┤ │ Couche Métier │ Services, Logique Domaine ├─────────────────────────────────┤ │ Couche Persistance │ Repositories, ORM ├─────────────────────────────────┤ │ Couche Base de Données │ PostgreSQL, MongoDB └─────────────────────────────────┘
Quand utiliser : Applications enterprise traditionnelles, systèmes CRUD-intensive.
MVC (Modèle-Vue-Contrôleur)
Sépare les données, la présentation et l'interaction utilisateur. Courant dans les applications web rendues côté serveur.
- Modèle : Données et logique métier
- Vue : Interface utilisateur et affichage des données
- Contrôleur : Gère l'entrée utilisateur, met à jour le modèle, sélectionne la vue
MVP (Modèle-Vue-Présentateur)
Une évolution du MVC. Le Présentateur gère toute la logique de présentation tandis que la Vue reste passive.
- Modèle : Données et logique de domaine
- Vue : Interface passive qui délègue au Présentateur
- Présentateur : Gère l'état, la logique, met à jour la Vue
MVVM (Modèle-Vue-VueModèle)
Se concentre sur le data binding. Le VueModèle expose des données et des commandes auxquelles la Vue se bind directement.
- Modèle : Données et règles métier
- Vue : Interface qui se bind aux propriétés du VueModèle
- VueModèle : Expose des données et commandes bindables
Quand utiliser : Interfaces complexes avec un data binding extensif (Angular, React avec gestion d'état, Android/iOS).
VIPER
Applique la Clean Architecture à iOS. Vue, Interacteur, Présentateur, Entité, Routeur.
Quand utiliser : Grandes applications iOS avec des fonctionnalités complexes.
Guide de Sélection des Patterns
PATTERN IDÉAL POUR ÉVITER QUAND ───────────────────────────────────────────────────────── En couches Apps enterprise CRUD Haute scalabilité requise MVC Web rendu côté serveur Interactions UI complexes MVP Apps mobiles testables Prototypes simples MVVM IU data-driven Formulaires simples VIPER Grandes apps iOS Petits projets Microservices Multi-équipes, scaling Produits en démarrage Monolithe Startups, petites équipes Échelle enterprise
Conclusion
L'architecture porte sur les compromis, pas les absolus. La meilleure architecture est celle qui correspond à votre contexte actuel tout en permettant l'évolution à mesure que vos besoins changent.
- Commencez avec un monolithe sauf si vous avez des raisons claires pour les microservices
- Construisez modulaire : Même dans un monolithe, maintenez des frontières claires
- Évitez les anti-patterns : Les monolithes distribués et bases de données partagées annulent les bénéfices
- Choisissez la communication judicieusement : Synchrone pour les fonctionnalités utilisateur, async pour les traitements en arrière-plan
- Investissez dans l'observabilité : Tracing, logging et health checks ne sont pas optionnels en production
L'objectif n'est pas d'avoir l'architecture la plus sophistiquée. L'objectif est d'avoir une architecture qui permet à votre équipe de livrer de la valeur efficacement.
Écrit par

Tech Lead et Ingénieur Full Stack pilotant une équipe de 5 ingénieurs chez Fygurs (Paris, Remote) sur un SaaS cloud-native Azure. Diplômé de 1337 Coding School (42 Network / UM6P). Écrit sur l'architecture, l'infrastructure cloud et le leadership technique.