Saltar al contenido principal
Volver a todos los artículos
Arquitectura Backend5 min de lectura

Tareas en Background: Olvídate de Kafka, tu Base de Datos es Suficiente (por ahora)

Aprende a manejar tareas en segundo plano sin la complejidad de Kafka o RabbitMQ. Te muestro un método simple y robusto usando la base de datos que ya tienes, ideal para la mayoría de los proyectos.

Como emprendedor y desarrollador, he visto este patrón repetirse una y otra vez: construyes una nueva funcionalidad, como el registro de usuarios. Todo va bien hasta que decides añadir el envío de un email de bienvenida. De repente, esa llamada a la API que tardaba 200ms ahora tarda 2 segundos. El usuario se queda esperando. Malas noticias.

La primera reacción de muchos es buscar la herramienta más potente del mercado. "¡Necesitamos Kafka!", "¡Implementemos RabbitMQ!". Para. Respira. Probablemente no lo necesites. Estás a punto de añadir una complejidad brutal a tu sistema, otra pieza que mantener, monitorizar y pagar, cuando la solución está, literalmente, ya en tu stack.

La belleza de desacoplar

El problema no es que enviar un email sea lento. El problema es que estás obligando al usuario a esperar a que termine una tarea que no necesita ser instantánea. El usuario solo necesita saber que su cuenta ha sido creada. El email puede llegar 10 segundos después y a nadie le importará.

La solución es ejecutar esa tarea en "segundo plano" (background). Desacoplas la tarea pesada del flujo principal de la petición. La respuesta a tu usuario vuelve a ser inmediata y tu aplicación se siente rápida y fluida.

La solución "aburrida" que funciona: una tabla en tu BD

Sí, así de simple. Puedes construir un sistema de colas de mensajes sorprendentemente robusto usando la base de datos que ya tienes (PostgreSQL, MySQL, la que sea). La idea es crear una tabla que funcione como una lista de tareas pendientes.

Llamémosla `jobs`.


CREATE TABLE jobs (
  id BIGSERIAL PRIMARY KEY,
  queue TEXT NOT NULL DEFAULT 'default',
  payload JSONB NOT NULL,
  status TEXT NOT NULL DEFAULT 'pending', -- pending, processing, completed, failed
  attempts INTEGER NOT NULL DEFAULT 0,
  last_error TEXT,
  run_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Analicemos esto:

  • `payload`: Aquí guardas la información necesaria para ejecutar la tarea. Para un email de bienvenida, sería algo como `{"type": "send_welcome_email", "user_id": 123}`.
  • `status`: Controla el ciclo de vida del trabajo.
  • `attempts`: Clave para implementar reintentos si algo falla.

Manos a la obra: El flujo de trabajo

El sistema tiene dos partes: "encolar" un trabajo y "procesarlo".

1. Encolar un trabajo

Esto es lo más fácil. En lugar de ejecutar tu lógica pesada directamente, simplemente insertas una fila en la tabla `jobs`.


INSERT INTO jobs (payload) VALUES ('{"type": "send_welcome_email", "user_id": 123}');

¡Listo! Tu endpoint de registro de usuario vuelve a ser rapidísimo. Has delegado el trabajo.

2. El "Worker": El que hace el trabajo sucio

Aquí está la magia. Necesitas un proceso separado, un "worker", que se ejecute constantemente en tu servidor. Su única misión es buscar trabajos pendientes y ejecutarlos.

El ciclo de vida de un worker es un bucle infinito:

1. Busca un trabajo pendiente y lo bloquea. Esto es CRUCIAL para evitar que varios workers cojan el mismo trabajo. Si usas PostgreSQL, tienes una herramienta elegantísima para esto: `FOR UPDATE SKIP LOCKED`.


    -- Inicia una transacción
    BEGIN;

-- Selecciona y bloquea el primer trabajo pendiente que encuentre SELECT id, payload, attempts FROM jobs WHERE status = 'pending' AND run_at <= NOW() ORDER BY created_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED;

-- Si encontró un trabajo (digamos con id=42), lo marca como 'processing' UPDATE jobs SET status = 'processing', updated_at = NOW() WHERE id = 42;

-- Cierra la transacción COMMIT; ```

Esta consulta es atómica y segura en entornos con múltiples workers. Si un worker ejecuta esta consulta, otro que la ejecute al mismo tiempo simplemente se saltará la fila bloqueada (`SKIP LOCKED`) y buscará la siguiente. Es una maravilla.

2. Procesa el trabajo. Con la información del `payload`, el worker ejecuta la lógica real (enviar el email, procesar la imagen, etc.).

3. Actualiza el estado. - Si todo va bien: `UPDATE jobs SET status = 'completed' WHERE id = 42;` - Si algo falla: `UPDATE jobs SET status = 'failed', attempts = attempts + 1, last_error = '...' WHERE id = 42;`. Puedes incluso programar un reintento actualizando `run_at` a una fecha futura.

¿Cuándo deja de ser suficiente?

Este enfoque te llevará muy, muy lejos. Probablemente cubra el 90% de los casos de uso para startups y productos en crecimiento. Pero seamos honestos, tiene límites.

Empezarás a necesitar algo más potente cuando:

  • El volumen es masivo: Hablamos de miles de trabajos por segundo. La carga en la tabla `jobs` puede empezar a afectar al resto de tu base de datos.
  • Necesitas garantías complejas: Sistemas como Kafka ofrecen garantías de entrega y orden que son muy difíciles de replicar a mano.
  • Requieres enrutamiento avanzado: Si necesitas patrones complejos como fan-out (un mensaje a múltiples consumidores), RabbitMQ es tu amigo.

Conclusión: Empieza simple, crece después

No te dejes seducir por la herramienta más brillante y compleja del mercado solo porque sí. La ingeniería pragmática consiste en resolver el problema que tienes hoy con la solución más simple y robusta posible.

Una tabla en tu base de datos como cola de trabajos es un patrón probado en batalla, fácil de implementar y de mantener. Cuando llegues a sus límites, tendrás un problema de escala, y créeme, ese es un buen problema que tener. Para entonces, ya sabrás exactamente qué necesitas y por qué lo necesitas.

Escrito por Samuel Moreno

Socio y Desarrollador en Sinergia Barcelona.