Problema y escenarios aplicables
Diseñe una plataforma compartida de notificaciones utilizada por servicios de autenticación, pedidos, redes sociales y marketing. Admite notificaciones push móviles, SMS, correo electrónico y mensajes in-app. Los mensajes transaccionales incluyen OTP y resultados de pagos; los mensajes masivos incluyen recordatorios de eventos y campañas de marketing. Quien llama puede enviar de inmediato o programar una notificación. Los usuarios pueden darse de baja (opt-out) por canal y tipo de notificación, y los mensajes de marketing deben respetar las horas de silencio (quiet hours) en la zona horaria de cada usuario.
Utilice estos supuestos de entrevista:
- El sistema crea mil millones de tareas de entrega por canal al día. Una notificación de negocio enviada tanto por SMS como por correo electrónico cuenta como dos tareas.
- El rendimiento promedio es de
1,000,000,000 / 86,400 ≈ 11,600tareas por segundo. Estime un pico de campaña a 20 veces el promedio, o alrededor de 232,000 tareas por segundo. - El objetivo de disponibilidad para la aceptación duradera en la API es del 99.99%, con una latencia de aceptación p99 inferior a 200 milisegundos.
- Para los OTP, el tiempo p99 desde la aceptación hasta el envío a un proveedor de canal es inferior a dos segundos. El trabajo de marketing puede suavizarse a lo largo de 15 minutos. Ninguno de los dos objetivos promete cuándo un dispositivo muestra realmente el mensaje.
- Si el sobre duradero (durable envelope) para una tarea de entrega promedia 1 KB, las escrituras lógicas son de aproximadamente 1 TB por día y 30 TB en 30 días, antes de índices, réplicas, confirmaciones de recepción (receipts) y compresión.
Estos valores guían las decisiones de particionamiento, acumulación (backlog) y aislamiento; no son afirmaciones sobre el rendimiento del proveedor. La creación de plantillas, los algoritmos de selección de audiencia, la facturación y las pruebas A/B están fuera del alcance. Esta pregunta está dirigida a roles sénior de backend, plataforma y diseño de sistemas. Su desafío central es preservar la prioridad, la recuperabilidad y una observabilidad verídica a través de canales externos con semánticas diferentes.
Qué evalúa el entrevistador
La primera señal es si el candidato define qué significa "enviado con éxito". Un 202 de la API significa que la plataforma aceptó el trabajo de manera duradera. Una respuesta exitosa del proveedor comúnmente significa que el proveedor aceptó una solicitud. La recepción en el dispositivo, la visualización en el sistema operativo y la apertura por parte del usuario son estados posteriores. La métrica Sends de Firebase puede significar que un mensaje se encoló o se pasó a APNs, mientras que una respuesta exitosa de APNs describe la solicitud. Colapsar todo esto en delivered corrompe tanto las métricas operativas como la respuesta a incidentes.
La segunda señal es si la prioridad está respaldada por el aislamiento de recursos. Una cola compartida con un campo de prioridad aún puede ver ocupados su disco, las conexiones de consumidores y la cuota del proveedor por una campaña de 50 millones de usuarios. Un diseño sólido separa las colas transaccionales y masivas, los consumidores y los presupuestos de canal protegidos, al tiempo que permite que el tráfico masivo tome prestada capacidad ociosa. El tráfico de OTP también necesita su propio límite; una etiqueta de "crítico" no debe eludir todas las protecciones.
La tercera señal es el lenguaje preciso sobre las garantías de entrega. Una cola estándar puede entregar una tarea más de una vez, por lo que los workers deben procesar un delivery_id estable de forma idempotente. Si ocurre un tiempo de espera (timeout) de red después de que un proveedor aceptó la solicitud, la deduplicación interna no puede deshacer ese efecto secundario externo. Sin una API de envío idempotente del lado del proveedor, la plataforma puede reducir la probabilidad de duplicados, registrar un resultado incierto y elegir el reintento o la conmutación por error (failover) según el riesgo del mensaje. No puede prometer una entrega exactly-once de extremo a extremo.
La cuarta señal es un ciclo de fallos cerrado. Los fallos permanentes dejan de reintentarse y desactivan los endpoints inválidos cuando esté justificado. Las respuestas HTTP 429, 5xx y los fallos de red utilizan backoff con jitter. Las tareas vencidas terminan. La reproducción desde una cola de mensajes no procesables (dead-letter replay) conserva el delivery_id original y vuelve a verificar el vencimiento y el estado de opt-out. Los recibos pueden duplicarse y llegar fuera de orden, por lo que el sistema almacena eventos sin procesar antes de derivar el estado actual mediante transiciones específicas de cada canal.
Preguntas para aclarar antes de responder
- ¿Qué significa "entregado" para el negocio? El objetivo controlable de OTP debe terminar en la aceptación del proveedor, porque los dispositivos fuera de línea, los operadores de telefonía y los permisos de usuario están fuera de la plataforma. Si el negocio requiere "leído", necesita una confirmación de recepción de canal admitida o un evento de cliente, con la cobertura debidamente especificada.
- ¿Quién asigna el tipo de notificación y la prioridad? El servidor es dueño de un catálogo de tipos controlado que asigna el tipo a la prioridad, canales permitidos, plantilla y reglas de opt-out. Permitir que los clientes envíen
critical=truepermite que cada equipo compita por el carril de emergencia. - ¿Qué mensajes pueden eludir las horas de silencio o los opt-outs? Solo los tipos de transacción aprobados para esa excepción pueden hacerlo. Las preferencias de marketing se verifican inmediatamente antes de encolar en el canal, de modo que un usuario que se da de baja después de la programación queda excluido.
- ¿Se permite el fallback entre canales? Reemplazar un SMS fallido con push cambia el costo, la alcanzabilidad y las expectativas del usuario. Configure un grafo de fallback por tipo de notificación y distinga el rechazo explícito de un resultado desconocido; la conmutación por error tras un resultado desconocido puede contactar al usuario dos veces.
- ¿Qué orden se requiere? Los mensajes de restablecimiento de contraseña para un usuario pueden necesitar ordenamiento, mientras que las alertas sociales comunes no suelen requerir un orden global. Conserve el orden solo en claves como
(user_id, notification_type)para evitar serializar todo el sistema. - ¿Cuáles son los límites de retención y privacidad? Los cuerpos de los mensajes pueden ser confidenciales. Mantenga versiones de plantillas y parámetros mínimos en las colas, encripte los campos seleccionados, aplique límites de retención y nunca registre OTPs, números de teléfono completos ni cuerpos de correos electrónicos en los logs.
Marco de respuesta de 30 segundos
"Separaría la aceptación duradera, la aceptación del proveedor, la entrega en el dispositivo y la lectura del usuario en diferentes estados. La entrada (ingress) escribe atómicamente la notificación y una outbox, y luego devuelve 202. La distribución hacia los destinatarios (fan-out) es asíncrona, y las políticas se evalúan cerca del despacho para preferencias, horas de silencio, vencimiento y deduplicación. Cada canal tiene colas y consumidores separados para tráfico transaccional, normal y masivo, con capacidad del proveedor protegida para el tráfico transaccional y capacidad ociosa prestada al trabajo masivo. Los workers procesan tareas at-least-once bajo un delivery_id estable, clasifican fallos permanentes, de límite de tasa y transitorios, y usan backoff exponencial con jitter para los casos transitorios. Los callbacks de proveedores se agregan como eventos y se procesan a través de una máquina de estados de canal, de modo que un evento sent tardío no degrade el estado delivered. Probaría una ráfaga de marketing junto con OTPs, tareas duplicadas y callbacks reordenados para demostrar el SLO de dos segundos y la recuperación del backlog."
Análisis detallado paso a paso
La API de ingress no llama sincrónicamente a un proveedor de SMS o push. Autentica a quien llama, valida un tipo de notificación controlado, fija la versión de la plantilla, valida los parámetros mínimos y escribe una notificación más un registro de outbox en una única transacción de base de datos:
~~~text POST /v1/notifications { request_id, notification_type, recipients | audience_id, template_version, template_params, schedule_at, expire_at }
202 Accepted { notification_id, accepted_at } ~~~
request_id deduplica los reintentos del cliente, notification_id identifica una notificación de negocio y delivery_id identifica la entrega a un usuario-canal. No pueden ser un único identificador porque una notificación puede distribuirse a muchos usuarios y canales. Un outbox relay publica solo después de que se confirma la transacción de ingress, cerrando la brecha entre la confirmación en base de datos y la publicación del mensaje. Para una campaña de 50 millones de destinatarios, ingress almacena una referencia a una instantánea de audiencia y un cursor; no crea 50 millones de filas dentro de una sola solicitud o transacción.
Un servicio de fan-out lee la instantánea en particiones y crea lotes de planificación pequeños. Cerca de la hora de envío, un servicio de políticas carga las reglas del tipo de notificación y las preferencias actuales del usuario, y luego evalúa el opt-out, las horas de silencio, la disponibilidad del canal, la política de frecuencia, el vencimiento y el orden de fallback. Las horas de silencio utilizan la zona horaria IANA del usuario y manejan las transiciones de horario de verano. Si no se conoce la zona, el producto usa un valor predeterminado explícito en lugar de usar silenciosamente la hora del servidor. Una tarea filtrada recibe SUPPRESSED junto con un motivo legible por máquina para que soporte técnico pueda explicar por qué no se envió.
Los datos principales se pueden dividir en tres registros:
~~~text Notification( notificationid, tenantid, notification_type, templateversion, scheduleat, expire_at )
Delivery( deliveryid, notificationid, user_id, channel, trafficclass, state, provider, providermessage_id, attemptcount, nextattemptat, stateversion )
DeliveryEvent( deliveryid, providereventid, eventtype, providertime, receivedat, rawpayloadref ) ~~~
Delivery es la vista actual materializada; DeliveryEvent conserva los hechos de recepción del proveedor. El contenido sin procesar y los datos personales residen en un almacenamiento controlado, con solo una referencia en la tabla de eventos. Cuando un proveedor suministra provider_event_id, se exige unicidad. De lo contrario, se aplica un hash a los campos normalizados del recibo para deduplicación. Verifique las firmas de los webhooks antes de escribir eventos y conserve los campos desconocidos de forma compatible; un nuevo campo del proveedor no debe romper los callbacks válidos.
Como mínimo, distinga estos estados:
~~~text ACCEPTED -> PLANNED -> QUEUED -> SENDING -> PROVIDER_ACCEPTED | v DELIVERED -> READ
Terminal side states: SUPPRESSED, EXPIRED, FAILED_PERMANENT ~~~
El diagrama expresa etapas de negocio, no un orden de llegada garantizado de callbacks. Un proveedor puede entregar delivered antes de sent, y los webhooks duplicados son comunes. El manejador añade un evento idempotente y luego actualiza la proyección con una tabla de transición indexada por canal, estado actual y nuevo evento. Un sent tardío no puede revertir un SMS que ya está en DELIVERED; un canal con confirmaciones de lectura puede avanzar de DELIVERED a READ. Un evento no mapeado permanece en la tabla raw y genera una alerta en lugar de forzar un estado adivinado.
El plano de datos de envío separa las colas por canal y clase de tráfico, como sms.transactional, sms.bulk y push.transactional. Cada grupo tiene métricas independientes de antigüedad del backlog, concurrencia de consumidores y colas de reintento. Suponga que un contrato con un proveedor de SMS permite 10,000 solicitudes por segundo. El planificador podría proteger 3,000 por segundo para tráfico transaccional, permitir que el tráfico masivo tome prestada la capacidad no utilizada y reclamarla cuando aumente el tráfico transaccional. Estos son ejemplos de configuración; los valores reales provienen de los contratos con proveedores y de pruebas de carga. La planificación equitativa por tenant (inquilino) evita que una sola campaña consuma todo el presupuesto masivo, mientras que el envejecimiento (aging) evita que el trabajo normal quede en inanición indefinidamente.
Las colas separadas requieren más tópicos, conexiones y configuración operativa que una sola cola con campos de prioridad, pero aíslan el backlog en disco y los recursos de los consumidores. A menor escala con un solo canal, una cola compartida con planificación ponderada y equitativa (weighted fair scheduling) puede ser más simple. Cuando los SLOs transaccionales y los plazos masivos difieren en órdenes de magnitud, el aislamiento físico es más fácil de demostrar. En cualquiera de los dos diseños, un único programador de canal aplica la cuota real del proveedor. Los consumidores individuales no deben asumir cada uno que poseen la cuota completa.
Un worker de canal recibe una tarea at-least-once y reclama atómicamente un intento bajo delivery_id. Si la entrega ya se encuentra en un estado terminal, confirma (acknowledges) la tarea de la cola. De lo contrario, verifica expire_at, renderiza la plantilla, llama al proveedor y almacena la respuesta. Una tarea de cola duplicada no puede crear un segundo registro de entrega. Un tiempo de espera externo todavía tiene tres resultados posibles: el proveedor no recibió la solicitud, la recibió pero se perdió su respuesta, o tiene un resultado de procesamiento desconocido. Reutilice una clave de idempotencia del proveedor cuando exista. Sin esa función, marque el intento como UNKNOWN y reconcilie a través de una consulta al proveedor o un recibo. Una notificación crítica puede reintentarse después de que el negocio acepte explícitamente el riesgo de duplicación; el marketing generalmente puede esperar a la reconciliación o al vencimiento.
La clasificación de errores determina la siguiente acción:
- Parámetros inválidos, plantillas inválidas y tokens de dispositivo confirmados como inválidos son permanentes. Pase a
FAILED_PERMANENTy desactive el endpoint solo cuando la evidencia lo respalde. APNs 410 significa que el token de dispositivo ya no está activo para el tema. - Las respuestas HTTP 429, los 5xx del proveedor y los fallos de conexión generalmente son reintentables. Utilice backoff exponencial, full jitter, un recuento máximo de intentos y un plazo general. Los reintentos aún pasan por el control de capacidad del canal para que la recuperación no cree un segundo pico.
- Los fallos de autenticación o de configuración de cuenta son incidentes de plataforma. Pause el canal del proveedor afectado y genere una alerta; reintentar cada mensaje amplifica el fallo.
- Un OTP o un recordatorio obsoleto que alcance
expire_atpasa a serEXPIRED. Una reproducción de mensajes fallidos no puede extender su vencimiento original ni asignar un nuevodelivery_idpara eludir la deduplicación.
La aceptación del proveedor no demuestra la entrega en el dispositivo. APNs devuelve un estado y apns-id para cada POST; el éxito demuestra el éxito a nivel de solicitud. Firebase también cuenta el encolado o la transferencia a APNs como Sends. Un proveedor de SMS puede ofrecer estados posteriores como queued, sent, delivered, undelivered y read, y sus webhooks pueden llegar desordenados. Por lo tanto, la API pública devuelve un estado en capas más status_reason. Los informes calculan por separado las tasas de aceptación, aceptación del proveedor, entrega observable y apertura observable. Si un canal no expone un estado, reporte desconocido en lugar de contabilizarlo como éxito o fallo.
Para la escala, particione las tablas de notificaciones y entregas por notification_id o tiempo, y escale las colas de trabajo por canal, clase y clave de partición. El trabajo que necesita ordenamiento por usuario usa una clave de partición de usuario estable; las tareas masivas no ordenadas pueden distribuirse con un hash más uniforme. Mantener todo el sobre lógico de 30 TB durante 30 días en índices en línea costosos es innecesario. Conserve el estado de entrega reciente en línea, comprima y archive los eventos más antiguos, y retenga solo los índices requeridos por cumplimiento y soporte. La planificación de capacidad añade por separado réplicas, índices, amplificación por recibos y escrituras de reintentos.
La prueba de aceptación decisiva es "una ráfaga masiva no puede retrasar los OTPs". Mantenga aproximadamente 232,000 tareas de campaña por segundo y luego agregue tráfico transaccional. Verifique que el p99 de OTP desde la aceptación hasta el envío al proveedor permanezca por debajo de dos segundos y que el backlog masivo se drene dentro de su ventana de 15 minutos. Inyecte respuestas 429 y 5xx y confirme que el backoff no se sincronice en olas de reintentos. Entregue la misma tarea de cola más de una vez y verifique que reutilice el delivery_id original. Envíe callbacks en orden delivered -> sent -> delivered y verifique que el estado nunca retroceda. Cubra también el opt-out inmediatamente antes del envío, transiciones de horario de verano, mensajes muertos vencidos y resultados desconocidos del proveedor. Esos resultados en las rutas de fallo hacen que "confiable" sea demostrable mediante pruebas.
Respuesta de muestra de alta calidad
"Definiría cuatro resultados separados: aceptación duradera en la plataforma, aceptación del proveedor, entrega observable en el dispositivo y lectura del usuario. La API promete solo el primero. Escribe la notificación y la outbox, y luego devuelve 202. La distribución (fan-out) es asíncrona, y las reglas más recientes de opt-out, horas de silencio, disponibilidad de canales y vencimiento se evalúan cerca del momento del envío antes de asignar un delivery_id estable.
Mil millones de tareas de canal por día promedian alrededor de 11,600 por segundo, con un pico de campaña de 20 veces de alrededor de 232,000 por segundo. Los OTPs deben llegar al proveedor en dos segundos, mientras que el marketing puede suavizarse a lo largo de 15 minutos. Por lo tanto, separaría colas y consumidores por canal y por clase transaccional, normal y masiva, protegería la capacidad del proveedor para transacciones y permitiría que el tráfico masivo tome prestada solo la capacidad ociosa. Cada tenant también obtiene una porción justa.
Los workers asumen un consumo at-least-once. El trabajo duplicado en cola reclama el mismo delivery_id. Los errores permanentes se detienen, mientras que los fallos 429, 5xx y de red usan backoff con jitter acotado por un plazo general. Un tiempo de espera del proveedor puede haber producido un efecto secundario externo. Reutilizo una clave de idempotencia del proveedor si está disponible; de lo contrario, marco UNKNOWN y reconcilio en lugar de afirmar que hay exactly-once de extremo a extremo.
Los recibos son eventos inmutables que se integran a través de una tabla de transiciones de canal, por lo que un evento sent tardío no puede sobrescribir delivered. Mi prueba de despliegue combina un pico de marketing con tráfico de OTPs, e inyecta trabajo duplicado, respuestas 429, tiempos de espera del proveedor, callbacks fuera de orden y un opt-out de último minuto. Las métricas clave son la antigüedad de la cola por clase, la latencia de envío al proveedor, la clase de fallo, el recuento de UNKNOWN y los intentos de regresión de estado."
Errores comunes
- Registrar delivered cuando la API devuelve 202 → solo se ha completado la aceptación duradera → separe ACCEPTED, PROVIDER_ACCEPTED, DELIVERED y READ.
- Poner todo el trabajo en una sola cola con prioridades → el backlog masivo sigue compartiendo disco, consumidores y cuota del proveedor → aísle los recursos por canal y clase de tráfico, con préstamos controlados.
- Permitir que el tráfico crítico eluda todos los límites → el tráfico anormal de OTP puede sobrecargar la plataforma y al proveedor → otorgue al tráfico transaccional su propio límite, alertas y equidad entre tenants.
- Afirmar que el usuario recibe exactly-once porque la cola es at-least-once → una llamada al proveedor puede tener éxito mientras se pierde su respuesta → haga que el trabajo interno sea idempotente por
delivery_id, reconcilie resultados externos desconocidos y transparente el riesgo de duplicación. - Reintentar cada error de inmediato → los errores permanentes desperdician capacidad y los fallos 429/5xx crean una tormenta de reintentos → clasifique fallos permanentes, transitorios, de throttling y de configuración de plataforma; aplique backoff con jitter en casos transitorios.
- Sobrescribir el estado actual directamente → un callback fuera de orden puede cambiar delivered de vuelta a sent → persista primero eventos idempotentes y luego actualice la proyección con una tabla de transición de canal.
- Congelar las preferencias del usuario al momento de la programación → un opt-out posterior aún recibiría marketing → vuelva a verificar las preferencias y las reglas de cumplimiento cerca del encolado en el canal.
- Asignar un nuevo ID durante la reproducción de mensajes no procesables (dead letters) → se elude la deduplicación y se pueden enviar notificaciones obsoletas → reutilice el
delivery_idoriginal y vuelva a verificar opt-out y vencimiento. - Usar ordenamiento global por simplicidad → usuarios no relacionados se bloquean entre sí y una sola partición limita el rendimiento → conserve solo el orden local que realmente requiera el tipo de notificación del usuario.
Preguntas de seguimiento y respuestas
Pregunta de seguimiento 1: Cincuenta millones de usuarios desean la campaña a las 9:00 a.m. hora local. ¿Qué cambia?
Durante el fan-out, construya depósitos de tiempo (time buckets) por zona horaria IANA y fecha local, luego produzca lotes con tasa controlada antes de sus ventanas. Agregue jitter dentro de cada ventana de destino en lugar de liberar todas las tareas exactamente a la hora en punto. Defina el comportamiento del producto para horas locales inexistentes o repetidas durante los cambios de horario de verano, como pasar al siguiente instante válido y enviar no más de una vez al día. "9:00 a.m." debe significar una ventana de envío al proveedor, ya que la visualización en el dispositivo permanece fuera del control de la plataforma.
Pregunta de seguimiento 2: El proveedor devolvió éxito pero nunca envió una confirmación de entrega. ¿Cuál es el estado?
Mantenga PROVIDER_ACCEPTED; no lo promueva a DELIVERED. Tras la ventana de observación del canal, el sistema puede exponer DELIVERY_UNKNOWN para operaciones, pero el estado desconocido no debe contabilizarse como un fallo. Si el proveedor ofrece una API de consulta o un informe agregado, reconcilie de forma asíncrona y transparente el retraso y la cobertura de esos datos.
Pregunta de seguimiento 3: ¿Qué sucede si el callback de delivered llega antes que el de sent?
Añada ambos eventos de forma idempotente. La proyección puede avanzar de PROVIDER_ACCEPTED o SENDING a DELIVERED; el sent posterior enriquece el historial de auditoría sin retroceder el estado. Si el mismo proveedor emite más adelante una revocación o error documentado, modele esa transición específica del proveedor explícitamente en lugar de adivinar el orden a partir de un entero global.
Pregunta de seguimiento 4: ¿Pueden dos proveedores de SMS realizar conmutación por error automáticamente?
El failover es razonable tras un rechazo explícito, un fallo antes de la conexión o cuando se activa un circuit breaker a nivel del proveedor. Un tiempo de espera posterior al envío puede significar que el proveedor principal ya entregó el mensaje, por lo que una conmutación por error inmediata eleva el riesgo de duplicar el SMS. OTP puede conmutar por error cuando los responsables de costos y seguridad acepten explícitamente ese riesgo, mientras que el marketing suele esperar una consulta, un recibo o el vencimiento. Configure la decisión por tipo de notificación en lugar de hacerlo de manera global.
Pregunta de seguimiento 5: ¿Cómo demuestra que el aislamiento de prioridades funciona?
Utilice una prueba de carga en circuito cerrado que combine tres presiones: un backlog masivo sostenido, un inquilino muy activo (hot tenant) y respuestas 429 repetidas del proveedor. La aceptación exige que el p99 de envío de OTP al proveedor sea inferior a dos segundos, que no haya trabajo masivo en los consumidores transaccionales, que la tasa agregada del proveedor esté dentro de la configuración y que el trabajo masivo se recupere dentro del plazo límite de 15 minutos. Luego, provoque el fallo de una partición de consumidores transaccionales y confirme que las instancias restantes asuman la capacidad protegida. El rendimiento promedio por sí solo no puede demostrar el aislamiento.