Tema representativo de entrevista

Entrevista de diseño de sistemas: Diseñar una plataforma confiable de entrega de webhooks

Diseño de sistemasDifícil
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Diseñe una plataforma multi-tenant que entregue eventos del producto a endpoints de webhook HTTPS configurados por el cliente. Debe admitir suscripciones, payloads firmados, entrega at-least-once, reintentos, registros de entrega, reenvío manual, equidad entre tenants y protección contra endpoints lentos, con fallas o maliciosos.

Problema y alcance

Una plataforma SaaS emite eventos como invoice.paid, order.shipped y user.disabled. Los clientes registran endpoints HTTPS y suscriben cada endpoint a tipos de eventos seleccionados. Diseñe la plataforma de salida desde el evento de negocio confirmado (committed) hasta la respuesta HTTP del cliente. La administración de endpoints, el historial de entregas, la rotación de secretos y el reenvío manual están dentro del alcance. El procesamiento interno del cliente después de acusar recibo de la solicitud queda fuera del control de la plataforma.

Utilice estos supuestos de entrevista:

  • El producto crea 50 millones de eventos de negocio por día. Cada evento coincide con cuatro endpoints en promedio, produciendo 200 millones de entregas lógicas por día.
  • La carga promedio es de aproximadamente 579 eventos y 2,315 entregas nuevas por segundo. Un pico de diez veces es de aproximadamente 5,787 eventos y 23,148 entregas nuevas por segundo.
  • Si los reintentos agregan un 10% más de intentos, la ruta de despacho en horas pico debe sostener aproximadamente 25,463 intentos por segundo.
  • Bajo un pico normal, el 99% de las nuevas entregas elegibles deben comenzar su primer intento dentro de los 10 segundos. Los reintentos tienen un plazo límite de 24 horas y el historial de entregas permanece consultable durante 30 días.
  • Con un payload de evento inmutable de 1.5 KB, 300 bytes de metadatos de entrega, 250 bytes por intento y 1.1 intentos por entrega, el almacenamiento lógico es de aproximadamente 190 GB por día o 5.7 TB durante 30 días. Esto excluye índices, réplicas, compresión y sobrecarga de almacenamiento de objetos.

Estos son datos de dimensionamiento, no puntos de referencia de la industria. La facturación, la transformación arbitraria de payloads, los webhooks entrantes y el diseño de la aplicación del cliente están fuera del alcance. El contrato clave es la entrega at-least-once: los duplicados y la llegada fuera de orden son posibles, mientras que la pérdida silenciosa dentro del límite prometido de retención y reintentos debe ser detectable y recuperable.

Qué evalúan los entrevistadores

La primera señal es si el candidato define con precisión los identificadores y la garantía. Un event_id representa un hecho de negocio confirmado. Un delivery_id representa ese evento que se dirige a un endpoint y se mantiene estable a través de reintentos automáticos y reentregas manuales. Una restricción única en (event_id, endpoint_id) evita que el reenvío de distribución cree una segunda entrega lógica. No evita que la misma solicitud HTTP llegue al cliente dos veces cuando un worker pierde la respuesta.

La segunda señal es el límite de la transacción. Publicar directamente después de una confirmación en la base de datos del negocio genera una brecha de escritura dual: la transacción puede confirmarse y el proceso puede fallar antes de publicar. Un outbox transaccional, un flujo de captura de datos de cambio (CDC) o una fuente de eventos duradera equivalente cierra esa brecha. Luego, la distribución materializa el estado de entrega antes del despacho, para que el sistema pueda responder qué endpoints fueron seleccionados, qué intento se ejecutó y qué queda pendiente.

La tercera señal es el aislamiento de fallas. Un endpoint lento no debe ocupar todas las conexiones. Un tenant con miles de destinos con fallas no debe consumir el presupuesto de reintentos de los tenants en buen estado. Las particiones de cola por sí solas no proporcionan equidad; el diseño necesita concurrencia delimitada por endpoint, cuotas de tenant, programación de reintentos y un estado de disyuntor (circuit breaker) o pausa.

La cuarta señal es la seguridad en dos direcciones. Los destinatarios necesitan una firma HMAC sobre los bytes exactos más metadatos de entrega autenticados. El remitente también debe tratar una URL controlada por el cliente como una superficie de SSRF, restringir esquemas y destinos, revalidar los resultados de DNS, restringir redireccionamientos y aislar la salida. Una respuesta sólida conecta estos controles con la rotación de secretos, la protección contra reproducción, la auditabilidad y la respuesta a incidentes.

Preguntas para aclarar antes de responder

  • ¿Qué se considera éxito? Este diseño trata cualquier respuesta 2xx como acuse de recibo del endpoint. Un 3xx, tiempo de espera agotado (timeout), error de conexión, 408, 429 o 5xx no es un éxito. El acuse de recibo no prueba que el procesamiento comercial posterior del cliente haya tenido éxito.
  • ¿Qué eventos y versión de payload se prometen? Cada tipo de evento necesita un esquema documentado y una política de versiones. Los reintentos envían los mismos bytes de payload inmutables; no deben reconstruir eventos antiguos a partir del estado actual de la base de datos.
  • ¿Qué orden se requiere? La entrega predeterminada no tiene orden. Si un cliente necesita orden para un agregado, incluya object_id y un object_version monótonamente creciente, u ofrezca una clave de ordenamiento opcional que acepte el bloqueo de inicio de línea (head-of-line blocking). El orden global no es necesario ni asequible.
  • ¿Durante cuánto tiempo puede la plataforma reintentar y reenviar? Aquí, los reintentos automáticos se detienen después de 24 horas y los registros permanecen durante 30 días. Un reenvío después de la ventana automática utiliza el evento original y la identidad de entrega, pero crea un nuevo registro de intento.
  • ¿Los endpoints pueden apuntar a cualquier lugar? El producto acepta únicamente endpoints HTTPS públicos. Se rechazan destinos privados, de bucle invertido (loopback), de enlace local (link-local), multidifusión, reservados y de metadatos en la nube para IPv4 e IPv6.
  • ¿Qué datos pueden salir de la plataforma? La autorización de suscripción y la minimización de payloads son parte de la distribución. Los campos confidenciales se omiten o se representan mediante una referencia de recurso cuando la integración puede recuperarlos a través de una API autenticada.

Respuesta de 30 segundos

“Confirmaría cada evento de negocio a través de un outbox, lo publicaría en un registro de eventos duradero y usaría un servicio de distribución para resolver las suscripciones activas. Este crea una fila de entrega por (event_id, endpoint_id) antes de colocar el trabajo en una cola. Los workers reclaman intentos con leases, aplican límites por endpoint y por tenant, firman los bytes inmutables junto con el ID de entrega estable y la marca de tiempo del intento actual, y los envían a través de HTTPS. Un 2xx completa la entrega; las fallas reintentables usan retroceso exponencial con jitter completo hasta el límite de 24 horas, mientras que las fallas permanentes se detienen. Dado que un timeout después del envío es ambiguo, el contrato es at-least-once y los clientes deduplican por ID de entrega. Agregaría rotación de secretos firmados, validación de URL segura contra SSRF, registros de entrega y reenvío, disyuntores de endpoints y reconciliación que detecte brechas en outbox, distribución o colas.”

Análisis detallado paso a paso

Comience en la creación del evento. En la misma transacción local que cambia el estado del negocio, escriba una fila de outbox que contenga event_id, tenant, tipo, versión de esquema, identidad y versión de objeto, hora de ocurrencia y una referencia a bytes de payload serializados canónicos. Un retransmisor de outbox publica en un registro duradero particionado. El retransmisor puede publicar dos veces, por lo que los consumidores posteriores deduplican por event_id. Si los datos del negocio abarcan varios sistemas, el productor con autoridad posee el evento; el servicio de webhooks no debe reconstruir hechos consultando tablas mutables.

Las API del plano de control pueden ser pequeñas y explícitas:

text
POST /v1/webhook-endpoints
PATCH /v1/webhook-endpoints/{endpoint_id}
POST /v1/webhook-endpoints/{endpoint_id}/rotate-secret
GET  /v1/webhook-deliveries?endpoint_id=&status=&cursor=
POST /v1/webhook-deliveries/{delivery_id}/replay

La creación de endpoints autentica al tenant, acepta una URL HTTPS y un filtro de tipo de evento, valida el destino y devuelve un secreto de firma una sola vez. Una entrega de verificación (challenge) puede demostrar la propiedad, pero una verificación exitosa no garantiza que las futuras respuestas de DNS sean seguras. La rotación de secretos mantiene activas las versiones actuales y anteriores durante una superposición delimitada y expone qué versión de firma se utilizó. Actualizar una URL crea una versión de configuración auditable; no reescribe silenciosamente intentos históricos.

Utilice cuatro registros duraderos:

text
Endpoint(endpoint_id, tenant_id, url, status, event_types,
         current_secret_version, previous_secret_version, config_version)

Event(event_id, tenant_id, type, schema_version, object_id,
      object_version, occurred_at, payload_ref, payload_hash)

Delivery(delivery_id, event_id, endpoint_id, endpoint_config_version,
         status, attempt_count, next_attempt_at, expires_at, lease_version)

Attempt(attempt_id, delivery_id, attempt_number, started_at, finished_at,
        http_status, latency_ms, error_class, response_digest)

El servicio de distribución lee un evento, carga las suscripciones activas autorizadas para ese tenant y tipo de evento, luego inserta filas de entrega en lotes delimitados. La base de datos impone la unicidad en (event_id, endpoint_id). Solo después de que existen las filas, encola los valores de delivery_id. Si el encolado falla, un limpiador (sweeper) encuentra filas PENDING vencidas sin trabajo activo en cola. Si el proceso de distribución falla a mitad de camino, el reenvío repite la búsqueda y la restricción de unicidad completa solo las filas faltantes. Una instantánea de suscripción almacenada o una versión de configuración de endpoint hace que las auditorías posteriores sean explicables.

El trabajo nuevo y los reintentos no deben compartir un único FIFO sin límites. Un programador selecciona las filas vencidas en intervalos de tiempo y luego aplica equidad ponderada por tenant, depósitos de tokens (token buckets) por endpoint y concurrencia delimitada por endpoint. Los endpoints saludables continúan mientras que un endpoint lento alcanza su propio límite de operaciones en curso. Las fallas repetidas abren un circuito y desplazan los intentos posteriores hacia adelante, pero la entrega permanece visible y elegible para intentos de sondeo. Una cuota para todo el tenant evita que millones de endpoints defectuosos consuman la flota. El trabajo de reintento puede tomar prestada capacidad inactiva, pero no puede hacer que la antigüedad de la entrega nueva más antigua incumpla su SLO.

Un worker reclama atómicamente una entrega con un lease y un lease_version incremental. Lee los bytes del payload almacenados, crea la marca de tiempo del intento y firma una cadena canónica como delivery_id.timestamp.payload_bytes con HMAC-SHA256. Envía los bytes firmados exactos y los encabezados que contienen el tipo de evento, el ID de entrega, la hora de ocurrencia del evento, la hora del intento, la versión del esquema y la versión de la firma. El consumidor verifica los bytes sin procesar mediante una comparación de tiempo constante, comprueba la tolerancia de la marca de tiempo y deduplica el ID de entrega estable. Cada reintento obtiene una nueva marca de tiempo de intento y firma mientras mantiene el ID de entrega y los bytes del evento.

La política de respuesta debe ser determinista. Cualquier 2xx marca la entrega como SUCCEEDED. No siga 3xx automáticamente. Trate 408, 429, 5xx, restablecimientos de conexión y timeouts como reintentables, y respete un Retry-After delimitado en 429 o 503; la mayoría de las demás respuestas 4xx son permanentes para esa configuración. Las fallas de TLS y DNS pueden comenzar como reintentables pero abrir el circuito del endpoint rápidamente. Utilice retroceso exponencial con jitter completo, un retraso máximo, un recuento máximo de intentos y el límite absoluto de 24 horas. Un timeout después de que la solicitud salió del worker es un resultado desconocido: reintentar puede duplicar el procesamiento del cliente, por lo que la plataforma nunca debe anunciar entrega exactly-once.

El reenvío manual crea otro Attempt para el mismo Delivery lógico; no crea un nuevo evento de negocio. El operador ve el payload inmutable original, la configuración del endpoint utilizada, todas las clases de respuesta y si los reintentos automáticos siguen activos. La autorización se vuelve a verificar antes del reenvío y el secreto activo actual del endpoint puede firmar el nuevo intento. Si el producto promete autenticación histórica byte por byte, debe retener la clave histórica requerida bajo una política de retención de claves definida.

Las URL proporcionadas por el cliente necesitan un límite de salida dedicado. Analice con una única implementación de URL bien probada, permita HTTPS y puertos aprobados, rechace credenciales en las URL, resuelva todas las respuestas A y AAAA y bloquee rangos privados, de loopback, de enlace local, reservados, de multidifusión y de metadatos. Valide nuevamente al momento de la conexión o fije (pin) la dirección validada para reducir las brechas de DNS-rebinding y de comprobación/uso (time-of-check/time-of-use). Deshabilite los redireccionamientos o vuelva a ejecutar la política completa para cada salto sin reenviar secretos. Coloque a los workers en una red de salida que no pueda alcanzar planos de control ni servicios internos, y limite el tiempo de conexión, el tiempo total de solicitud, los bytes de respuesta y la descompresión.

La capacidad sigue a la distribución, no solo al recuento de eventos. Cincuenta millones de eventos multiplicados por cuatro suscripciones producen 200 millones de entregas por día. A 1.1 intentos cada una, el historial de intentos es de 220 millones de filas por día. Los tamaños sin procesar asumidos dan 50M × 1.5 KB = 75 GB, 200M × 300 B = 60 GB y 220M × 250 B = 55 GB, totalizando aproximadamente 190 GB por día. Mantenga pequeños los índices del estado vencido actual, particione el historial por tiempo y hash de tenant, y archive los payloads inmutables y los intentos antiguos en un almacenamiento más económico. Mida el retraso (lag) de evento a distribución, las antigüedades más viejas de entregas nuevas y de reintentos, la latencia del primer intento, el éxito por endpoint y clase de estado, la amplificación de reintentos, el estado del circuito, la limitación (throttling) de tenants y los resultados de reenvíos.

Finalmente, reconcilie cada límite duradero. Compare las filas de outbox confirmadas con los ID de eventos publicados, los eventos con la instantánea de suscripción esperada y el recuento de entregas, las filas de entregas vencidas con los leases del programador y los recuentos terminales con el historial de intentos. La inyección de fallas debe hacer fallar al retransmisor después de publicar, hacer fallar la distribución a mitad de camino, duplicar mensajes de cola, terminar un worker después de que el endpoint remoto procese el POST pero antes de la escritura de éxito local, devolver respuestas 429 sincronizadas y hacer que los endpoints de un tenant se cuelguen. La aceptación se expresa como recuentos duraderos, antigüedad de cola delimitada, recuperación justa y duplicados explicables, no simplemente una solicitud de demostración exitosa.

Respuesta de ejemplo sólida

“Asignaría a cada hecho de negocio confirmado un event_id inmutable y a cada par evento-endpoint un delivery_id estable. El productor escribe el evento en un outbox en su transacción de negocio. Un retransmisor publica en un registro duradero y la distribución materializa las entregas bajo una restricción única (event_id, endpoint_id) antes de encolarlas. Eso permite que un reenvío repare el trabajo faltante sin crear una segunda entrega lógica.

En el pico establecido, la distribución crea aproximadamente 23,148 entregas nuevas por segundo y la ruta de despacho planifica para aproximadamente 25,463 intentos por segundo, incluidos los reintentos. Un programador separa el trabajo nuevo y los reintentos, aplica equidad ponderada por tenant, concurrencia por endpoint y depósitos de tokens, y luego permite que los workers reclamen intentos con leases versionados. Por lo tanto, un cliente lento consume solo su propia asignación. Las fallas repetidas abren el circuito de un endpoint mientras las pruebas de sondeo y el plazo límite de reintento de 24 horas permanecen visibles.

Cada intento firma delivery_id, la marca de tiempo del intento y los bytes exactos del payload inmutable con HMAC-SHA256. El cliente verifica la firma con una comparación de tiempo constante, rechaza marcas de tiempo obsoletas y deduplica el ID de entrega estable. Un 2xx tiene éxito. 408, 429, 5xx, errores de red y timeouts se reintentan con retroceso exponencial y jitter completo; la mayoría de las demás respuestas 4xx se detienen. Un worker puede fallar después de que el cliente procesó una solicitud pero antes de registrar el éxito, por lo que prometo entrega at-least-once y documento que los consumidores deben ser idempotentes.

El plano de control proporciona administración de endpoints y suscripciones, rotación de secretos con superposición delimitada, registros de entrega y reenvío. Las URL de endpoints pasan controles de HTTPS, rangos de IP, DNS-rebinding, redireccionamiento, tiempos de espera y tamaño de respuesta dentro de una red de salida aislada. Probaría el diseño reconciliando cada límite duradero e inyectando publicaciones duplicadas, distribución parcial, confirmaciones perdidas, respuestas masivas 429 y un tenant lleno de endpoints colgados. Los criterios de aprobación son el SLO del primer intento, la ausencia de brechas de entrega inexplicables, la amplificación de reintentos delimitada y la recuperación justa para los tenants saludables.”

Errores comunes

  • Publicar después de confirmar los datos de negocio → una falla entre las dos operaciones pierde el evento del webhook → escriba en un outbox transaccional o consuma un flujo de cambios duradero equivalente.
  • Crear un nuevo ID de entrega para cada reintento → el destinatario no puede deduplicar una entrega lógica → mantenga estable delivery_id y cree registros de intentos separados.
  • Prometer entrega HTTP exactly-once → una respuesta perdida después del procesamiento remoto deja al remitente sin poder conocer el resultado → ofrezca entrega at-least-once y exija un manejo idempotente por parte del consumidor.
  • Reconstruir payloads en el reintento → el estado actual de la base de datos cambia el evento histórico e invalida las firmas antiguas → almacene los bytes de eventos canónicos inmutables y su versión de esquema.
  • Usar un solo FIFO para todos los endpoints → los destinos lentos ocupan conexiones y retrasan a los clientes saludables → delimite el trabajo por endpoint y programe con equidad de tenants.
  • Reintentar cada respuesta distinta de 2xx inmediatamente → las fallas permanentes desperdician capacidad y los reintentos sincronizados amplifican los incidentes → clasifique los errores y use retroceso exponencial con jitter completo y un plazo límite.
  • Seguir redireccionamientos desde URLs de clientes → una URL pública puede redirigir a los workers a servicios internos → deshabilite los redireccionamientos o vuelva a validar completamente cada salto en una salida aislada.
  • Firmar JSON parseado → la reserialización cambia los bytes y hace que fallen firmas válidas → firme y verifique el cuerpo sin procesar exacto más los metadatos de ID y marca de tiempo autenticados.
  • Tratar un reenvío desde el panel como un nuevo evento → los clientes posteriores pueden aplicar el hecho de negocio dos veces bajo una nueva identidad → reenvíe la entrega existente y registre un nuevo intento.

Preguntas de seguimiento y respuestas

Pregunta de seguimiento 1: ¿Por qué la plataforma no puede garantizar una entrega exactly-once?

Supongamos que el endpoint confirma su trabajo y devuelve 200, pero la conexión se cierra antes de que el worker lea la respuesta. Reintentar puede repetir el efecto secundario del cliente; no reintentar puede perder un evento que nunca llegó. El remitente no tiene una transacción atómica con un servidor arbitrario del cliente. Un ID de entrega estable, idempotencia del lado del destinatario y la reconciliación hacen que la entrega at-least-once sea manejable, pero no convierten dos bases de datos y una red en una única confirmación exactly-once.

Pregunta de seguimiento 2: ¿Cómo evita que un endpoint roto retrase a todos los demás?

Asigne a cada endpoint un límite pequeño de operaciones en curso y un token bucket, luego programe entre tenants con equidad ponderada. Los timeouts liberan leases y programan reintentos retrasados en lugar de suspenderse (sleep) dentro de los workers. Las fallas consecutivas abren un circuito, de modo que solo se ejecutan sondeos controlados mientras los endpoints saludables utilizan la flota. Supervise la amplificación de reintentos tanto del endpoint como del tenant; un atacante con muchos endpoints aún debe permanecer dentro de un presupuesto para todo el tenant.

Pregunta de seguimiento 3: ¿Qué garantía de orden ofrecería?

El valor predeterminado es sin orden de entrega. Incluya occurred_at, object_id y un object_version monotónico para que los consumidores puedan descartar actualizaciones obsoletas o consultar el estado actual. Si un nivel de pago requiere ordenamiento para un objeto, particione esa suscripción mediante una clave de ordenamiento y permita solo una secuencia activa por clave. Un elemento anterior que falle bloqueará los elementos posteriores en esa clave, por lo que el producto debe exponer la compensación entre latencia y disponibilidad en lugar de afirmar un orden global.

Pregunta de seguimiento 4: ¿Qué sucede durante la rotación del secreto de firma?

Cree una nueva versión, conserve la versión anterior durante una superposición delimitada e identifique las versiones aceptadas en los encabezados o en la documentación. Durante la superposición, incluya firmas de ambas claves o permita que los consumidores prueben ambas de forma segura. Los nuevos intentos utilizan la clave activa, mientras que los bytes del payload inmutable y los ID de entrega permanecen sin cambios. Audite el autor, la hora, la versión de configuración del endpoint y el retiro final. Una clave comprometida puede requerir un retiro inmediato e instrucciones de reenvío visibles para el cliente en lugar de un período de superposición.

Pregunta de seguimiento 5: ¿Cómo se recupera después de que la distribución se completó solo parcialmente?

Reenvíe el evento duradero a través de la distribución. La restricción única (event_id, endpoint_id) hace que las inserciones completadas no realicen ninguna acción (no-ops) y crea solo las entregas faltantes. La reconciliación compara la instantánea de suscripción almacenada del evento o la versión de configuración con las filas materializadas. Si la semántica de suscripción establece "configuración al momento del evento", conserve esa instantánea; usar las suscripciones actuales durante la reparación podría enviar el evento a un destino que no tenía derecho a recibirlo cuando ocurrió el evento.

Pregunta de seguimiento 6: ¿El reenvío manual debería restablecer el plazo límite de 24 horas?

El reintento automático y el reenvío por parte del operador son contratos independientes. El trabajo automático se detiene en el plazo límite original. Un reenvío dentro de la ventana de historial de 30 días crea un nuevo intento solo después de las verificaciones de autorización y del estado del endpoint, y la interfaz de usuario lo marca claramente como manual. No debería reactivar silenciosamente la programación automática. Para eventos de seguridad o de eliminación, la política puede prohibir el reenvío después de un plazo límite comercial más corto, aunque los registros sigan siendo visibles.

Fuentes públicas

Preguntas relacionadas

Herramienta de entrevista relacionada

Usa Resolver para una respuesta de diseño de sistemas

Aclara primero los requisitos y luego avanza a través de la escala, la arquitectura, la elección de componentes y las compensaciones (trade-offs).

Ver la herramienta