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
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Ambassador Pattern para Acceso Resiliente a Servicios Remotos
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
FAQ
P: En que se diferencia de Proxy? R: Proxy controla acceso a un unico objeto. Ambassador maneja especificamente resiliencia de servicio remoto y usualmente se despliega como proceso local o libreria.
P: Puedo usar esto con gRPC? R: Si. Los interceptores de gRPC son una forma de ambassador pattern para agregar reintentos, deadlines y auth a llamadas de servicio.
¿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.
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. Recursos Relacionados
Circuit Breaker Pattern
Prevent cascading failures by stopping requests to failing services. An architectural pattern for resilient distributed systems.
PatternAbstract Factory for Cross-Platform UI Component Families
Create families of related objects without specifying concrete classes, enabling platform-specific implementations that share a common interface
RecipeLocal Microservices Development with Docker Compose
Orchestrate multi-service local environments with Docker Compose including databases, caches, message brokers, and reverse proxies with hot reload and shared networks
RecipeDeploy Applications to Kubernetes with Helm Charts
Package, version, and deploy Kubernetes applications using Helm charts with value overrides, template functions, and release management for reproducible infrastructure
RecipeLoad Balancing with HAProxy and Health Checks
Configure HAProxy as a high-performance load balancer with active health checks, sticky sessions, and SSL termination for resilient service distribution