Ambassador Pattern para Acceso Resiliente a Servicios
Agrega un ambassador local que maneja reintentos, circuit breaking y monitoreo al llamar servicios remotos, manteniendo el cliente simple y la logica de servicio pura
El Ambassador pattern crea una instancia helper local que actua en nombre de un servicio remoto. Maneja preocupaciones de red como reintentos, timeouts, circuit breaking y logging, manteniendo el codigo del cliente limpio y la interfaz del servicio remoto simple. Este pattern es comun en microservicios y despliegues containerizados.
Cuando Usar Esto
- Un cliente llama un servicio remoto y necesita reintentos, cacheo o monitoreo
- Quieres mantener la interfaz de servicio simple sin cross-cutting concerns
- Restricciones de lenguaje o framework previenen usar un sidecar proxy
Problema
Cada servicio que llama una API remota duplica logica de reintentos, manejo de timeouts y recoleccion de metricas. Esto hincha los clientes y hace las politicas de resiliencia inconsistentes.
Solucion
// ambassador/ServiceClient.ts
interface UserService {
getUser(id: string): Promise<{ id: string; name: string }>;
}
// Implementacion de servicio remoto
class RemoteUserService implements UserService {
async getUser(id: string): Promise<{ id: string; name: string }> {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
}
// Ambassador con logica de resiliencia
class UserServiceAmbassador implements UserService {
private circuitOpen = false;
private failureCount = 0;
private readonly failureThreshold = 5;
private readonly retryCount = 3;
private readonly timeoutMs = 2000;
constructor(private remote: UserService) {}
async getUser(id: string): Promise<{ id: string; name: string }> {
if (this.circuitOpen) {
throw new Error('Circuit breaker is open');
}
for (let attempt = 1; attempt <= this.retryCount; attempt++) {
try {
const result = await this.callWithTimeout(id);
this.onSuccess();
return result;
} catch (error) {
console.log(`Attempt ${attempt} failed:`, error);
if (attempt === this.retryCount) {
this.onFailure();
throw error;
}
await this.delay(1000 * attempt); // Exponential backoff
}
}
throw new Error('Unreachable');
}
private async callWithTimeout(id: string): Promise<{ id: string; name: string }> {
return Promise.race([
this.remote.getUser(id),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), this.timeoutMs)
),
]);
}
private onSuccess(): void {
this.failureCount = 0;
}
private onFailure(): void {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.circuitOpen = true;
setTimeout(() => {
this.circuitOpen = false;
this.failureCount = 0;
}, 30000);
}
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Cliente usa el ambassador transparentemente
class OrderService {
constructor(private users: UserService) {}
async getOrderWithUser(orderId: string): Promise<unknown> {
const order = { id: orderId, userId: 'user-123' };
const user = await this.users.getUser(order.userId);
return { ...order, user };
}
}
// Uso
const remote = new RemoteUserService();
const ambassador = new UserServiceAmbassador(remote);
const orders = new OrderService(ambassador);
Variacion: Ambassador de Monitoreo
// ambassador/Monitoring.ts
class MonitoringAmbassador implements UserService {
private requestCount = 0;
private errorCount = 0;
private totalLatency = 0;
constructor(private remote: UserService) {}
async getUser(id: string): Promise<{ id: string; name: string }> {
const start = Date.now();
this.requestCount++;
try {
const result = await this.remote.getUser(id);
this.totalLatency += Date.now() - start;
return result;
} catch (error) {
this.errorCount++;
throw error;
}
}
getMetrics(): { requests: number; errors: number; avgLatency: number } {
return {
requests: this.requestCount,
errors: this.errorCount,
avgLatency: this.requestCount > 0 ? this.totalLatency / this.requestCount : 0,
};
}
}
Como Funciona
- Remote Service provee la logica de negocio core
- Ambassador envuelve el servicio remoto con resiliencia y observabilidad
- Client llama al ambassador como si fuera el servicio real
- Policies (reintentos, circuit breaking) se centralizan en el ambassador
Consideraciones de Produccion
- Combina con un service mesh (Istio, Linkerd) para enforcement de politicas a nivel de cluster
- Usa connection pooling en el ambassador para reducir overhead de TCP
- Manten el ambassador stateless para que pueda recrearse en fallo
Errores Comunes
- Poner logica de negocio en el ambassador en lugar de logica de resiliencia
- No distinguir entre errores reintentables y no reintentables
- Fallar en propagar senales de cancelacion a traves del ambassador
Troubleshooting
- Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
- Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
- Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
- Tests break when the design changes: favor stable contracts over internal structure.
- Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.
Temas Avanzados
Escenario: Ambassador para Servicio Legacy
Sistema: Microservicio moderno necesita llamar a servicio legacy SOAP
Patron: Ambassador como intermediario
Arquitectura:
Modern Service -> Ambassador -> Legacy SOAP Service
Ambassador responsabilidades:
1. Traducir REST/JSON a SOAP/XML
2. Retries con backoff exponencial
3. Circuit breaker
4. Metricas y logging
5. Rate limiting
6. Caching de respuestas
```typescript
// Ambassador: envuelve el servicio legacy
class LegacyAmbassador {
private circuitBreaker: CircuitBreaker;
private cache = new Map<string, { data: unknown; expiry: number }>();
private retryConfig = { maxRetries: 3, backoffMs: 1000 };
constructor(private legacyEndpoint: string) {
this.circuitBreaker = new CircuitBreaker({
failureThreshold: 5,
resetTimeoutMs: 30000,
});
}
async callLegacy(method: string, params: unknown): Promise<unknown> {
// 1. Circuit breaker
if (!this.circuitBreaker.canExecute()) {
throw new Error("Circuit open: legacy service unavailable");
}
// 2. Cache check
const cacheKey = `${method}:${JSON.stringify(params)}`;
const cached = this.cache.get(cacheKey);
if (cached && cached.expiry > Date.now()) {
return cached.data;
}
// 3. Retry con backoff
for (let attempt = 0; attempt < this.retryConfig.maxRetries; attempt++) {
try {
const result = await this.callSOAP(method, params);
this.circuitBreaker.recordSuccess();
this.cache.set(cacheKey, { data: result, expiry: Date.now() + 60000 });
return result;
} catch (err) {
this.circuitBreaker.recordFailure();
if (attempt < this.retryConfig.maxRetries - 1) {
await new Promise(r => setTimeout(r, this.retryConfig.backoffMs * Math.pow(2, attempt)));
}
}
}
throw new Error("Legacy service failed after retries");
}
private async callSOAP(method: string, params: unknown): Promise<unknown> {
// Traducir JSON a XML SOAP envelope
const soapEnvelope = this.jsonToSOAP(method, params);
const response = await fetch(this.legacyEndpoint, {
method: "POST",
headers: { "Content-Type": "text/xml" },
body: soapEnvelope,
});
const xml = await response.text();
return this.soapToJSON(xml);
}
private jsonToSOAP(method: string, params: unknown): string {
return `<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<${method}>${JSON.stringify(params)}</${method}>
</soap:Body>
</soap:Envelope>`;
}
private soapToJSON(xml: string): unknown {
// Parse XML response to JSON
return JSON.parse(xml.match(/<return>(.*)<\/return>/s)?.[1] || "{}");
}
}
Lecciones:
- Ambassador aísla la complejidad del servicio legacy
- El servicio moderno no sabe que habla con SOAP
- Circuit breaker protege contra fallos en cascada
- Cache reduce llamadas al servicio legacy
- Metricas del ambassador son visibles para monitoreo
### Ambassador vs Sidecar: cual uso?
Usa Ambassador cuando necesitas un intermediario que envuelve un servicio externo (legacy, third-party). El ambassador vive en el cliente y traduce/protige las llamadas. Usa Sidecar cuando necesitas funcionalidad complementaria que vive junto al servicio (logging, monitoring, proxy). Ambassador es cliente-side, Sidecar es server-side. Amb pueden ser containers en K8s.
## Puntos Clave
- **Aplica ambassador pattern para acceso resiliente a servicios** cuando necesites una solución práctica para tu caso de uso.
- **Monitorea el rendimiento** después de implementar; mide latencia, errores y uso de recursos antes y después.
- **Revisa la sección de Troubleshooting** ante errores comunes; la mayoría tienen causa raíz documentada con solución.
- **Mantén dependencias actualizadas** y ejecuta tests en CI para prevenir regresiones en producción.
## Errores Comunes en Producción
- Aplicar el patrón donde no se necesita abstracción, agregando complejidad accidental.
- Dejar que el patrón se filtre en módulos no relacionados y confundir los límites de responsabilidad.
- Sobre-ingeniería en la primera implementación en lugar de comenzar simple y medir el dolor.
- Saltar los tests de contrato, de modo que las refactorizaciones rompan consumidores en silencio.
- Ignorar modos de fallo que el patrón no cubre.
- Usar el patrón como opción por defecto en lugar de elegir la herramienta adecuada para la escala actual.
- Olvidar documentar cuándo dejar de usar el patrón y qué lo reemplaza.
- Carecer de observabilidad sobre rendimiento y propagación de errores del patrón. Preguntas frecuentes
¿Es este patrón adecuado para proyectos pequeños?
Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.
¿Cómo se compara este patrón con alternativas?
Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.
¿Puedo aplicar este patrón parcialmente?
Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.
Recursos Relacionados
Patrón Circuit Breaker
Previene fallos en cascada deteniendo solicitudes a servicios que están fallando. Un patrón arquitectural para sistemas distribuidos resilientes.
PatternAbstract Factory para Familias de Componentes UI
Crea familias de objetos relacionados sin especificar clases concretas, habilitando implementaciones especificas de plataforma que comparten una interfaz comun
RecipeDesarrollo Local de Microservicios con Docker Compose
Orquesta entornos locales multi-servicio con Docker Compose incluyendo bases de datos, caches, message brokers y reverse proxies con hot reload y redes compartidas
RecipeDespliegue de Aplicaciones en Kubernetes con Helm Charts
Empaqueta, versiona y despliega aplicaciones Kubernetes usando Helm charts con value overrides, funciones de template y release management para infraestructura reproducible
RecipeLoad Balancing con HAProxy y Health Checks
Configura HAProxy como load balancer de alto rendimiento con health checks activos, sticky sessions y SSL termination para distribucion resiliente de servicios