En un SaaS, un email de activación no es solo un mensaje que sale de un servidor. Es una parte del producto. Si llega tarde, se duplica o nadie puede explicar que paso, la persona usuaria puede abandonar el onboarding aunque el registro haya funcionado.
Un contrato de entrega ayuda a quitar esa incertidumbre. No promete que todos los mensajes llegarán siempre, porque eso depende de proveedores y destinatarios. Promete algo mas útil para el equipo: cada intento tendrá un estado, un contexto y una siguiente acción clara.
Qué debe prometer el contrato de entrega
Antes de escribir workers o añadir otra cola, define qué significa “entregado” en tu producto. Yo separaría estas etapas:
-
requested: el producto pidió el email. -
queued: el backend guardó el trabajo. -
sending: un worker lo tomó. -
accepted: el proveedor aceptó la petición. -
observed: una prueba o evento confirmó el resultado esperado.
accepted no es lo mismo que “la persona lo leyó”. Esta diferencia parece pequeña, pero evita que soporte prometa demasiado. También permite investigar si el problema está en la aplicación, en el proveedor o en el buzón de destino.
Un registro mínimo puede verse así:
delivery_id identificador único
user_id usuario del SaaS
kind welcome, verify o reset
template versión de la plantilla
status estado actual
attempts número de intentos
last_error error breve y seguro
created_at momento de creación
updated_at última transición
La plantilla debe tener una versión. Si el usuario recibe un texto antiguo, podremos saber qué contenido generó el worker sin buscar en cada despliegue. Este detalle vuelve el debugging mucho menos dificil.
Diseña el flujo paso a paso
El flujo puede empezar en la transacción que crea la cuenta, pero no debería enviar el email dentro de la misma petición HTTP. Guarda un evento o trabajo y responde al frontend. Después, un worker procesa la cola.
signup -> delivery.requested -> delivery.queued
-> worker toma el trabajo
-> accepted o failed
Para evitar duplicados, usa una clave de idempotencia basada en el caso de uso. Por ejemplo:
welcome:user_482:template_v3
Antes de crear otra tarea, busca una entrega activa con esa clave. El worker también debe comprobar el estado justo antes de llamar al proveedor. Si un proceso ya la marcó como accepted, no conviene reintentar de nuevo solo porque un timeout escondió la respuesta.
Los reintentos tienen que distinguir errores temporales de errores permanentes. Un timeout de red puede volver a intentarse con una espera creciente. Una dirección rechazada necesita una ruta de corrección, no cinco llamadas iguales. Anota la razón, el número de intento y el próximo momento de ejecución.
Para diseñar los estados y sus salidas, resulta útil pensar en contratos de salida para automatizaciones. La idea es parecida: cada paso debe producir una señal que el siguiente componente pueda interpretar.
Prueba el onboarding con un buzón aislado
Las pruebas end-to-end suelen fallar cuando varias ejecuciones comparten la misma bandeja. Un test puede leer el mensaje de otro branch y parecer correcto. La solución práctica es crear un buzón o dirección de prueba por ejecución y guardar su identidad junto al delivery_id.
En algunos equipos se usa un correo temporal gratuito o un correo desechable gratuito para validar el flujo en staging. Eso puede servir, pero el test debe tratar esa dirección como un fixture aislado: no la mezcles con métricas de producción, no guardes secretos en el código y define cuánto tiempo se conserva.
También conviene guardar un pequeño recibo de la prueba:
{
"run_id": "ci-1842",
"delivery_id": "del_8f21",
"expected_subject": "Confirma tu cuenta",
"observed": true,
"attempts": 1
}
Si alguien escribe tepm mail com en una nota de QA, esa cadena no debe romper el parser ni entrar como una dirección real. Los fixtures deben validar formato y separar las anotaciones humanas de los datos que usa el worker.
Para flujos con varios pasos, también vale la pena versionar acciones de correo. Versionar no significa hacer una plataforma enorme; significa poder reproducir que configuración produjo cada resultado.
Mide evidencia, no solo envíos
Una métrica de “emails enviados” es insuficiente. Añadiría estas señales:
- tiempo desde
requestedhastaaccepted; - porcentaje de entregas que necesitaron reintento;
- duplicados evitados por la clave de idempotencia;
- entregas sin confirmación después del límite;
- fallos agrupados por tipo, no por contenido privado.
La privacidad importa también en los logs. Guarda identificadores y dominios cuando sea necesario, pero evita tokens, enlaces completos de recuperación y cuerpos enteros. Un panel con demasiados datos personales es dificil de compartir durante un incidente.
Errores comunes
El primer error es llamar al proveedor antes de persistir el trabajo. Si el proceso cae después del envío, no hay forma confiable de saber qué ocurrió. El segundo es usar un solo estado failed para todo; así se pierde la diferencia entre un rechazo permanente y una caída temporal.
Otro error frecuente es borrar la evidencia apenas el test termina. Conserva un resumen corto durante el tiempo que tu equipo necesite para depurar, y elimina lo que ya no aporta valor. Finalmente, no conviertas cada reintento en una notificación para la persona usuaria: los estados internos pueden ser detallados mientras la interfaz mantiene un mensaje simple.
Checklist de implementación
- [ ] Cada entrega tiene un identificador y una clave de idempotencia.
- [ ] Los estados distinguen petición, cola, proveedor y observación.
- [ ] La plantilla se guarda con una versión.
- [ ] Los reintentos tienen límite y clasificación de errores.
- [ ] Cada prueba usa un fixture de inbox aislado.
- [ ] Los logs no contienen tokens ni cuerpos completos.
- [ ] El frontend explica qué puede hacer la persona si el email no llega.
Preguntas rápidas
¿Necesito una cola especializada desde el inicio?
No. Una tabla en PostgreSQL y un worker sencillo pueden ser suficientes para un SaaS pequeño. Empieza con estados claros y cambia de herramienta cuando una necesidad observada lo justifique.
¿accepted significa que el email llegó?
No necesariamente. Significa que el proveedor aceptó la solicitud. La entrega final depende de más sistemas, por eso conviene separar aceptación, observación y lectura.
¿Qué hago si el test encuentra un mensaje viejo?
Relaciona cada ejecución con un buzón, asunto o identificador único. Busca por el delivery_id o por un encabezado de prueba, no solamente por el asunto.
Recapitulación
Un contrato de entrega convierte el email de onboarding en una pieza observable del producto. Con estados pequeños, idempotencia, fixtures aislados y métricas útiles, el equipo puede saber qué pasó y qué hacer después. Es una mejora de backend, pero también de productividad: menos adivinanzas, menos reintentos ciegos y conversaciones de soporte mas claras.
Top comments (0)