Type something to search...
El Reintento que Sabe Esperar: Patrones de Resiliencia para Arquitecturas de Microservicios

El Reintento que Sabe Esperar: Patrones de Resiliencia para Arquitecturas de Microservicios

Has estado ahí. Tu microservicio recibe la respuesta de un servicio externo y no es un error técnico: no hay stack trace, no hay excepción que capturar, no hay un HTTP 500. Es algo más sutil, un estado de negocio que dice ahora no. CANCELADO. NO DISPONIBLE. El servicio está vivo, responde, simplemente no puede atender tu solicitud en este momento.

Poco después llega la instrucción del negocio: inténtalo más tarde. Y ahí empieza el problema real.

«Más tarde» suena razonable hasta que se traduce a un sistema distribuido. ¿Cuánto es más tarde? ¿Quién guarda esa intención mientras transcurre? ¿Qué ocurre si el proceso que debía esperar se reinicia a mitad de camino, o si el mismo mensaje se entrega dos veces y el reintento se duplica? ¿Y si llegan mil respuestas CANCELADO en el mismo minuto y todas deben esperar a la vez sin que nadie gaste recursos esperando?

Lo que parecía un detalle de implementación es un problema de diseño con nombre, con componentes y con varias formas de salir mal si se improvisa. Este artículo recorre ese diseño completo: por qué la espera debe ser un dato y no un proceso, qué piezas la sostienen, cómo se evita que un fallo en cualquier punto produzca duplicados o transacciones atascadas, y cómo se concreta todo en AWS, sin olvidar que el patrón es más importante que cualquier servicio.

Por qué no basta con dormir el proceso

El primer instinto suele ser retener el proceso. Un Thread.sleep, un hilo bloqueado, o un mensaje que no se confirma para que el broker lo reentregue pasado un rato. Es tentador porque funciona en local, con poco volumen y en las demostraciones donde los casos borde son teóricos.

El costo, sin embargo, crece en silencio: un proceso que espera sigue siendo un proceso. Con diez esperas simultáneas el impacto es marginal. Con mil, cifra que cualquier sistema bajo carga real alcanza sin esfuerzo, la infraestructura paga por tiempo en el que no trabaja. Escalar horizontalmente no ayuda, porque solo multiplica los procesos igualmente bloqueados.

La raíz del problema es conceptual. Se está modelando la espera como una actividad, cuando en realidad es un estado. Un proceso dormido consume memoria, hilos y conexiones, mientras que un registro en una base de datos no consume nada mientras espera: existe, y el sistema lo consulta cuando necesita saber qué hacer.

De esa distinción nace el patrón de reintento diferido duradero. Consiste en convertir la intención de «volver a intentarlo en el instante T» en información persistente, en lugar de mantenerla como tiempo de ejecución.

Tres preguntas, tres responsabilidades

Para que la espera viva en el sistema sin ocupar procesos hay que responder tres preguntas distintas, y conviene que cada una tenga un dueño distinto. La primera es cuándo debe ocurrir el reintento: alguien tiene que conservar la intención «actúa en este instante» y cumplirla aunque pasen horas. La segunda es si el reintento todavía tiene sentido cuando el momento llegue, porque la transacción pudo resolverse por otra vía mientras esperaba. La tercera es cómo garantizar que el reintento se ejecute una sola vez, aunque el sistema falle en el medio.

El diseño reparte esas responsabilidades entre unas pocas piezas. Tres colas desacoplan a los participantes: una recibe las respuestas del servicio externo, otra entrega los eventos de reintento cuando el planificador los dispara, y una tercera lleva las solicitudes hacia el servicio externo. Un almacén de estado funciona como fuente de verdad: guarda el estado de cada transacción, el intento en curso y el instante previsto del próximo disparo, y solo acepta cambios condicionales. Un planificador diferido conserva la intención de «entregar este evento en el instante T» sin mantener ningún proceso ocupado. Dos manejadores ejecutan la lógica de negocio: el manejador de respuestas (lo llamaré A) interpreta lo que dice el servicio externo y decide si hay que planificar un reintento, y el manejador de reintentos (B) recibe el evento del planificador, comprueba que siga siendo válido y publica la nueva solicitud. Por último, las colas de mensajes fallidos (DLQ, por dead-letter queue) aíslan lo que no se pudo procesar o entregar y levantan alertas.

Lo que hace correcto al conjunto no es la sofisticación de cada pieza, sino que cada una resuelve exactamente una cosa y desconoce las demás. El planificador no sabe nada de negocio, el almacén no sabe cuándo disparar y los manejadores no se conocen entre sí. Esa ignorancia mutua permite que el sistema se recupere de un fallo sin que ninguna pieza tenga que coordinarse con otra.

Hay además una decisión que protege el modelo existente: el planificador nunca habla directamente con el servicio externo. Publica en la cola de reintento y es B quien decide qué hacer con ese evento. Así no se abren nuevos endpoints ni se introducen caminos de ejecución paralelos. El planificador responde al cuándo y la lógica de negocio sigue viviendo donde siempre vivió.

El siguiente diagrama sigue el recorrido de un mensaje desde que el servicio externo responde CANCELADO hasta que se publica una nueva solicitud, o se descarta si ya no hace falta. Las líneas punteadas marcan rutas de fallo.

flowchart TD
    EXT["Servicio externo"] --> QR[["Cola de respuesta"]]
    QR --> A["Manejador de respuestas (A)"]
    QR -.->|mensajes fallidos| DLQ0[["DLQ de respuesta"]]
    A <--> DB[("Almacén de estado")]

    A -->|EXITOSO| OK["Flujo habitual"]
    A -->|CANCELADO, intentos agotados| FAIL["Marcar FALLIDA<br/>+ alerta"]
    A -->|"CANCELADO, intentos disponibles<br/>(1. PLANIFICANDO + proximo_disparo<br/>2. crear planificación)"| PL["Planificador diferido<br/>(espera creciente)"]

    PL --> QRE[["Cola de reintento"]]
    PL -.->|falla de entrega| DLQ1[["DLQ del planificador"]]
    QRE -.->|mensajes fallidos| DLQ2[["DLQ de reintento"]]

    QRE --> B["Manejador de reintentos (B)"]
    B <--> DB
    B -->|obsoleto o ya procesado| DROP["Descartar"]
    B -->|sigue pendiente| QS[["Cola de solicitud"]]
    QS --> EXT

Una convención pequeña que evita errores grandes

Antes de hablar de estados hay que fijar un detalle de numeración que parece trivial y, sin embargo, es responsable de errores muy difíciles de rastrear. El intento = 1 es la solicitud original, la primera vez que el microservicio contacta al servicio externo. El primer reintento es el intento = 2, el segundo es el intento = 3, y así sucesivamente. El campo max_intentos cuenta la solicitud original, no solo los reintentos: con max_intentos = 4 hay una solicitud original y hasta tres reintentos.

Si esto no queda escrito, el error de off-by-one aparece tarde y en producción. Una transacción hace un reintento de más porque alguien contó desde cero, o se detiene antes de tiempo porque otro incluyó el original en el conteo de reintentos. Son bugs silenciosos: el sistema funciona, pero no se comporta como el negocio espera.

El almacén de estado y las reglas del juego

Con la numeración clara, el siguiente paso es definir qué guarda el almacén para cada transacción. Son pocos campos, pero cada uno tiene una razón de ser.

CampoQué significaCuándo cambia
transaccion_idClave primariaNunca
estadoEN_CURSO, PLANIFICANDO, PLANIFICADO, ENVIANDO, EXITOSA o FALLIDAEn cada transición condicional
intento_actualÚltimo intento (n) cuya solicitud salió hacia el servicio externoAl pasar de ENVIANDO a EN_CURSO, donde se convierte en n+1
intento_siguienteIntento (n+1) que se está planificando o enviando; vacío en otro casoSe fija al pasar de EN_CURSO a PLANIFICANDO y se limpia al volver a EN_CURSO
max_intentosTope de intentos, contando el originalConstante
proximo_disparoInstante UTC del reintento, con la variación aleatoria ya incorporadaSe escribe junto con la transición a PLANIFICANDO
actualizado_enAuditoría y detección de transacciones detenidasEn cada escritura

La razón de modelar así es que la corrección de un flujo distribuido no está en el orden de los pasos. La trampa más común al diseñar reintentos es suponer que, si cada paso se ejecuta en el orden correcto, el resultado será correcto. Pero los mensajes se reentregan, se duplican, llegan tarde, o el proceso se cae a mitad de una operación y reaparece con el mismo mensaje en la cola. En esas condiciones, la única forma de mantener la consistencia es trabajar con estados explícitos y permitir cada transición solo si el estado actual es exactamente el esperado. Si no coincide, el mensaje es obsoleto o duplicado, y se descarta sin ejecutar nada. En DynamoDB esto se logra con una ConditionExpression, y en una base relacional con un UPDATE ... WHERE estado = :esperado AND intento_actual = :n.

La vida de una transacción discurre así. Nace en EN_CURSO cuando se envía la solicitud original. Si la respuesta es exitosa, termina en EXITOSA. Si llega un CANCELADO y los intentos están agotados, termina en FALLIDA. Pero si todavía quedan intentos, pasa primero a PLANIFICANDO, un estado intermedio que significa «se decidió reintentar, aunque la planificación puede no existir aún», y luego a PLANIFICADO cuando la planificación queda confirmada. Cuando llega el evento, la transacción pasa a ENVIANDO mientras se publica la nueva solicitud y vuelve a EN_CURSO, ya con el intento incrementado, cuando la solicitud sale. Si mientras espera la transacción se resuelve por otra vía (un proceso manual, una corrección externa), puede pasar directamente a EXITOSA.

stateDiagram-v2
    [*] --> EN_CURSO: solicitud enviada (intento n)
    EN_CURSO --> EXITOSA: respuesta EXITOSO
    EN_CURSO --> FALLIDA: CANCELADO y n = max_intentos
    EN_CURSO --> PLANIFICANDO: CANCELADO y n < max_intentos<br/>(guarda intento_siguiente y proximo_disparo)
    PLANIFICANDO --> PLANIFICADO: planificación confirmada
    PLANIFICANDO --> ENVIANDO: evento llega antes de confirmar
    PLANIFICADO --> ENVIANDO: llega el evento (intento n+1)
    ENVIANDO --> ENVIANDO: reentrega retoma la publicación
    ENVIANDO --> EN_CURSO: solicitud publicada (intento_actual = n+1)
    PLANIFICANDO --> EXITOSA: resuelta por otra vía
    PLANIFICADO --> EXITOSA: resuelta por otra vía
    EXITOSA --> [*]
    FALLIDA --> [*]

Cada flecha del diagrama es, en realidad, una condición: «pasa a este estado solo si el estado actual es aquel y el número de intento es este». Si cualquiera de las dos cosas falla, la operación no se ejecuta, y esa regla aplicada sin excepciones es lo que hace correcto al sistema aunque los mensajes lleguen duplicados o desordenados.

Conviene decir que la regla de «resuelta por otra vía» es una decisión, no una verdad universal. Aquí se permite desde PLANIFICANDO y PLANIFICADO, pero no desde ENVIANDO: en ese punto la solicitud ya está en vuelo y será la respuesta del servicio externo la que decida el destino. Otros equipos podrían optar por permitirla también desde ENVIANDO e intentar cancelar la solicitud. Lo importante es elegir y dejarlo documentado.

Tres escrituras que no son atómicas

Cuando A recibe un CANCELADO y decide planificar un reintento, tiene que hacer tres cosas: actualizar el almacén de estado, crear la planificación y confirmar el mensaje de la cola. Ninguna de las tres está conectada a las otras por una transacción. Si el proceso cae entre dos de ellas, el mensaje se reentregará y A deberá poder retomar desde donde quedó, sin duplicar ni perder nada.

La primera versión intuitiva de este flujo calculaba la fecha del reintento en el momento de crear la planificación. Funciona hasta que el proceso cae justo después de crearla y antes de anotar nada: la reentrega vuelve a calcular, y como la espera incluye una variación aleatoria, obtiene un instante distinto y crea una segunda planificación para el mismo intento. El negocio terminaba viendo reintentos duplicados sin que ningún componente hubiera «fallado» en sentido estricto.

La solución es cuestión de orden. Cuando A detecta un CANCELADO con intentos disponibles, lo primero que hace es una transición condicional de EN_CURSO(n) a PLANIFICANDO, y en esa misma escritura guarda intento_siguiente = n+1 y el proximo_disparo, el instante exacto del reintento. Solo después crea la planificación, usando ese instante ya persistido. Si el proceso cae entre ambos pasos, la reentrega encuentra la transacción en PLANIFICANDO, lee el proximo_disparo guardado e intenta crear la planificación de nuevo. Si ya existía, el planificador responde con un error de conflicto que A trata como éxito, porque la planificación está creada con el instante correcto. El resultado es el mismo: un único reintento programado.

El flujo completo de A queda así. Primero descarta las respuestas tardías: si el número de intento de la respuesta no coincide con intento_actual, pertenece a un intento viejo y se confirma sin más. Si es EXITOSO y la transacción está EN_CURSO, pasa a EXITOSA. Si es CANCELADO y n = max_intentos, pasa a FALLIDA y se emite una alerta de negocio. Y si es CANCELADO con intentos disponibles, ejecuta la secuencia descrita: guardar el instante, crear la planificación, marcar PLANIFICADO y confirmar el mensaje. Si en cualquier reentrega la transacción ya está más adelante, en PLANIFICADO o ENVIANDO, significa que otra ejecución ya hizo el trabajo: A confirma el mensaje y termina. Del mismo modo, si la transición final a PLANIFICADO falla porque el estado ya avanzó, es un éxito, no un error.

sequenceDiagram
    participant Q as Cola de respuesta
    participant A as Manejador A
    participant DB as Almacén de estado
    participant PL as Planificador
    Q->>A: CANCELADO (intento n)
    A->>DB: EN_CURSO(n) → PLANIFICANDO + proximo_disparo
    Note over A,DB: Punto de fallo 1
    A->>PL: Crear planificación (nombre determinista)
    Note over A,PL: Punto de fallo 2
    PL-->>A: OK o conflicto (= éxito)
    A->>DB: PLANIFICANDO → PLANIFICADO
    Note over A,DB: Punto de fallo 3
    A->>Q: confirmar mensaje

Cuando el evento llega: ejecutar una sola vez

Horas después, el planificador dispara el evento y este aterriza en la cola de reintento, donde B lo recoge. Tanto el planificador como la cola ofrecen entrega al menos una vez, lo cual significa que el mismo evento puede llegar dos veces. B tiene que producir el mismo resultado en ambos casos, y para ello usa las mismas transiciones condicionales que A.

Al recibir el evento, B lee la transacción. Si el intento del evento no coincide con intento_siguiente, el evento es obsoleto y se descarta. Si coincide, mira el estado: desde PLANIFICADO o PLANIFICANDO intenta la transición a ENVIANDO; si ya está en EN_CURSO, EXITOSA o FALLIDA, el mensaje es un duplicado de un procesamiento terminado y se descarta. Una vez en ENVIANDO, publica la nueva solicitud en la cola del servicio externo con una clave de idempotencia que combina el identificador de la transacción con el número de intento (por ejemplo, TX-100200#2). Por último, la transición de ENVIANDO a EN_CURSO fija intento_actual = n+1, limpia intento_siguiente y cierra el ciclo.

Aquí aparecieron dos dificultades.

La primera es una carrera. Si el evento llega mientras la transacción sigue en PLANIFICANDO, porque A fue lento, cayó o su reentrega se demoró, un B que exigiera PLANIFICADO descartaría el evento, y después A marcaría PLANIFICADO sin que nadie volviera a disparar el reintento. La transacción quedaría esperando un evento que ya pasó. La salida es que B acepte también PLANIFICANDO como estado de partida, y que A trate como éxito el fallo condicional que encontrará después.

La segunda es más sutil. Si B cae después de pasar a ENVIANDO y antes de publicar, la reentrega encuentra la transacción en ENVIANDO. Con una regla ingenua de «si el estado no es el esperado, descartar», ese mensaje se tiraría y la transacción quedaría atascada para siempre, sin solicitud y sin nadie que la reintente. Por eso, cuando B encuentra ENVIANDO con el intento correcto, no descarta: retoma. Vuelve a publicar con la misma clave de idempotencia y completa la transición a EN_CURSO.

Esa decisión tiene un precio que hay que aceptar con los ojos abiertos. Si dos entregas del mismo evento llegan en paralelo, ambas pueden publicar la solicitud. Es inocuo únicamente porque el servicio externo descarta el duplicado gracias a la clave de idempotencia. Esa es una precondición del patrón, no un detalle: si el servicio externo no respeta la clave, el diseño necesita el patrón outbox. En él, la solicitud a publicar se guarda en el almacén junto con la transición de estado, y un publicador independiente se encarga de enviarla de forma fiable. Es más costoso de construir, pero elimina del todo la ventana entre publicar y confirmar.

sequenceDiagram
    participant PL as Planificador
    participant Q as Cola de reintento
    participant B as Manejador B
    participant DB as Almacén de estado
    participant QS as Cola de solicitud
    PL->>Q: evento (intento n+1)
    Q->>B: entrega del evento
    B->>DB: PLANIFICADO o PLANIFICANDO → ENVIANDO
    Note over B,DB: Punto de fallo 1
    B->>QS: publicar (clave transaccion_id#n+1)
    Note over B,QS: Punto de fallo 2
    B->>DB: ENVIANDO → EN_CURSO (intento_actual = n+1)
    Note over B,DB: Punto de fallo 3
    B->>Q: confirmar mensaje

Una espera que crece para no presionar siempre igual

Hay una pregunta que todavía no hemos contestado: cuánto hay que esperar. Si la causa del CANCELADO persiste, porque el servicio externo tiene un problema que tarda en resolverse, reintentar con el mismo intervalo cada vez aplica la misma presión sin darle tiempo real de recuperarse. La espera debería crecer. La fórmula, configurable, es la siguiente:

espera(k) = min( base × factor^(k-1) , tope ) × (1 + u),   u ∈ [-jitter, +jitter]

Aquí k es el número de reintento (tras fallar el intento n, se usa k = n), base es la espera antes del primer reintento, factor es cuánto crece en cada paso, tope es el máximo de la espera nominal y jitter es la variación aleatoria relativa. Con valores típicos (base = 2 h, factor = 2, tope = 24 h, jitter = ±10 %), una transacción con max_intentos = 4 queda así:

ReintentoIntentoEspera nominalRango con ±10 %
122 h1 h 48 min – 2 h 12 min
234 h3 h 36 min – 4 h 24 min
348 h7 h 12 min – 8 h 48 min

Un matiz que suele pasarse por alto: en la fórmula, la variación se aplica después del tope, de modo que la espera real puede llegar a tope × 1,1, es decir, 26,4 horas con estos valores. Si el negocio necesita un máximo estricto, hay que recortar de nuevo tras aplicar la variación. Cualquiera de las dos opciones es válida, siempre que esté documentada.

El jitter merece su propio párrafo porque no es un adorno. Imagina que llegan mil respuestas CANCELADO en el mismo minuto, por una caída momentánea del servicio externo. Si todas calculan exactamente la misma espera, sus reintentos se disparan al mismo instante horas después y golpean al servicio en ráfaga, justo cuando intentaba recuperarse. La variación aleatoria dispersa esa ráfaga a lo largo de un intervalo, y reparte la carga de forma natural.

Hay, además, una decisión con impacto directo en la corrección: el jitter se calcula en la aplicación, no se delega al planificador. Así el proximo_disparo, con su variación ya incorporada, queda registrado en el almacén. Si el proceso falla y el mensaje se reentrega, la planificación se recrea con exactamente el mismo instante, y el comportamiento es determinista aunque el proceso se haya reiniciado.

Esta política tampoco tiene por qué ser uniforme. No todo CANCELADO representa la misma situación de negocio: algunos justifican esperas cortas y otros largas, y el criterio puede depender del tipo de transacción, del cliente o del motivo concreto del rechazo.

Proteger al servicio que se quiere recuperar

El jitter ayuda, pero no es suficiente si el volumen es muy alto o si el servicio externo está en un estado delicado. B debería incorporar tres controles adicionales.

El primero es limitar su propia concurrencia al publicar en la cola de solicitud. Si B puede procesar cien eventos por segundo pero el servicio externo solo absorbe diez, el límite debe vivir en B y no depender de que el servicio resista.

El segundo es un cortacircuitos (circuit breaker). Si el servicio está claramente degradado, porque responde lento, con errores o directamente no responde, tiene más sentido pausar o reprogramar los reintentos que seguir gastando el presupuesto de max_intentos. Un fallo por indisponibilidad técnica no equivale a un CANCELADO de negocio, y tratarlos igual acorta artificialmente la vida de una transacción que aún podría resolverse. Conviene advertir que la máquina de estados que hemos descrito no modela esta transición; hacerlo implicaría permitir, por ejemplo, volver de PLANIFICADO o ENVIANDO a PLANIFICANDO conservando intento_siguiente y fijando un nuevo proximo_disparo, con un nombre de planificación que incluya un sufijo de secuencia para evitar conflictos con el anterior.

El tercero es la cancelación proactiva de planificaciones. Si una transacción se resuelve por otra vía y todavía tiene una planificación pendiente, el sistema es correcto de todas formas: cuando el planificador dispare, B encontrará un estado distinto de los esperados y descartará el evento. Pero eliminar la planificación en cuanto la transacción se resuelve es una buena práctica, porque reduce ruido, eventos inútiles y costo.

Qué pasa cuando algo falla en el camino

La prueba real de un diseño así no es el flujo feliz, sino lo que ocurre cuando algo se rompe entre dos operaciones que no son atómicas. Recorrer los fallos relevantes es la mejor manera de comprobar que las reglas anteriores se sostienen.

FalloEstado en que quedaQué ocurre en la reentrega
A cae antes de pasar a PLANIFICANDOEN_CURSOEl mensaje se reentrega y el proceso se repite completo, sin efectos duplicados
A cae tras PLANIFICANDO, antes de crear la planificaciónPLANIFICANDOSe reutiliza el proximo_disparo guardado y se crea la planificación: un solo reintento
A cae tras crear la planificación, antes de marcar PLANIFICADOPLANIFICANDOLa creación responde «ya existe» y se trata como éxito
El evento llega mientras A sigue en PLANIFICANDOPLANIFICANDOB acepta ese estado y avanza a ENVIANDO; el fallo condicional posterior de A se trata como éxito
El planificador no puede entregar el eventoPLANIFICADOReintentos de entrega y, al agotarse, DLQ del planificador, con el mensaje conservado
El evento se entrega dos vecesPLANIFICADO → ENVIANDOB descarta el duplicado por transición condicional o retoma si la primera ejecución quedó a medias
B cae tras ENVIANDO, antes de publicarENVIANDOLa reentrega retoma y publica
B cae tras publicar, antes de pasar a EN_CURSOENVIANDOSe republica con la misma clave de idempotencia y el servicio externo descarta el duplicado
Llega una respuesta de un intento anteriorCualquieraSe descarta por el número de intento; el estado queda intacto
Se agotan los intentosFALLIDAAlerta de negocio: es un fallo de negocio, no técnico

El patrón se repite en todas las filas: el almacén de estado, con sus transiciones condicionales, actúa como árbitro. Cualquier operación que llegue cuando el estado no es el esperado simplemente no tiene efecto.

Hay un caso que esta tabla deja fuera a propósito, y es el de los mensajes que terminan en una DLQ. Un mensaje que cae allí significa que la transacción quedó detenida en un estado intermedio, y cada cola cuenta una historia diferente. La DLQ de respuesta indica que A falló repetidamente con una respuesta, y la transacción quedó en EN_CURSO o PLANIFICANDO. La DLQ del planificador indica que el planificador no pudo entregar el evento a la cola, y la transacción espera en PLANIFICADO. La DLQ de reintento indica que B falló repetidamente con un evento que sí llegó, y la transacción puede estar en PLANIFICANDO, PLANIFICADO o ENVIANDO. En los tres casos, el procedimiento es el mismo: diagnosticar la causa, corregirla y reenviar el mensaje a su cola de origen. Como los manejadores son idempotentes, reprocesar es seguro. Toda DLQ debería tener una alarma que salte con el primer mensaje, porque cualquiera de ellos es una incidencia.

Aun así, un diseño serio no debería depender únicamente de que alguien mire las DLQ. Por eso conviene sumar un reconciliador, un proceso periódico que busca transacciones detenidas en un estado intermedio durante más de un umbral razonable (por ejemplo, el doble de la latencia máxima esperada). Si encuentra una en PLANIFICANDO, reintenta la creación de la planificación con el proximo_disparo guardado. Si encuentra una en PLANIFICADO con el instante muy pasado, publica directamente el evento en la cola de reintento. Si la encuentra en ENVIANDO, republica el evento para que B retome. Como todas esas acciones pasan por los mismos manejadores idempotentes, el reconciliador no introduce riesgos nuevos. Para ejecutarlo de forma eficiente en DynamoDB hace falta un índice secundario por estado y actualizado_en.

Llevarlo a AWS

Con los conceptos claros, el mapeo a servicios de AWS es casi directo, porque cada componente lógico tiene un equivalente natural. Las colas de solicitud, respuesta y reintento son colas Amazon SQS de tipo Standard. El planificador diferido es Amazon EventBridge Scheduler, usando planificaciones de una sola ejecución (one-time schedules). El almacén de estado puede ser Amazon DynamoDB, con ConditionExpression para las transiciones, o una base relacional con UPDATE ... WHERE si ya forma parte del stack. Las DLQ son colas SQS adicionales, y la plataforma de ejecución es Amazon EKS, con pods que obtienen sus permisos mediante IRSA o EKS Pod Identity. Es una combinación razonable, pero no la única posible: lo esencial del patrón sobrevive a cualquier otra elección.

La elección del planificador responde a una limitación concreta. El retraso nativo de SQS tiene un máximo de 15 minutos, suficiente para reintentos rápidos pero insuficiente cuando el negocio pide esperas de horas. EventBridge Scheduler permite registrar un evento para cualquier instante futuro, sean horas o días, sin mantener ningún proceso activo durante la espera. Las planificaciones one-time se disparan una vez y, si se configura ActionAfterCompletion = DELETE, se eliminan solas.

Un detalle que suele confundir al principio: las DLQ de esta arquitectura se configuran de dos maneras distintas. La del planificador se define en el destino de cada planificación, mediante DeadLetterConfig. Las de SQS (respuesta y reintento) se definen en la cola de origen mediante una redrive policy con un maxReceiveCount, y conviene que el visibility timeout de esas colas sea mayor que el tiempo máximo de procesamiento del manejador, para que un mensaje en proceso no reaparezca en otro consumidor.

Cómo configurar cada planificación

Cada reintento se registra como una planificación con una expresión at(yyyy-mm-ddThh:mm:ss) que fija el instante exacto. Hay varios parámetros que es fácil dejar implícitos y que después producen comportamientos inesperados.

El ScheduleExpressionTimezone debe ser explícito. Sin él, la expresión at() se interpreta en UTC, y si alguien del equipo no lo tiene presente puede esperar disparos en otro horario. Lo más sencillo es calcular siempre en UTC y dejarlo escrito. El FlexibleTimeWindow debe estar en OFF: esa propiedad permite al planificador añadir una ventana de flexibilidad para optimizar recursos, pero con ella activa el instante real de disparo deja de ser predecible, y no tiene sentido ceder ese determinismo cuando el proximo_disparo ya incorpora el jitter y está guardado. El ActionAfterCompletion debe ser DELETE, para que las planificaciones ejecutadas desaparezcan en lugar de acumularse y ocupar cuota sin propósito. Conviene, también, usar un grupo de planificaciones dedicado a los reintentos, que permite aislar permisos, etiquetas y cuotas del resto de la cuenta. Y, por último, la RetryPolicy y la DeadLetterConfig del destino garantizan que, si el planificador no logra entregar el evento a SQS, lo reintente y, al agotarse los intentos, lo envíe a la DLQ del planificador para su análisis.

Este es un ejemplo concreto, el de la planificación del segundo intento de la transacción TX-100200:

{
  "Name": "retry-TX-100200-2",
  "GroupName": "reintentos-ms-procesamiento",
  "ScheduleExpression": "at(2026-10-03T16:38:00)",
  "ScheduleExpressionTimezone": "UTC",
  "FlexibleTimeWindow": { "Mode": "OFF" },
  "ActionAfterCompletion": "DELETE",
  "Target": {
    "Arn": "arn:aws:sqs:<region>:<cuenta>:cola-reintento",
    "RoleArn": "arn:aws:iam::<cuenta>:role/scheduler-reintentos",
    "Input": "{\"transaccion_id\":\"TX-100200\",\"origen\":\"PLANIFICADOR_REINTENTO\",\"intento\":2}",
    "RetryPolicy": {
      "MaximumRetryAttempts": 5,
      "MaximumEventAgeInSeconds": 3600
    },
    "DeadLetterConfig": {
      "Arn": "arn:aws:sqs:<region>:<cuenta>:dlq-planificador"
    }
  }
}

Como JSON no admite comentarios, vale la pena leer el ejemplo campo por campo: Name es un nombre determinista (prefijo fijo, identificador de la transacción y número de intento); ScheduleExpression contiene el proximo_disparo en UTC; Target.Arn apunta a la cola de reintento; RoleArn es el rol que el planificador asume para escribir en esa cola; Input es la carga que B recibirá, con el número de intento incluido; y RetryPolicy y DeadLetterConfig gobiernan lo que ocurre si la entrega falla.

Idempotencia al crear la planificación

El nombre retry-TX-100200-2 es determinista a propósito. Si A intenta crear una planificación que ya existe, el planificador responde con un ConflictException, y A debe tratarlo explícitamente como un éxito y no como un error.

Pero hay un matiz importante. Con ActionAfterCompletion = DELETE, la planificación desaparece después de dispararse, y el nombre queda libre de nuevo. Eso significa que la unicidad del nombre no es la barrera definitiva contra los duplicados. Esa barrera sigue siendo el almacén de estado con sus transiciones condicionales: el planificador resuelve el cuándo, pero la corrección la garantiza el almacén. Un último detalle práctico: los nombres de planificación tienen restricciones de longitud y de caracteres, así que si el identificador de la transacción es largo o contiene símbolos especiales conviene codificarlo, y un hash corto funciona bien.

Permisos con el menor privilegio posible

Los pods del microservicio necesitan scheduler:CreateSchedule para crear planificaciones, scheduler:DeleteSchedule si se implementa la cancelación proactiva, e iam:PassRole restringido al rol del planificador, todo acotado al grupo de planificaciones dedicado. El rol del planificador, por su parte, solo necesita sqs:SendMessage sobre la cola de reintento y la DLQ del planificador. Si las colas están cifradas con una clave KMS gestionada por el cliente, ese rol requiere además kms:GenerateDataKey y kms:Decrypt sobre la clave. Y la política de la cola de reintento debería aceptar mensajes únicamente del rol del planificador: esa restricción cierra la posibilidad de que cualquier otro componente inyecte reintentos de forma no autorizada.

Las cuotas que conviene mirar antes de producción

Crear planificaciones es una llamada a una API con límites de tasa, y existe además un máximo de planificaciones activas simultáneamente por cuenta y región. Esos valores cambian con el tiempo, así que la fuente definitiva es Service Quotas en la consola de AWS y no la documentación de terceros.

El volumen que importa estimar no es solo la ráfaga máxima de CANCELADO por segundo, sino también cuántas planificaciones pueden estar activas al mismo tiempo. Con espera creciente, cada planificación vive más, hasta varias horas, y con muchas transacciones en espera el número puede crecer más de lo esperado. Para absorber ráfagas sin perder mensajes hay tres medidas. La primera es limitar la concurrencia del consumidor de la cola de respuesta: SQS aplica contrapresión de forma natural, y si la creación falla por throttling, el mensaje reaparece tras el visibility timeout y se reintenta. La segunda es usar reintentos con backoff y jitter en la propia llamada de creación, para no amplificar el throttling. Y la tercera es solicitar un aumento de cuota con antelación si el análisis de capacidad lo indica.

Si no hay un planificador gestionado

Todo lo anterior es independiente de la tecnología, porque el planificador diferido es un componente lógico y no necesariamente un servicio gestionado. Si el entorno no ofrece uno, la alternativa más directa es una tabla de reintentos pendientes en la base de datos, junto con un proceso periódico (poller) que busque las transacciones cuyo proximo_disparo ya pasó y las envíe a la cola de reintento.

Es más portable y no introduce dependencias externas, pero tiene un costo real: un componente más que operar, una latencia que depende de la frecuencia del sondeo en lugar de ser casi inmediata, y una presión de lectura adicional sobre el almacén proporcional al número de transacciones pendientes. La máquina de estados, la idempotencia y la política de espera creciente no cambian en absoluto; solo cambia quién dispara el evento cuando llega el momento.

La corrección del patrón no depende de tener un planificador diferido gestionado. Depende del diseño: la máquina de estados, las transiciones condicionales y los manejadores idempotentes funcionan igual con cualquier mecanismo de disparo.

Lo que hay que medir para confiar en el sistema

Un flujo con esta complejidad necesita observabilidad proporcional. Sin métricas concretas es imposible saber si el sistema funciona como se diseñó o si acumula problemas que solo aflorarán bajo carga.

En cada cola SQS hay dos métricas esenciales: ApproximateNumberOfMessagesVisible, que indica cuántos mensajes esperan procesamiento, y ApproximateAgeOfOldestMessage, que indica cuánto lleva esperando el más antiguo y suele ser la primera señal de un consumidor atascado o caído. Las DLQ merecen vigilancia aparte, porque significan cosas distintas, como vimos antes, y su alarma debería saltar ante el primer mensaje. EventBridge Scheduler publica en CloudWatch métricas de invocaciones al destino, errores de entrega, entregas enviadas a la DLQ y throttling; un pico de errores de entrega suele delatar un problema de permisos o de disponibilidad de SQS que de otro modo pasaría desapercibido. Los nombres exactos de esas métricas conviene verificarlos en la documentación vigente de AWS antes de definir alarmas.

Las métricas más valiosas para entender el comportamiento de negocio son las del propio microservicio: el porcentaje de respuestas CANCELADO sobre el total, el número de planificaciones creadas y de conflictos recibidos (que mide, indirectamente, la frecuencia de las reentregas), las transacciones que terminan en FALLIDA por intentos agotados, y la distribución de transacciones por estado e intento. Esta última permite detectar, por ejemplo, un acúmulo de transacciones en PLANIFICANDO que no avanzan a PLANIFICADO, señal clara de un problema al crear planificaciones.

Pero lo que más se agradece en un incidente real no es una métrica sino un registro de auditoría: la posibilidad de reconstruir el historial completo de una transacción, con qué intentos ocurrieron, con qué identificador de planificación, cuándo se creó, cuándo se esperaba el disparo y cuál fue el resultado. El almacén de estado solo guarda el estado actual, así que este historial exige una tabla de eventos de solo-añadir, donde cada transición deje una fila. Sin ella, cuando algo sale mal en producción la respuesta es especulación; con ella, el diagnóstico es una consulta.

Lo que el patrón garantiza, y lo que no

Conviene nombrar con claridad las garantías. Una transacción no produce reintentos duplicados. El ciclo es finito, porque cada transacción termina en un estado terminal, sea EXITOSA o FALLIDA. Y el sistema se comporta correctamente ante reentregas y fallos en cualquier punto del flujo, siempre que se cumpla la precondición de idempotencia del servicio externo, o se adopte outbox.

Lo que no garantiza es precisión de reloj. El instante programado es el momento en que el evento queda disponible en la cola, no el momento en que se ejecuta, y entre ambos intervienen la cola, la disponibilidad del consumidor y su concurrencia. La desviación es de segundos en condiciones normales. Es algo distinto del jitter, que es intencional y puede ser de minutos; ambas cosas coexisten y hay que distinguirlas al documentar el comportamiento esperado.

Antes de llevar el patrón a producción, estas garantías deberían convertirse en pruebas formales y no en afirmaciones de diseño. Una ráfaga de N cancelaciones simultáneas no debe perder ni duplicar ningún reintento. La reentrega forzada de cada mensaje, en cada paso del flujo, no debe alterar el resultado final, lo que incluye el caso del evento que llega con la transacción aún en PLANIFICANDO y el de B caído en ENVIANDO. Ninguna transacción debe superar max_intentos. Una respuesta tardía de un intento anterior no debe modificar el estado. Las alertas de DLQ y de intentos agotados deben dispararse, el reconciliador debe reanudar transacciones detenidas en cada estado intermedio, y el servicio externo no debe recibir más de X solicitudes por segundo durante una ráfaga de reintentos. No son criterios opcionales de calidad: son la forma de demostrar que el diseño funciona en condiciones reales y no solo en el camino feliz.

La espera como decisión de diseño

Volvamos al punto de partida. El negocio pide reintentar más tarde, y alguien tiene que decidir cómo se modela ese «más tarde» dentro del sistema.

Como resumen, estos son los puntos que sostienen todo lo anterior. La espera debe ser un estado persistente y no un proceso dormido, porque lo primero no cuesta nada mientras transcurre y lo segundo se paga hora a hora. Cada pieza resuelve una sola responsabilidad (cuándo, si todavía tiene sentido, y una sola vez) y por eso el conjunto se recupera sin coordinación central. La corrección vive en las transiciones condicionales del almacén y no en el orden de los pasos, de modo que cualquier mensaje duplicado, tardío u obsoleto simplemente no tiene efecto. Guardar el instante del reintento antes de crear la planificación, aceptar PLANIFICANDO en B y retomar desde ENVIANDO son las tres decisiones que cierran los huecos que las primeras versiones del diseño dejaban abiertos. Y la espera creciente con jitter calculado en la aplicación protege al servicio externo y mantiene el comportamiento determinista.

La diferencia entre un Thread.sleep y este patrón no es de complejidad superficial, sino de qué tan bien el sistema entiende su propia situación. Un proceso que duerme no sabe que está esperando un reintento, no puede decirlo, no puede auditarse y no puede recuperarse de un fallo sin ayuda externa. Un almacén de estado con transiciones condicionales sí sabe en qué punto está, puede registrarlo, puede responder preguntas sobre su historial y puede retomar exactamente donde lo dejó si algo falla. Eso es lo que hace escalable la solución, y no el hecho de que use servicios concretos: el planificador es intercambiable, mañana puede ser una tabla con un poller o un servicio de otro proveedor, y lo que permanece es la máquina de estados, la idempotencia y la política de espera.

Quedan, además, varias líneas abiertas que merecen exploración. La más inmediata es modelar formalmente el cortacircuitos dentro de la máquina de estados, distinguiendo con claridad las indisponibilidades técnicas de los rechazos de negocio, para que las primeras no consuman el presupuesto de intentos. Otra es refinar la política de espera según el motivo del rechazo, o incluso volverla adaptativa, ajustando la base y el factor a partir de la tasa de éxito observada en reintentos anteriores. También vale la pena estudiar la adopción de outbox de forma sistemática en los casos donde el servicio externo no ofrezca garantías de idempotencia, y evaluar su costo frente al de aceptar un duplicado ocasional. Y, en el plano de la validación, tiene mucho potencial la inyección controlada de fallos (chaos engineering) aplicada a cada uno de los puntos intermedios descritos, para comprobar de forma continua, y no solo antes del lanzamiento, que las garantías se mantienen a medida que el sistema evoluciona.

Darle tiempo al servicio externo para recuperarse no es una concesión ante un fallo. Es reconocer que en sistemas distribuidos los fallos suelen ser temporales, que una segunda oportunidad bien diseñada tiene un valor real, y que esperar bien, sin desperdiciar recursos, sin duplicar y sin perder el hilo, es una forma de resiliencia tan importante como actuar rápido cuando todo va bien.

Related Posts

Transformando Colecciones con Java Streams: 15 Métodos Esenciales

Transformando Colecciones con Java Streams: 15 Métodos Esenciales

Introducción En el mundo de Java, trabajar con colecciones de datos solía ser sinónimo de bucles interminables, condicionales anidados y código repetitivo. Pero con la llegada de Java Streams

Cuándo Usar Colas de Mensajes en el Desarrollo de Software

Cuándo Usar Colas de Mensajes en el Desarrollo de Software

Las colas de mensajes son herramientas clave para construir sistemas distribuidos, escalables y tolerantes a fallos. En este artículo te comparto una guía con situaciones comunes donde su uso es altam

RabbitMQ 1: Introducción a RabbitMQ, El Corazón de la Mensajería Asíncrona

RabbitMQ 1: Introducción a RabbitMQ, El Corazón de la Mensajería Asíncrona

En el mundo del desarrollo de software moderno, especialmente con el auge de los microservicios y los sistemas distribuidos, la forma en que las diferentes partes de una aplicación se comunican es fun

RabbitMQ 2: Arquitectura y Enrutamiento Avanzado en RabbitMQ

RabbitMQ 2: Arquitectura y Enrutamiento Avanzado en RabbitMQ

En nuestro primer artículo, exploramos qué es RabbitMQ, por qué es fundamental para la comunicación asíncrona en sistemas distribuidos y cuáles son sus casos de uso típicos. Lo comparamos con una "ofi

RabbitMQ 3: Configuración y Gestión de Colas en RabbitMQ

RabbitMQ 3: Configuración y Gestión de Colas en RabbitMQ

Después de entender qué es RabbitMQ y cómo sus Exchanges y Bindings dirigen los mensajes, llegamos a la Cola. La cola es fundamentalmente un buffer confiable: es el lugar donde los mensajes esperan su

RabbitMQ 4: Robustez y Seguridad en RabbitMQ

RabbitMQ 4: Robustez y Seguridad en RabbitMQ

Hemos recorrido el camino desde la introducción a RabbitMQ y su papel en la mensajería asíncrona, pasando por su arquitectura, componentes de enrutamiento (Exchanges y Bindings), y la gestión detallad

RabbitMQ 5: Consumo de Recursos, Latencia y Monitorización de RabbitMQ

RabbitMQ 5: Consumo de Recursos, Latencia y Monitorización de RabbitMQ

Hemos explorado la teoría detrás de RabbitMQ, su arquitectura, cómo enruta mensajes y cómo podemos construir sistemas robustos y seguros. Sin embargo, para operar RabbitMQ de manera efectiva en produc

Kafka 1: Introducción a Apache Kafka, fundamentos y Casos de Uso

Kafka 1: Introducción a Apache Kafka, fundamentos y Casos de Uso

En el panorama tecnológico actual, los datos son el motor que impulsa la innovación. La capacidad de procesar, reaccionar y mover grandes volúmenes de datos en tiempo real se ha convertido en una nece

RabbitMQ 6: Alta Disponibilidad y Escalabilidad con Clustering en RabbitMQ

RabbitMQ 6: Alta Disponibilidad y Escalabilidad con Clustering en RabbitMQ

Hasta ahora, hemos hablado de cómo un nodo individual de RabbitMQ maneja mensajes, gestiona colas, y cómo monitorizar su rendimiento y seguridad. Sin embargo, para aplicaciones críticas que no pueden

Kafka 2: Arquitectura Profunda de Kafka, Topics, Particiones y Brokers

Kafka 2: Arquitectura Profunda de Kafka, Topics, Particiones y Brokers

En nuestro primer artículo, despegamos en el mundo de Apache Kafka, sentando las bases de lo que es esta potente plataforma de streaming de eventos y diferenciándola de los sistemas de mensajería trad

Kafka 3: Productores y Consumidores, Configuración y Buenas Prácticas

Kafka 3: Productores y Consumidores, Configuración y Buenas Prácticas

Hemos navegado por los conceptos esenciales de Apache Kafka y desentrañado la arquitectura que reside bajo la superficie, comprendiendo cómo los Topics se dividen en Particiones distribuidas entre Bro

Kafka 4: Procesamiento de Datos en Tiempo Real con Kafka Streams y ksqlDB

Kafka 4: Procesamiento de Datos en Tiempo Real con Kafka Streams y ksqlDB

En los artículos anteriores, hemos construido una sólida comprensión de Apache Kafka: qué es, por qué es una plataforma líder para streaming de eventos, cómo está estructurado internamente con Topic

Spring WebFlux 1: Fundamentos Reactivos y el Corazón de Reactor

Spring WebFlux 1: Fundamentos Reactivos y el Corazón de Reactor

¡Hola, entusiasta del desarrollo moderno! 👋 En el vertiginoso mundo de las aplicaciones web, donde la escalabilidad y la eficiencia son reyes, ha surgido un paradigma que desafía el modelo tradicion

Spring WebFlux 2: Alta Concurrencia sin Más Hilos

Spring WebFlux 2: Alta Concurrencia sin Más Hilos

¡Bienvenido de nuevo a nuestra inmersión en Spring WebFlux! 👋 En la primera parte de esta serie, exploramos el "por qué" de la programación reactiva, entendiendo los problemas del bloqueo y descubri

Kafka 6: Despliegue, Seguridad y Optimización

Kafka 6: Despliegue, Seguridad y Optimización

Hemos explorado la arquitectura fundamental de Apache Kafka, la dinámica entre productores y consumidores, sus potentes capacidades para el procesamiento de flujos de datos y las herramientas que enri

Spring WebFlux 3: Comunicación, Datos y Errores Reactivos

Spring WebFlux 3: Comunicación, Datos y Errores Reactivos

¡Continuemos nuestro viaje por el fascinante mundo de Spring WebFlux! En la Parte 1, sentamos las bases de la programación reactiva y exploramos Project Reactor, el corazón de WebFlux. En la **Pa

Kafka 7: Patrones Avanzados y Anti-Patrones con Kafka

Kafka 7: Patrones Avanzados y Anti-Patrones con Kafka

Hemos recorrido un camino considerable en nuestra serie sobre Apache Kafka. Desde sus fundamentos y arquitectura interna hasta la interacción con productores y consumidores, las herramientas de proces

Kafka 5: Más Allá del Core, Explorando el Ecosistema de Apache Kafka

Kafka 5: Más Allá del Core, Explorando el Ecosistema de Apache Kafka

Hemos navegado por las entrañas de Apache Kafka, comprendiendo su funcionamiento interno, la interacción entre productores y consumidores, e incluso cómo procesar datos en tiempo real con Kafka Stream

Spring WebFlux 4: Comunicación Avanzada, Pruebas y Producción

Spring WebFlux 4: Comunicación Avanzada, Pruebas y Producción

La serie Spring WebFlux nos ha llevado a través de un viaje fascinante por el mundo de la programación reactiva, desde sus fundamentos y el poder de Project Reactor hasta la construcción de arquit

Arquitectura DDD y Hexagonal: Construyendo Software para el Futuro

Arquitectura DDD y Hexagonal: Construyendo Software para el Futuro

En el dinámico mundo del desarrollo de software, la complejidad es el enemigo silencioso. Las aplicaciones crecen, los requisitos cambian y, sin una guía clara, el código puede convertirse rápidamente

Arquitectura de Base de Datos para Identidad, Autenticación y Autorización (IAM)

Arquitectura de Base de Datos para Identidad, Autenticación y Autorización (IAM)

Cuando se habla de seguridad en el contexto de una aplicación, la conversación casi siempre gira en torno a las capas visibles: el cifrado en tránsito, las políticas de contraseñas, los tokens de aute

Arquitectura distribuida y el abandono consciente de ACID

Arquitectura distribuida y el abandono consciente de ACID

En el mundo de los sistemas distribuidos, hay una verdad incómoda que enfrentamos tarde o temprano: no podemos tenerlo todo. La promesa de las transacciones ACID tradicionales —esa garantía tranquiliz

Estándar de Arquitectura: Transacciones Distribuidas (Patrón Saga)

Estándar de Arquitectura: Transacciones Distribuidas (Patrón Saga)

PARTE I: PRINCIPIOS Y NORMATIVA 1. Fundamentos de Consistencia Eventual Debido a la naturaleza distribuida del sistema, se abandona el modelo ACID tradicional (Atomicidad inmediata con bloque

Observabilidad sin Ruido: Diseñando un Sistema de Logs con AOP en Arquitecturas DDD

Observabilidad sin Ruido: Diseñando un Sistema de Logs con AOP en Arquitecturas DDD

Hay una tensión que todo equipo de desarrollo enfrenta tarde o temprano: la necesidad de saber qué está pasando dentro del sistema sin que esa necesidad contamine el código que lo hace funcionar. Los

Arquitectura Modular por Contexto: Cuando la Teoría se Encuentra con la Realidad

Arquitectura Modular por Contexto: Cuando la Teoría se Encuentra con la Realidad

Has estado ahí. Es lunes por la mañana, abres el proyecto en tu IDE, y necesitas modificar cómo se procesa un pedido. Treinta minutos después, todavía estás navegando entre carpetas intentando encontr

Cuando un sistema debe ejecutar lo mismo siempre y algo distinto cada vez

Cuando un sistema debe ejecutar lo mismo siempre y algo distinto cada vez

Imagina que estás diseñando el flujo de solicitud de productos financieros de un banco. Un cliente puede pedir una tarjeta de crédito o un crédito para comprar un vehículo. Los dos productos son disti

Observabilidad sin Ruido: Diseñando un Sistema de Logs con AOP en Arquitecturas DDD — Parte II

Observabilidad sin Ruido: Diseñando un Sistema de Logs con AOP en Arquitecturas DDD — Parte II

La primera parte de este artículo construyó el argumento conceptual: por qué los logs dispersos se convierten en deuda técnica, cómo AOP permite centralizar la observabilidad sin contaminar la lógica

Cuando el sistema nuevo tiene que hablarle al sistema viejo en su idioma

Cuando el sistema nuevo tiene que hablarle al sistema viejo en su idioma

Imagina que llevas meses construyendo un sistema moderno sobre PostgreSQL, desplegado en contenedores sobre una infraestructura en la nube. Los datos están bien estructurados, las relaciones son clara

Observabilidad sin Ruido: Diseñando un Sistema de Logs con AOP en Arquitecturas DDD — Parte III

Observabilidad sin Ruido: Diseñando un Sistema de Logs con AOP en Arquitecturas DDD — Parte III

Las dos primeras partes de esta serie resolvieron un problema bien delimitado: construir un sistema de logging centralizado que operara de forma transversal sobre una arquitectura DDD sin contaminar l

Vistas y funciones como contratos de API sobre una base unificada

Vistas y funciones como contratos de API sobre una base unificada

En proyectos donde los microservicios comparten una misma base de datos, hay momentos en los que un cambio que comienza como una tarea rutinaria dentro de un equipo puede terminar en una reunión con t

El Arte de Filtrar Datos en HTTP: Entre la Elegancia de la URL y la Potencia del Payload

El Arte de Filtrar Datos en HTTP: Entre la Elegancia de la URL y la Potencia del Payload

El Arte de Filtrar Datos en HTTP: Entre la Elegancia de la URL y la Potencia del Payload Seguro que alguna vez empezaste con un endpoint de listado que parecía no necesitar demasiado. Un par de fil

Paginacion: Cuando offset ya no es suficiente

Paginacion: Cuando offset ya no es suficiente

Te llega la tarea y parece de las fáciles: "agregar paginación al listado de artículos". Añades ?page=0&size=20, Spring te proporciona Pageable, el repositorio hereda findAll(pageable) y, en poc

Convención de Nombres en Clases Java: Mejora de Legibilidad y Mantenibilidad

Convención de Nombres en Clases Java: Mejora de Legibilidad y Mantenibilidad

En proyectos de gran escala, identificar rápidamente el tipo de una clase facilita la comprensión del código, la colaboración y la depuración. Sin un esquema de nombres estandarizado, es común que los

Estructuración de Carpetas en Proyectos de Software

Estructuración de Carpetas en Proyectos de Software

En proyectos de software de gran escala, la falta de una estructura de carpetas bien definida puede generar desorden, dificultando la mantenibilidad, escalabilidad y comprensión del código. Muchas vec

Desarrollo de Software Implementando Gitflow

Desarrollo de Software Implementando Gitflow

Introducción El desarrollo de software requiere metodologías y flujos de trabajo que permitan un control eficiente del código fuente. Uno de los enfoques más utilizados para la gestión de versiones

Descubre el Poder del SemVer: Optimiza el Versionado de tu Software y Mantén un CHANGELOG Excepcional

Descubre el Poder del SemVer: Optimiza el Versionado de tu Software y Mantén un CHANGELOG Excepcional

El Versionado Semántico (SemVer) es una herramienta fundamental para comunicar de forma precisa los cambios en el software, facilitando el mantenimiento y la colaboración. Complementarlo con un **

Optimizando el Acceso a Datos: La Importancia de las Proyecciones JPA en Spring Boot

Optimizando el Acceso a Datos: La Importancia de las Proyecciones JPA en Spring Boot

El Costo Oculto de Traer Demasiada Información En el desarrollo de aplicaciones que interactúan con bases de datos, una tarea fundamental es la recuperación de datos. Al usar Object-Relational Map

Asegurando el Tejido Distribuido: Una Guía para la Seguridad en Arquitecturas de Microservicios

Asegurando el Tejido Distribuido: Una Guía para la Seguridad en Arquitecturas de Microservicios

El viaje hacia arquitecturas de microservicios ha transformado la forma en que construimos y desplegamos software. La agilidad, escalabilidad y resiliencia que ofrecen son innegables. Sin embargo, est

Implementando Trunk Based Development con Ramas de Vida Corta en Tu Equipo: Una Guía Completa

Implementando Trunk Based Development con Ramas de Vida Corta en Tu Equipo: Una Guía Completa

En el ámbito del desarrollo de software, la elección de una estrategia de versionamiento eficiente y estable es fundamental para el éxito de un equipo. El Trunk Based Development (TBD) tradicional, do

Estándar de Codificación y Gestión de Errores en Arquitecturas de Microservicios

Estándar de Codificación y Gestión de Errores en Arquitecturas de Microservicios

En el ecosistema de aplicaciones web modernas, la arquitectura de microservicios distribuidos se ha consolidado como un paradigma dominante. Su flexibilidad, escalabilidad y resiliencia son innegables

Manejando el Caos: Una Guía Definitiva sobre Excepciones de Dominio 🎯

Manejando el Caos: Una Guía Definitiva sobre Excepciones de Dominio 🎯

En el desarrollo de software moderno, escribir código que funciona perfectamente en escenarios ideales es solo el primer paso. La verdadera fortaleza de un sistema se manifiesta cuando debe enfrentar

El Arte de Conectar: Forjando un DataSource Dinámico en Spring Boot

El Arte de Conectar: Forjando un DataSource Dinámico en Spring Boot

En el vertiginoso universo del desarrollo de software, donde la seguridad es un pilar innegociable y la agilidad es la moneda de cambio, nos enfrentamos a desafíos que van más allá de la simple lógica

Más Allá del `try-catch`: Diseñando un Sistema de Excepciones Robusto y Escalable 🎯

Más Allá del `try-catch`: Diseñando un Sistema de Excepciones Robusto y Escalable 🎯

En el desarrollo de APIs, el manejo de errores suele ser un ciudadano de segunda clase. Con frecuencia, nos conformamos con respuestas de error genéricas, inconsistentes o, en el peor de los casos, co

Transacciones Declarativas en Arquitecturas Hexagonales con Spring WebFlux y AOP

Transacciones Declarativas en Arquitecturas Hexagonales con Spring WebFlux y AOP

En el desarrollo de aplicaciones reactivas modernas, especialmente bajo paradigmas como la Arquitectura Hexagonal, surgen desafíos que nos obligan a repensar cómo aplicamos conceptos transversales. Un

Respuestas API Consistentes: Un Wrapper Transversal con Spring WebFlux y `WebFilter`

Respuestas API Consistentes: Un Wrapper Transversal con Spring WebFlux y `WebFilter`

En entornos modernos de microservicios, la consistencia en las respuestas de una API es más que una cuestión de estética: es un factor crítico para la mantenibilidad, observabilidad y experiencia del

El tramo en cascada que todo proyecto ágil necesita (y casi ninguno tiene)

El tramo en cascada que todo proyecto ágil necesita (y casi ninguno tiene)

Llevas un tiempo trabajando en equipos que dicen practicar Scrum, y algo no termina de cuadrarte. Los sprints avanzan, la demo sale en tiempo, el tablero se vacía cada dos semanas. Y aun así, al cabo