Tema representativo de entrevista

Entrevista de diseño de sistemas: Diseña un sistema de facturación de suscripciones

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

Pregunta

Diseña un sistema de facturación de suscripciones para planes mensuales y anuales, períodos de prueba, mejoras (upgrades), reducciones (downgrades), prorrateo, renovaciones, reintentos de pagos fallidos, facturas y webhooks. Explica la máquina de estados, el motor de facturación, la idempotencia de pagos, el ordenamiento de eventos, la capacidad, la conciliación y la recuperación ante fallas.

Pregunta y alcance

Diseña un servicio que administre suscripciones recurrentes para un producto SaaS, de medios o de membresía. Los clientes pueden elegir planes mensuales o anuales, convertir de un período de prueba a acceso de pago, cambiar de plan a mitad de ciclo y recuperarse de renovaciones fallidas. El sistema debe generar facturas auditables, notificar al producto sobre los derechos de acceso (entitlements) y procesar eventos asíncronos de un proveedor de pagos.

El material público de entrevistas presenta "diseñar un sistema de facturación de suscripciones" como una pregunta de diseño de sistemas sobre eventos de creación de suscripciones y expiración de períodos de prueba, cuellos de botella, métricas de clientes y monitoreo del sistema. La documentación de Stripe ubica suscripciones, facturas, PaymentIntents, períodos de prueba, prorrateos, recuperación de ingresos y webhooks en el mismo ciclo de vida. Este artículo no afirma que una empresa en particular siempre haga esta pregunta.

Asume 1 millón de suscripciones activas, un promedio de un evento relacionado con facturación por suscripción al día, picos de renovación de hasta 10 veces el tráfico normal, montos almacenados en la unidad monetaria más pequeña y un proveedor que puede agotar el tiempo de espera (timeout), duplicar eventos o entregar eventos después de que el estado local haya cambiado. El sistema debe evitar cargos duplicados, mantener las facturas rastreables y hacer converger el estado de los derechos de acceso tras los reintentos.

Qué evalúa el entrevistador

Una respuesta sólida separa planes, suscripciones, facturas, intentos de pago y derechos de acceso. Escribir paid=true en una fila de cliente no puede expresar prorrateo, períodos de gracia, reembolsos o múltiples intentos para una sola factura.

La siguiente señal es un límite de idempotencia explícito. La creación de facturas, las llamadas de pago, el manejo de webhooks y la entrega de derechos de acceso pueden reintentarse. Una cola por sí sola no evita un segundo cargo o una segunda autorización.

El entrevistador también evalúa la semántica de tiempo y contabilidad: anclas de facturación (billing anchors), fin de prueba, zonas horarias, años bisiestos/meses irregulares, mejoras inmediatas, cancelación al final del período y montos inmutables. Las facturas históricas no deben cambiar cuando cambia el precio de un plan.

Finalmente, el candidato debe explicar picos, pagos fallidos, eventos fuera de orden, reembolsos manuales, discrepancias de conciliación y reparación. El resultado útil es saber qué se debió cobrar, qué se cobró y qué derechos de acceso son válidos después de la recuperación.

Preguntas de aclaración antes de responder

  • ¿Cuál es el ancla de facturación (billing anchor)? ¿Mes calendario o una fecha flotante? Esto cambia el cálculo de fin de período y el manejo de febrero o el día 31.
  • ¿Cuándo se cobra una mejora (upgrade)? ¿Inmediatamente, en el siguiente período o a elección del cliente? Esto cambia el prorrateo y los estados de pago pendiente.
  • ¿Cómo funciona una reducción (downgrade)? ¿Reembolso inmediato, efecto en el siguiente período o crédito en cuenta? Esto cambia las líneas de factura y el manejo de reembolsos.
  • ¿Cuándo remueve el acceso un pago fallido? Un período de gracia de solo lectura y una suspensión inmediata tienen diferentes reglas de derechos de acceso.
  • ¿Están dentro del alcance los impuestos, descuentos o cargos por uso? De ser así, las entradas de precios deben versionarse antes de la finalización de la factura.
  • ¿Cuál es la fuente de la verdad para los derechos de acceso? El producto puede consumir los eventos del proveedor directamente, o el sistema de facturación puede publicar eventos entitlement.changed. La elección cambia la reproducción (replay) y la consistencia.
  • ¿Se requieren múltiples monedas, reembolsos o ajustes manuales? Si es así, utiliza unidades menores enteras y nunca sobrescribas facturas históricas.

Respuesta de 30 segundos

“Separaría planes, suscripciones, facturas, intentos de pago y derechos de acceso. Una máquina de estados de suscripción decide el período actual y la siguiente acción; un motor de facturación crea una instantánea inmutable de la factura en cada límite y utiliza invoice_id como la clave de idempotencia de pago. Confirma el pago a través de webhooks firmados más consultas al proveedor, deduplica por ID de evento y permite que los eventos lleguen fuera de orden. Representa las mejoras y reducciones como líneas de factura positivas y negativas explícitas. Los pagos fallidos entran en una máquina de estados de reintentos acotada y con variación aleatoria (jitter), y los derechos de acceso siguen el estado de pago confirmado más una política de gracia documentada. Probaría el diseño con conciliación, invariantes de estado e inyección de fallas.”

Solución paso a paso

Paso 1: Definir objetos e invariantes.

text
Plan(id, version, currency, interval, unit_amount, trial_days)
Subscription(id, customer_id, plan_version, status, period_start, period_end)
Invoice(id, subscription_id, period_start, period_end, currency, total, status)
InvoiceLine(id, invoice_id, source, description, quantity, unit_amount, amount)
PaymentAttempt(id, invoice_id, attempt_no, provider_key, status, provider_id)
Entitlement(id, customer_id, feature, valid_until, source_invoice_id)

Los planes pueden cambiar, pero una factura emitida almacena su versión de precio y una instantánea de la moneda. Los invariantes clave son: un período de suscripción tiene como máximo una factura efectiva; el resultado de un proveedor avanza una factura una sola vez; una falla tardía no puede sobrescribir un pago confirmado; y reproducir cambios de derechos de acceso converge en una versión.

Paso 2: Dirigir el trabajo con una máquina de estados.

text
trialing --trial_end--> active
active --renewal--> invoice_open
invoice_open --payment_succeeded--> paid
invoice_open --payment_failed--> past_due
past_due --retry_succeeded--> paid
past_due --retries_exhausted--> canceled
active --cancel_at_period_end--> canceling
canceling --period_end--> canceled

Los comandos o eventos con una versión ejecutan transiciones; las solicitudes web arbitrarias no deberían mutar el estado directamente. Define la política de derechos de acceso para active, past_due y canceled: por ejemplo, past_due puede conservar acceso de solo lectura durante tres días mientras que canceled remueve funciones de pago. Registra el motivo, evento de origen, actor y hora para cada transición.

Paso 3: Generar facturas y manejar cambios de plan.

Particiona un worker de facturación por period_end. Primero crea una factura bajo una restricción única como (subscription_id, period_start), luego calcula sus líneas. Para una mejora en el día 15, registra el plan anterior no utilizado como una línea negativa y el plan nuevo restante como una línea positiva. Utiliza unidades menores enteras y una regla explícita de segundos o días; nunca restes valores de punto flotante redondeados.

Congela el monto, impuestos, descuento, instantánea de uso y versión del plan cuando se finaliza la factura. El trabajo del período puede ejecutarse dos veces: la restricción única devuelve la factura original y el worker verifica si ya existe un intento de pago. La cancelación al final del período cambia cancel_at_period_end; no elimina el historial de facturas. La cancelación inmediata crea un reembolso o ajuste de crédito.

Paso 4: Separar la idempotencia de pago de los tiempos de espera de red.

text
provider_key = "invoice:" + invoice_id + ":attempt:" + attempt_no
POST payment-provider/charges
  Idempotency-Key: provider_key

Escribe un PaymentAttempt local antes de llamar al proveedor. Tras un tiempo de espera (timeout), no infieras una falla; reintenta con la misma clave o consulta al proveedor. Stripe documenta que las solicitudes idempotentes repetidas devuelven el primer resultado y comparan parámetros para evitar la reutilización accidental de claves. Por lo tanto, la clave debe representar una operación de negocio, no un intento de red.

En caso de éxito, un webhook o consulta al proveedor registra provider_id, monto, moneda y hora de finalización. Una actualización condicional de la base de datos permite que solo open o past_due pasen a paid; un payment_failed tardío va al registro de auditoría y no puede revertir un pago confirmado. Una discrepancia de dinero o moneda ingresa a conciliación en lugar de otorgar o remover acceso automáticamente.

Paso 5: Manejar duplicados y reordenamiento de webhooks con un inbox.

text
WebhookInbox(event_id PRIMARY KEY, received_at, payload_hash, processed_at)
Outbox(id, aggregate_id, event_type, payload, published_at)

Verifica la firma, almacena el evento sin procesar y el hash en WebhookInbox, y confirma (acknowledge) un event_id duplicado. El procesador utiliza la versión del objeto, el estado del proveedor y el estado local de la factura para decidir si avanzar; la hora de llegada no es una garantía de ordenamiento. Stripe recomienda webhooks de suscripción para actividad asíncrona y cambios de derechos de acceso, por lo que el procesamiento interno también debe ser reproducible.

Una transacción local puede escribir el estado de facturación, una intención de cambio de derechos de acceso y un registro de Outbox juntos. Luego, un publicador envía el registro al servicio de derechos de acceso. Los consumidores deduplican por (aggregate_id, version), de modo que los reintentos del proveedor, los reintentos de cola y los reinicios de consumidores no creen una segunda autorización efectiva.

Paso 6: Estimar capacidad y aislar picos.

Con 1 millón de suscripciones activas y un evento de facturación por suscripción al día, la tasa estable es de aproximadamente 11.6 eventos/segundo; un pico de renovación de 10x es de aproximadamente 116 eventos/segundo. La carga más pesada son las líneas de factura simultáneas, llamadas de pago, webhooks y notificaciones. Agrupa el trabajo periódico en bloques por tiempo (bucket), limita cada lote y añade variación aleatoria (jitter) para evitar un pico único al inicio de la hora.

WorkloadMain bottleneckControl
Period scanHot index and lock contentionBucket by period_end, short transactions, unique constraint
Payment callProvider latency and limitsBounded concurrency, idempotency key, exponential backoff
WebhookDuplicate and out-of-order eventsInbox dedupe, conditional version update, replay queue
Entitlement syncDownstream backlogOutbox, consumer lag, tenant quotas

Indexa las suscripciones por ID de suscripción y fin de período; haz que el trabajo de pago y notificación sea asíncrono. Asigna cuotas separadas a clientes grandes (tenants), días de renovación concentrados y cuentas de alto uso para que un solo tenant no pueda consumir toda la concurrencia de pago. Estos números son suposiciones iniciales; los límites del proveedor y las pruebas de fallas deben calibrar el diseño.

Paso 7: Conciliar, reparar y observar.

Concilia diariamente contra facturas, transacciones del proveedor y exportaciones de liquidación. Para cada factura verifica que los ítems sumen el total, el monto de pago sea igual al monto adeudado y la expiración de derechos de acceso no exceda el tiempo pagado confirmado. Crea registros inmutables de ajuste o reembolso para las diferencias; nunca sobrescribas un monto antiguo.

Rastrea el retraso del escaneo de período, la latencia de facturas, los resultados de pago desconocidos, el conteo de reintentos, la antigüedad de past_due, la tasa de duplicados y retraso de procesamiento de webhooks, la demora de propagación de derechos de acceso, las diferencias monetarias de conciliación y las fallas de pago por tenant. Los registros de auditoría deben vincular subscription_id, invoice_id, payment_attempt_id, ID de evento e ID de traza para que una queja pueda rastrearse hasta la respuesta del proveedor.

Paso 8: Probar los límites con una matriz de fallas.

Inyecta trabajos periódicos repetidos; una caída del sistema tras la creación de la factura; un cargo del proveedor seguido de una respuesta perdida; webhooks duplicados y fuera de orden; un pago de prorrateo fallido; un período de prueba que finaliza sin método de pago; cancelación y renovación concurrentes; una caída del consumidor tras la publicación en Outbox; y un reembolso manual compitiendo con un reintento automático.

Las verificaciones de aceptación son: una factura efectiva por (subscription_id, period_start); ningún segundo cargo para un provider_key; el pago confirmado no es sobrescrito por una falla tardía; los eventos de derechos de acceso reproducidos producen la misma versión; cada diferencia de conciliación tiene un ajuste rastreable; y cada acción automatizada puede reconstruirse a partir de los registros de auditoría.

Ejemplo de respuesta de alta calidad

“Separaría planes, suscripciones, facturas, intentos de pago y derechos de acceso, y capturaría una instantánea del precio del plan cuando se crea una factura. Una máquina de estados de suscripción maneja pruebas, renovaciones, períodos de gracia y cancelaciones. Un worker de período crea una factura bajo una clave única; los cambios de plan se convierten en líneas de factura positivas y negativas usando unidades menores enteras y una regla de prorrateo explícita.

Antes de llamar al proveedor, escribo un intento y derivo una clave de idempotencia invoice_id + attempt_no estable. Un tiempo de espera (timeout) usa la misma clave o una consulta al proveedor; nunca inventa una clave nueva. Los webhooks se verifican, se almacenan en un inbox y se deduplican por ID de evento. Las actualizaciones de versión condicionales evitan que una falla fuera de orden sobrescriba un pago confirmado. Un Outbox envía de manera confiable la intención de derechos de acceso downstream.

Aíslo la concurrencia de pagos, escaneos de períodos, webhooks, notificaciones y cuotas por tenant, y añado variación aleatoria (jitter) a los picos de renovación. La conciliación diaria compara facturas locales, transacciones del proveedor y liquidaciones. Probaría trabajos periódicos duplicados, un cargo exitoso con respuesta perdida, webhooks reordenados, prorrateo fallido, cancelación y renovación concurrentes, y convergencia de derechos de acceso tras la reproducción.”

Errores comunes

  • Error: almacenar solo paid en la suscripción → Por qué falla: facturas, intentos, períodos de gracia y reembolsos desaparecen → Solución: modelar facturas, intentos de pago y derechos de acceso por separado.
  • Error: crear un nuevo ID de pago para cada reintento tras un timeout → Por qué falla: un cargo de negocio puede ejecutarse dos veces → Solución: mantener una clave de idempotencia por operación y consultar resultados desconocidos.
  • Error: aplicar eventos según la hora de llegada → Por qué falla: los webhooks pueden duplicarse, reordenarse o retrasarse → Solución: almacenar un inbox y usar versiones de objetos más transiciones condicionales.
  • Error: recalcular facturas antiguas a partir del precio del plan actual → Por qué falla: un cambio de precio reescribe la historia → Solución: congelar precio, impuestos, descuento, uso y versión del plan en la factura.
  • Error: cancelar inmediatamente después de una falla → Por qué falla: una interrupción transitoria del proveedor se convierte en una interrupción de derechos de acceso → Solución: usar una política de gracia past_due y una máquina de estados de dunning acotada.
  • Error: eliminar registros de período al cancelar → Por qué falla: auditoría, reembolsos y conciliación pierden evidencia → Solución: anexar registros de cancelación y ajuste mientras se conserva el historial.
  • Error: tratar una transacción de base de datos como una transacción de pago → Por qué falla: el proveedor está fuera de la transacción local → Solución: cerrar el ciclo con llamadas idempotentes, webhooks, consultas y conciliación.
  • Error: escanear cada suscripción en un momento exacto → Por qué falla: bloqueos, llamadas de pago y notificaciones generan picos juntos → Solución: agrupar en bloques (bucket), añadir variación (jitter), encolar y asignar cuotas al trabajo.

Preguntas de seguimiento y respuestas

Pregunta de seguimiento 1: Falla el pago del cambio a un plan superior. ¿Qué sucede con el acceso?

Mantén el derecho de acceso anterior, crea un pending_update y una nueva factura, y cambia de plan solo después de confirmar el pago. Si el producto permite la actualización antes del cobro, marca el nuevo derecho de acceso como limitado por gracia con una expiración explícita. En ambas políticas, las versiones de facturas y derechos de acceso siguen siendo rastreables; cambiar solo plan_id es insuficiente.

Pregunta de seguimiento 2: No llega ningún webhook del proveedor durante tres días. ¿Cómo lo detectas?

Ejecuta un trabajo de conciliación a partir del estado de la factura local y consultas al proveedor. Alerta sobre open o past_due antiguos, o facturas expiradas sin un evento terminal; si la consulta sigue siendo desconocida, pausa la cancelación automática y derívala a revisión manual. Un webhook es una notificación de baja latencia, no la única fuente de la verdad.

Pregunta de seguimiento 3: La cancelación y la renovación llegan al mismo tiempo. ¿Cuál prevalece?

Asigna versiones monotónicas a los comandos de suscripción y serialízalos con una actualización condicional o una partición ordenada. cancel_at_period_end puede coexistir con la renovación actual; la cancelación inmediata verifica si hay una factura abierta o un intento de pago antes de emitir un reembolso o anulación. Los eventos de auditoría deben explicar el resultado final de facturación y derechos de acceso.

Pregunta de seguimiento 4: ¿Cómo agregas facturación por uso sin contar dos veces?

Deduplica IDs de eventos de uso inmutables y crea una instantánea de uso por período de facturación. Un duplicado incrementa las métricas de recepción pero no la cantidad facturable. Congela la instantánea en la liquidación; enruta los eventos tardíos al siguiente período o a un ajuste manual. Almacena las versiones de origen y agregación en las líneas de la factura para que el recálculo pueda compararse.

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