Architecture orientée événements : comment nous avons remplacé un système monolithique qui coûtait 50 000 dollars par mois
Un guide pratique sur l'architecture orientée événements, illustré par des exemples concrets de code « avant/après », qui montre comment nous avons décomposé un système de traitement des commandes étroitement couplé en un pipeline d'événements résilient et évolutif.
Le problème : un système monolithique incapable de s'adapter à la croissance
Notre client disposait d'une application monolithique Django chargée du traitement des commandes. Chaque commande déclenchait une chaîne d'opérations synchrones : validation des stocks → prélèvement du paiement → envoi d'un e-mail de confirmation → mise à jour des statistiques → notification à l'entrepôt. Si le service de messagerie était lent, l'ensemble du processus de paiement se bloquait. Si le système d'analyse était hors service, les commandes échouaient.
Le système traitait 200 commandes par minute en temps normal, mais tombait en panne lors des ventes flash, où le volume atteignait 2 000 commandes par minute. Pour pallier ce problème, l'entreprise dépensait 50 000 dollars par mois en serveurs surdimensionnés.
Avant : La chaîne synchrone de la douleur
# L'ancienne méthode : tout est synchrone, tout est
couplédéf process_order(request) :
order = Order.objects.create(**request.data)
# Si l'une de ces opérations échoue, la commande échoue
inventory_service.reserve(order) # 200 ms
payment_service.charge(order) # 800 ms
email_service.send_confirmation(order) # 500 ms
analytics_service.track(order) # 300 ms
warehouse_service.notify(order) # 400 ms
# Total : temps de réponse
minimum de 2 200 ms # Si le service de messagerie est indisponible : la commande échoue
# Si le service d'analyse est lent : le paiement est lent
return Response({'order_id': order.id})L'architecture : événements et commandes
Nous avons divisé le système en deux concepts :
Commandes (opérations qui DOIVENT aboutir) : réservation de stock + paiement. Ces opérations restent synchrones car la commande en dépend.
Événements (actions qui DOIVENT se produire) : e-mails, analyses, notifications de l'entrepôt. Ceux-ci sont publiés sous forme d'événements et traités de manière asynchrone. En cas d'échec, ils sont relancés — le client n'a pas à attendre.
Après : Approche orientée événements avec Celery + Redis
# La nouvelle approche : synchronisation pour le chemin critique, événements pour le
reste def process_order(request):
order = Order.objects.create(**request.data)
# Chemin critique : doit aboutir (synchrone)
inventory_service.reserve(order) # 200 ms
payment_service.charge(order) # 800 ms
# Publication d'événement : « fire-and-forget » (asynchrone)
publish_event('order.completed', {
'order_id': order.id,
'customer_email': order.customer.email,
'total': str(order.total),
})
# Total : temps de réponse de
1000 ms # Problème de messagerie ? La commande aboutit quand même. Nouvelle tentative plus tard.
return Response({'order_id': order.id})
# Gestionnaires d'événements (workers séparés)
@celery_app.task(bind=True, max_retries=5, default_retry_delay=60)
def handle_order_completed(self, event_data):
"""Chaque gestionnaire est indépendant et peut être relancé"""
try:
email_service.send_confirmation(event_data)
except Exception as exc:
self.retry(exc=exc, countdown=2 ** self.request.retries * 60)Le modèle de bus d'événements
Nous avons mis au point un bus d'événements léger à l'aide de Redis Streams (il ne s'agit pas simplement d'un système pub/sub : les flux offrent une meilleure durabilité et permettent de créer des groupes de consommateurs) :
import redisimport
jsonfrom datetime import
datetimeclass EventBus:
def __init__(self):
self.redis = redis.Redis(host='redis', port=6379, db=0)
def publish(self, event_type: str, data: dict):
"""Publier un événement sur Redis Stream"""
event = {
'type': event_type,
'data': json.dumps(data),
'timestamp': datetime.utcnow().isoformat(),
}
self.redis.xadd(f'events:{event_type}', event)
def subscribe(self, event_type: str, group: str, consumer: str):
"""Lire les événements avec un groupe de consommateurs (chaque événement traité une seule fois)"""
try:
self.redis.xgroup_create(
f'events:{event_type}', group, id='0', mkstream=True
)
except redis.ResponseError:
pass # Le groupe existe déjà
while True:
events = self.redis.xreadgroup(
group, consumer,
{f'events:{event_type}': '>'},
count=10, block=5000
)
for stream, messages in events:
for msg_id, fields in messages:
yield msg_id, json.loads(fields[b'data'])
self.redis.xack(stream, group, msg_id)Résultats après 3 mois
| Système métrique | Avant | Après |
|---|---|---|
| Temps de réponse lors du paiement | 2 200 ms | 1 000 ms |
| Débit maximal | 200 commandes par minute | 5 000 commandes par minute |
| Coût mensuel du serveur | 50 000 $ | 12 000 $ |
| Conséquences de la panne du service de messagerie électronique | Toutes les commandes échouent | Les commandes s'enchaînent, les e-mails s'accumulent |
| Risque lié au déploiement | Tout ou rien | Déployer les services de manière indépendante |
Quand NE PAS utiliser une architecture orientée événements
L'approche orientée événements n'est pas toujours la solution. Évitez-la dans les cas suivants :
1. Vous devez respecter un ordre strict. Les événements peuvent se produire dans le désordre. Si l'étape B doit suivre l'étape A, veillez à ce qu'elles restent synchronisées.
2. Vous avez besoin d'une cohérence immédiate. Les événements sont cohérents à terme. Si l'utilisateur doit voir le résultat immédiatement, n'utilisez pas d'événements asynchrones.
3. Votre équipe est réduite. L'architecture orientée événements ajoute à la complexité opérationnelle. Une équipe de deux ou trois développeurs a tout intérêt à opter pour une architecture monolithique bien structurée.
4. Vous recevez moins de 100 requêtes par minute. Une architecture monolithique synchrone suffit amplement. N'en faites pas trop.
Commencez par un monolithe. Définissez des événements lorsque vous rencontrez des difficultés. La pire architecture est celle conçue pour résoudre des problèmes que vous n'avez pas encore.
— alokknight Ingénierie
