🧪 Problem-Driven Systems Lab

🌐 Caso 09 β€” IntegraciΓ³n externa inestable#

Estado Stacks CategorΓ­a

[!IMPORTANT] πŸ“– Ver AnΓ‘lisis TΓ©cnico Senior de esta soluciΓ³n (PHP)

Este documento es un resumen ejecutivo. La evidencia de ingenierΓ­a, los algoritmos y la remediaciΓ³n profunda viven en el link de arriba y en comparison.md.


πŸ” QuΓ© problema representa#

Una API, servicio o proveedor externo introduce latencia, errores intermitentes o reglas cambiantes que afectan al sistema propio. El control interno termina donde empieza el tercero β€” y ahΓ­ es exactamente donde la resiliencia debe comenzar a aplicarse.

No controlamos al tercero. No controlamos su uptime, ni su versionado, ni sus rate limits, ni su ventana de mantenimiento. Lo que sΓ­ controlamos es cΓ³mo respondemos a su falla: con cache, con budget, con breaker, con schema mapping defensivo, con fallback degradado. El lab demuestra esa diferencia.


⚠️ Síntomas típicos#

  • Errores intermitentes difΓ­ciles de reproducir (anda, no anda, anda otra vez)
  • Respuestas lentas o con formatos cambiantes (drift de schema sin aviso)
  • Dependencia funcional alta del proveedor (si cae, caemos)
  • Necesidad de reprocesar manualmente cuando algo se pierde en el aire

🧩 Causas frecuentes#

  • Contratos dΓ©biles o mal versionados del proveedor
  • Rate limits no considerados en el diseΓ±o del cliente
  • Manejo insuficiente de errores y reintentos
  • Falta de almacenamiento intermedio o idempotencia

πŸ”¬ Estrategia de diagnΓ³stico#

  • Clasificar tipos de fallas y frecuencia (timeout vs 5xx vs schema drift vs rate limit)
  • Medir dependencia por flujo de negocio (cuΓ‘les dejan al negocio sin operar)
  • Revisar contratos, timeout y polΓ­ticas del proveedor (SLA real vs documentado)
  • DiseΓ±ar pruebas de resiliencia controladas (chaos engineering puntual)

πŸ’‘ Opciones de soluciΓ³n#

  • Adaptadores internos para desacoplar contrato externo (proxy de schema)
  • Manejo idempotente y colas cuando aplique
  • Circuit breaker, caching y fallback combinados
  • Versionado defensivo y validaciΓ³n de payloads (no asumir, verificar)

πŸ—ΊοΈ Diagrama β€” Adapter endurecido: budget β†’ cache β†’ breaker β†’ schema mapping#

text
                       request: /catalog-hardened?sku=widget-A
                                          β”‚
                                          β–Ό
                            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                            β”‚ providerBudget.tryAcquire()β”‚   ← Semaphore (max N/window)
                            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       permits == 0       β”‚      permit obtenido
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚              └─────────────────────────┐
                  β–Ό                                                                 β–Ό
        ╔════════════════════════╗                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β•‘ served_from: cache     β•‘                              β”‚ breaker.get() == "open"?       β”‚
        β•‘ reason: budget_exhaustedβ•‘                              β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
        β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•                                     β”‚ si               β”‚ no
                  β–²                                                     β–Ό                  β–Ό
                  β”‚                                       ╔════════════════════╗   call provider real
                  β”‚                                       β•‘ served_from: cache β•‘   (simulado en lab)
                  β”‚                                       β•‘ breaker:open       β•‘          β”‚
                  β”‚                                       β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•          β–Ό
                  β”‚                                                                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€  provider falla?β”‚
                  β”‚   β”‚                                                            β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚   β”‚                                                                 β”‚
                  β”‚   β”‚                                                       no        β”‚ si
                  β”‚   β”‚                                                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚   β”‚                                                β–Ό                              β–Ό
                  β”‚   β”‚                                       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚   β”‚                                       β”‚ snapshotCache   β”‚         β”‚ breaker.set("open")  β”‚
                  β”‚   β”‚                                       β”‚   .put(fresh)   β”‚         β”‚ AtomicReference CAS  β”‚
                  β”‚   β”‚                                       β”‚ breaker.set     β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚   β”‚                                       β”‚   ("closed")    β”‚                    β”‚
                  β”‚   β”‚                                       β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                    β”‚
                  β”‚   β”‚                                                β”‚                              β”‚
                  β”‚   β”‚                                                β–Ό                              β–Ό
                  β”‚   β”‚                                       ╔══════════════════╗      ╔════════════════════╗
                  β”‚   β”‚                                       β•‘ served_from:     β•‘      β•‘ served_from: cache β•‘
                  β”‚   β”‚                                       β•‘   provider       β•‘      β•‘ snapshot (stale)   β•‘
                  β”‚   β”‚                                       β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•      β•šβ•β•β•β•β•β•β•β•€β•β•β•β•β•β•β•β•β•β•β•β•β•
                  β”‚   β”‚                                                                          β”‚
                  β””β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  Legacy: cualquier fallo del provider = falla visible al cliente.
  Hardened: budget protege cuota; cache protege disponibilidad; breaker protege al provider.

πŸ—οΈ ImplementaciΓ³n actual#

βœ… PHP 8#

catalog-legacy pega directo al proveedor sin protecciones; catalog-hardened usa adapter + cache + breaker + budget. integration/state, sync-events y diagnostics/summary exponen budget restante, schema mappings y eventos de cuarentena. Ver php/README.md. Modo aislado: puerto 819.

Python 3.12#

Misma lΓ³gica con stdlib + dict como cache + threading.Lock para budget. Ver python/README.md. Modo aislado: puerto 839. Hub: http://localhost:8200/09/.

Node.js 22#

AbortSignal.timeout(ms) (Node 18+) marca deadline del llamado externo + circuit breaker en memoria con tres estados (closed/open/half_open) y reapertura automΓ‘tica tras cooldown. Ver node/README.md. Modo aislado: puerto 829. Hub: http://localhost:8300/09/.

Java 21#

Semaphore como budget de cuota (tryAcquire() no bloquea β€” si no hay permits, sirve snapshot). ConcurrentHashMap como snapshot cache thread-safe. AtomicReference<String> como breaker con CAS implΓ­cito. Ver java/README.md. Modo aislado: puerto 849. Hub: http://localhost:8400/09/.

.NET 8#

SemaphoreSlim.Wait(0) como budget de cuota (no bloquea β€” si no hay permits, sirve snapshot). ConcurrentDictionary<string,string> como snapshot cache thread-safe. Interlocked.CompareExchange sobre el estado del breaker (closed/open/half_open) con CAS explicito. Ver dotnet/README.md. Modo aislado: puerto 859. Hub: http://localhost:8500/09/.


βš–οΈ Trade-offs#

  • MΓ‘s desacoplamiento implica mΓ‘s componentes que mantener
  • Persistencia intermedia requiere limpieza y soporte (TTLs, vacuums)
  • Fallback puede afectar frescura de datos (snapshot viejo vs error visible)

πŸ’Ό Valor de negocio#

Mitiga dependencia de terceros y evita que un proveedor defina la estabilidad de tu producto. Ejemplos reales: el catΓ‘logo sigue navegable aunque el provider estΓ© caΓ­do; el checkout sigue cotizando con snapshot mientras la API externa se recupera; el partner externo no tira al sistema entero cuando aplica un rate limit nuevo.


πŸ› οΈ Stacks disponibles#

StackEstado
🐘 PHP 8OPERATIVO (adapter + cache + breaker persistente)
🐍 Python 3.12OPERATIVO (stdlib + threading.Lock + dict cache)
🟒 Node.js 22OPERATIVO (AbortSignal.timeout + CB en memoria)
β˜• Java 21OPERATIVO (Semaphore budget + snapshot cache + AtomicReference breaker)
πŸ”΅ .NET 8OPERATIVO (SemaphoreSlim budget + ConcurrentDictionary cache + Interlocked.CompareExchange breaker)

πŸš€ CΓ³mo levantar#

Modo hub (recomendado):

bash
docker compose -f compose.root.yml    up -d --build && curl http://localhost:8100/09/health   # PHP
docker compose -f compose.python.yml  up -d --build && curl http://localhost:8200/09/health   # Python
docker compose -f compose.nodejs.yml  up -d --build && curl http://localhost:8300/09/health   # Node
docker compose -f compose.java.yml    up -d --build && curl http://localhost:8400/09/health   # Java

Agotar budget y observar fallback a snapshot cache (ejemplo Java, budget=5):

bash
# 6 calls consecutivos: los primeros 5 al provider, el 6to es budget_exhausted β†’ cache
for i in 1 2 3 4 5 6 7; do
  curl -s "http://localhost:8400/09/catalog-hardened?sku=widget-A" | head -c 120; echo
done

# Estado: breaker, budget restante, tamaΓ±o del cache
curl http://localhost:8400/09/sync-events

πŸ“š Lectura recomendada#

DocumentoQuΓ© cubre
comparison.mdComparativa multi-stack con snippets de adapter por lenguaje
docs/postmortem.mdPostmortem del incidente que motivΓ³ el caso
docs/context.mdPor quΓ© la resiliencia empieza donde termina nuestro control
docs/symptoms.mdCΓ³mo se ve un proveedor inestable desde el log de prod
docs/root-causes.mdLas 4 causas de fragilidad externa mΓ‘s frecuentes
docs/solution-options.mdAdapter, cache, breaker, budget, schema versioning
docs/trade-offs.mdLo que cuesta sostener componentes extras
docs/business-value.mdContinuidad de operaciΓ³n frente a terceros

πŸ“ Estructura del caso#

09-unstable-external-integration/
β”œβ”€β”€ README.md                    ← este archivo
β”œβ”€β”€ comparison.md                ← comparativa multi-stack
β”œβ”€β”€ compose.compare.yml          ← los 7 stacks juntos
β”œβ”€β”€ docs/                        ← anΓ‘lisis + postmortem
β”œβ”€β”€ shared/                      ← assets compartidos
β”œβ”€β”€ 🐘 php/                      ← `OPERATIVO` β€” adapter + breaker persistente
β”œβ”€β”€ 🐍 python/                   ← `OPERATIVO` β€” stdlib + Lock + dict cache
β”œβ”€β”€ 🟒 node/                     ← `OPERATIVO` β€” AbortSignal.timeout + CB
β”œβ”€β”€ β˜• java/                     ← `OPERATIVO` β€” Semaphore + cache + AtomicReference
β”œβ”€β”€ πŸ”΅ dotnet/                   ← `OPERATIVO` β€” SemaphoreSlim + ConcurrentDictionary cache + Interlocked CAS breaker
β”œβ”€β”€ 🐹 go/                   ← `OPERATIVO` β€” chan struct{} como semaforo de cuota
└── πŸ¦€ rust/                   ← `OPERATIVO` β€” Mutex<i64>; el guard libera en todos los caminos

Ver esta carpeta en GitHub ↗