π Caso 09 β IntegraciΓ³n externa inestable#
[!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#
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#
| Stack | Estado |
|---|---|
| π PHP 8 | OPERATIVO (adapter + cache + breaker persistente) |
| π Python 3.12 | OPERATIVO (stdlib + threading.Lock + dict cache) |
| π’ Node.js 22 | OPERATIVO (AbortSignal.timeout + CB en memoria) |
| β Java 21 | OPERATIVO (Semaphore budget + snapshot cache + AtomicReference breaker) |
| π΅ .NET 8 | OPERATIVO (SemaphoreSlim budget + ConcurrentDictionary cache + Interlocked.CompareExchange breaker) |
π CΓ³mo levantar#
Modo hub (recomendado):
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 # JavaAgotar budget y observar fallback a snapshot cache (ejemplo Java, budget=5):
# 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#
| Documento | QuΓ© cubre |
|---|---|
comparison.md | Comparativa multi-stack con snippets de adapter por lenguaje |
docs/postmortem.md | Postmortem del incidente que motivΓ³ el caso |
docs/context.md | Por quΓ© la resiliencia empieza donde termina nuestro control |
docs/symptoms.md | CΓ³mo se ve un proveedor inestable desde el log de prod |
docs/root-causes.md | Las 4 causas de fragilidad externa mΓ‘s frecuentes |
docs/solution-options.md | Adapter, cache, breaker, budget, schema versioning |
docs/trade-offs.md | Lo que cuesta sostener componentes extras |
docs/business-value.md | Continuidad 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