Primero lo primero: ¿qué carajos es un side-effect?
No, no hablamos de medicina. En programación, un side-effect es cualquier cosa que sucede como consecuencia de tu acción principal, pero que no es la acción principal en sí.
Ejemplo rápido: un usuario hace un pedido en tu app.
La acción principal es guardar el pedido en la base de datos.
Pero después vienen los “extras”:
- Mandar un correo de confirmación ✉️
- Restar el inventario 📦
- Avisar al sistema de pagos 💳
- Notificar a logística 🚚
- Sumarte puntos de lealtad ⭐️
- Y si andas de creativo, hasta disparar confetti en la oficina 🎉
Todo eso son side-effects. Necesarios, sí. Pero si los metes todos en tu endpoint, vas directo a un ataque de nervios.
“Endpoint burrito” (porque le metes de todo y ya ni cierra 🌯😂)
Cuando sobrecargas un endpoint de side-effects, pasa lo mismo que cuando tu tía quiere meter 30 tuppers en el refri chico: simplemente no cabe y algo va a tronar.
Cada side-effect tiene cierta probabilidad de fallar (digamos 0.5%). Poquito, ¿no? Sí, pero súmalos todos: 10 efectos y ya tienes un 5% de error en tu endpoint crítico. ¿Quieres que 5 de cada 100 pedidos se pierdan en el limbo? No lo creo.
Y el tiempo… uff. Si una API responde en 30ms, multiplícalo por 10 side-effects y ya vas en 300ms mínimo. Y eso si no usas Python con Flask, porque ahí se siente eterno para I/O pesado.
Aquí entran los eventos como superhéroes 🦸♂️
La solución clásica es: en lugar de meter todo en el endpoint, dispara un evento y que los sistemas alrededor hagan su magia.
Un pedido se crea → un evento vuela al aire → cada servicio escucha y actúa.
El endpoint sigue limpio, rápido, y tú no lloras en silencio.
Tu primera versión puede ser tan sencilla como esto:
async function placeOrder() {
// acción principal
await saveOrderToDatabase()
// en lugar de hacer TODO aquí, lo mandamos a la cola
await Promise.allSettled([
schedule(effect1),
schedule(effect2),
schedule(effect3),
// y los que vengan...
])
}
Cada schedule()
mete una tarea en la cola. Y cada tarea tiene su propio consumidor. Si algo falla, la cola lo guarda y lo reintenta.
Queue processing: tu nuevo mejor amigo
La gran ventaja de meter tus side-effects a una cola: persistencia.
Tus tareas no se pierden si algo truena.
Celery, Kafka, RabbitMQ… elige tu veneno. Todos cumplen con la gracia de guardar, reintentar y darte paz mental.
Y ojo con los poison pills: esas tareas que nunca van a funcionar porque traen bug. Ahí sí necesitas alertas. “Si falló más de X veces, despierta al dev de guardia”.
¿El sistema completo se cayó por horas? No hay bronca. Cuando revive, la cola sigue procesando. Eso sí: cuidado con el stampede. Todas las tareas esperando para correr al mismo tiempo pueden ser peor que la caída inicial.
El stampede es justo lo que suena: una estampida 🐂.
En sistemas con colas pasa cuando tu aplicación estuvo caída un rato, la cola siguió acumulando tareas pendientes, y cuando el sistema revive… ¡zas!, todas las tareas quieren correr al mismo tiempo.
Ejemplo rápido:
- Tu tienda online estuvo caída 3 horas.
- Durante ese tiempo, se generaron 10,000 pedidos que la cola guardó.
- Levantas los servidores y, de golpe, los workers arrancan a procesar esos 10,000 pedidos como si no hubiera mañana.
- Resultado: tu base de datos y tus APIs de terceros reciben un tsunami de requests y se te cae todo otra vez.
Es como cuando en un concierto se abre la puerta de seguridad y toda la multitud entra corriendo al mismo tiempo. No falla por “un error de software” en sí, sino porque la carga se concentra en un instante.
Tareas: flaquitas pero inteligentes
La receta:
- Scheduler anémico → no hace lógica, solo mete a la cola y loguea:
function scheduleTask(task) {
logger.log("Scheduling task", task)
queue.push(task)
}
- Task mínima → solo datos clave:
const task = {
event: "order_placed",
order_id: 123,
}
Nada de meter objetos completos, solo la referencia.
- Task function inteligente → hace el trabajo real, consulta DB, valida, ejecuta y lanza error si falla:
function doTheTask(task) {
logger.log("Checking task", task)
const order = db.query(`select * from orders where id=${task.order_id}`)
if (order.task_not_done_yet) {
logger.log("Doing task", task)
// hacer la chamba
// THROW en error para que la cola reintente
logger.log("Finished task", task)
} else {
logger.log("Skipping task", task)
}
}
¿Clave aquí? Loguea todo. Puede sonar ridículo, pero esos logs tontos son lo que te salva en una madrugada de producción.
Backup scheduling: el seguro de vida
Debes poder reconstruir la cola desde tu DB. Algo como:
function rebuildQueue() {
const orders = db.query(
`select id from orders where task_not_done_yet`
)
for (const order of orders) {
await scheduleTask(order)
}
}
Córrelo cada hora, incluso puedes programarlo como otra tarea en la cola. Eso es el patrón fan-out: una tarea que lanza otras tareas.
¿Sirve de qué?
- Porque garantizar exactly-once delivery es imposible.
- Porque si pierdes la cola, puedes reconstruirla.
- Porque si algo no se programó, lo reprogramas.
Y para evitar la estampida de datos, es importante:
- limitar cuántas tareas procesas en paralelo,
- usar backoff exponencial en los reintentos,
- o escalonar el arranque de tus workers.
¿Por qué sirve el Backoff exponencial?
Si un servicio externo está caído o saturado, reventarlo con miles de retries inmediatos solo lo hunde más (y de paso tira tu propio sistema). El backoff da “espacio para respirar”. El Backoff significa que, cuando algo falla y lo vas a reintentar, no lo hagas de inmediato como loco, sino que esperes un tiempo antes de intentarlo otra vez.
Lo básico es así:
- Intentas la tarea → falla.
- Esperas un ratito (ej. 1 segundo).
- Vuelves a intentar → falla otra vez.
- Ahora esperas más tiempo (ej. 2 segundos).
- Vuelves a intentar → si sigue fallando, esperas aún más (4s, 8s, 16s…).
Ese patrón se llama exponential backoff (porque el tiempo de espera se va multiplicando).
En resumen, compa
Los side-effects son inevitables. Son como los cuñados: aunque no los planeaste, siempre aparecen.
El truco no es eliminarlos, sino darles su espacio en el sistema.
Tu endpoint solo dispara el evento. La cola guarda, reparte, reintenta y organiza el desmadre.
Resultado: endpoints rápidos, sistemas menos frágiles y devs con menos ojeras.
Y recuerda: el día que todo falle, siempre puedes decir que fue culpa del becario.
Nos vemos pronto 👋