Back to Blog
February 22, 20261,000 views

Arquitectura basada en eventos: cómo sustituimos un sistema monolítico que costaba 50 000 dólares al mes

Una guía práctica sobre la arquitectura basada en eventos con ejemplos reales de código «antes y después», en la que se muestra cómo descompusimos un sistema de procesamiento de pedidos muy interconectado en un flujo de eventos resistente y escalable.

El problema: un monolito que no podía escalar

Nuestro cliente contaba con una aplicación monolítica de Django que gestionaba el procesamiento de pedidos. Cada pedido activaba una cadena de operaciones sincrónicas: validar el inventario → cobrar el pago → enviar un correo electrónico de confirmación → actualizar las estadísticas → notificar al almacén. Si el servicio de correo electrónico era lento, todo el proceso de pago se bloqueaba. Si el sistema de estadísticas no funcionaba, los pedidos fallaban.

El sistema procesaba 200 pedidos por minuto en horario normal, pero se colapsaba durante las ventas relámpago, cuando alcanzaba los 2.000 por minuto. Para compensar esto, pagaban 50.000 dólares al mes por servidores con un exceso de capacidad.

Antes: La cadena sincrónica del dolor

# The old way: everything synchronous, everything coupled
def process_order(request):
    order = Order.objects.create(**request.data)
    
    # If ANY of these fail, the order fails
    inventory_service.reserve(order)        # 200ms
    payment_service.charge(order)           # 800ms
    email_service.send_confirmation(order)  # 500ms
    analytics_service.track(order)          # 300ms
    warehouse_service.notify(order)         # 400ms
    
    # Total: 2200ms minimum response time
    # If email is down: order fails
    # If analytics is slow: checkout is slow
    return Response({'order_id': order.id})

La arquitectura: eventos y comandos

Hemos dividido el sistema en dos conceptos:

Operaciones (que DEBEN completarse con éxito): reserva de existencias + pago. Estas se mantienen en modo síncrono porque el pedido depende de ellas.

Eventos (cosas que DEBERÍAN ocurrir): correo electrónico, análisis, notificaciones del almacén. Estos se publican como eventos y se procesan de forma asíncrona. Si fallan, se vuelven a intentar; el cliente no tiene que esperar.

A continuación: Programación basada en eventos con Celery + Redis

# The new way: synchronous for critical path, events for the rest
def process_order(request):
    order = Order.objects.create(**request.data)
    
    # Critical path: must succeed (synchronous)
    inventory_service.reserve(order)   # 200ms
    payment_service.charge(order)      # 800ms
    
    # Publish event: fire-and-forget (async)
    publish_event('order.completed', {
        'order_id': order.id,
        'customer_email': order.customer.email,
        'total': str(order.total),
    })
    
    # Total: 1000ms response time
    # Email down? Order still succeeds. Retried later.
    return Response({'order_id': order.id})


# Event handlers (separate workers)
@celery_app.task(bind=True, max_retries=5, default_retry_delay=60)
def handle_order_completed(self, event_data):
    """Each handler is independent and retriable"""
    try:
        email_service.send_confirmation(event_data)
    except Exception as exc:
        self.retry(exc=exc, countdown=2 ** self.request.retries * 60)

El patrón de bus de eventos

Hemos creado un bus de eventos ligero utilizando Redis Streams (no solo pub/sub: Streams ofrece durabilidad y grupos de consumidores):

import redis
import json
from datetime import datetime

class EventBus:
    def __init__(self):
        self.redis = redis.Redis(host='redis', port=6379, db=0)
    
    def publish(self, event_type: str, data: dict):
        """Publish event to 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):
        """Read events with consumer group (each event processed once)"""
        try:
            self.redis.xgroup_create(
                f'events:{event_type}', group, id='0', mkstream=True
            )
        except redis.ResponseError:
            pass  # Group already exists
        
        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)

Resultados tras 3 meses

Sistema métricoAntesDespués de
Tiempo de respuesta del proceso de pago2 200 ms1 000 ms
Rendimiento máximo200 pedidos por minuto5.000 pedidos por minuto
Coste mensual del servidor50 000 dólares12 000 dólares
Repercusiones de la interrupción del servicio de correo electrónicoTodos los pedidos fallanLos pedidos se procesan, los correos electrónicos se ponen en cola
Riesgo de implementaciónTodo o nadaImplementar servicios de forma independiente

Cuándo NO utilizar una arquitectura basada en eventos

El enfoque basado en eventos no siempre es la solución. Evítalo cuando:

1. Es necesario un orden estricto. Los eventos pueden producirse fuera de orden. Si el paso B debe ocurrir después del paso A, manténlos sincronizados.

2. Necesitas consistencia inmediata. Los eventos son consistentes a largo plazo. Si el usuario debe ver el resultado de inmediato, no utilices eventos asíncronos.

3. Tu equipo es pequeño. La arquitectura basada en eventos aumenta la complejidad operativa. Para un equipo de 2 o 3 desarrolladores, es más adecuado un monolito bien estructurado.

4. Tienes menos de 100 solicitudes por minuto. Una aplicación monolítica síncrona se encarga de esto sin problemas. No compliques demasiado el diseño.

Empieza con un monolito. Extrae eventos cuando notes que te cuesta. La peor arquitectura es aquella que se construye para problemas que aún no tienes.

— alokknight Ingeniería