La arquitectura de microservicios prospera gracias a un conjunto de patrones que resuelven problemas transversales como el ruteo, el descubrimiento dinámico, la resiliencia y la observabilidad. Adoptarlos de forma integral permite que los servicios se comuniquen de manera segura, que sobrevivan a fallos parciales y que entreguen datos operativos para optimizar la plataforma. A continuación analizamos los patrones más difundidos y mostramos ejemplos prácticos en Java.
El API Gateway actúa como única puerta de entrada para los clientes externos. Gestiona el enrutamiento hacia los microservicios internos, aplica autenticación, limitación de tasa, agregación de respuestas y traducciones de protocolos. Soluciones populares incluyen Spring Cloud Gateway, Kong y Amazon API Gateway.
Un gateway bien diseñado centraliza las preocupaciones transversales sin convertirse en un cuello de botella. Debe escalar horizontalmente, exponer métricas de rendimiento y soportar estrategias de despliegue como blue-green.
El siguiente código define rutas y filtros personalizados. Cada petición recibe un encabezado de trazabilidad y se enruta mediante un nombre lógico que luego se resuelve por service discovery.
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder, CorrelationIdFilter filter) {
return builder.routes()
.route("orders", r -> r.path("/api/orders/**")
.filters(f -> f.filter(filter).retry(config -> config
.setRetries(2)
.setStatuses(HttpStatus.SERVICE_UNAVAILABLE)))
.uri("lb://orders-service"))
.route("payments", r -> r.path("/api/payments/**")
.filters(f -> f.filter(filter))
.uri("lb://payments-service"))
.build();
}
}
El filtro de correlación agrega un identificador compartido por toda la llamada para facilitar la trazabilidad distribuida.
El descubrimiento de servicios permite que las instancias se registren y que los consumidores obtengan la dirección actualizada mediante una registry. En entornos dinámicos donde los contenedores escalan automáticamente, esta funcionalidad es imprescindible. Herramientas como Eureka, Consul y Nomad proveen APIs REST y mecanismos de health checking.
El discovery puede ser del lado del cliente, delegando en la librería la resolución de instancias, o del lado del servidor, donde el balanceador recibe la petición y elige el destino. Ambos enfoques requieren mantener la información actualizada y eliminar instancias inestables mediante heartbeats.
Este ejemplo muestra cómo un microservicio se registra y obtiene la lista de instancias disponibles para invocar otro servicio.
@EnableEurekaClient
@SpringBootApplication
public class InventoryServiceApplication {
public static void main(String[] args) {
SpringApplication.run(InventoryServiceApplication.class, args);
}
@Bean
public RestTemplate restTemplate(DiscoveryClient discoveryClient) {
return new RestTemplateBuilder()
.additionalInterceptors((request, body, execution) -> {
request.getHeaders().add("X-Correlation-Id", MDC.get("traceId"));
return execution.execute(request, body);
})
.build();
}
public URI resolveOrdersService(DiscoveryClient discoveryClient) {
ServiceInstance instance = discoveryClient.getInstances("orders-service")
.stream()
.findAny()
.orElseThrow(() -> new IllegalStateException("Servicio de pedidos no disponible"));
return instance.getUri();
}
}
En entornos de contenedores suele delegarse el discovery al plano de control de Kubernetes, que expone servicios virtuales y balanceo de carga interno.
El circuit breaker evita que un servicio saturado provoque fallas en cascada. Supervisa las llamadas y, si detecta errores recurrentes o tiempos de espera prolongados, abre el circuito para evitar nuevas invocaciones por un período. Tras una ventana de prueba, el circuito pasa a estado medio abierto y permite un número limitado de llamadas. Bibliotecas como Resilience4j y sistemas externos como Envoy ofrecen esta capacidad.
El siguiente servicio protege la llamada a envíos con un circuit breaker y un limitador de tasa. Se define un fallback para mantener la respuesta aunque el servicio remoto esté degradado.
@Service
public class ShippingGateway {
private final WebClient client;
public ShippingGateway(WebClient.Builder builder) {
this.client = builder.baseUrl("http://shipping-service").build();
}
@CircuitBreaker(name = "shipping", fallbackMethod = "fallbackRate")
@RateLimiter(name = "shipping")
public Mono<ShippingQuote> getQuote(String orderId) {
return client.get()
.uri("/api/shipping/{id}", orderId)
.retrieve()
.bodyToMono(ShippingQuote.class);
}
public Mono<ShippingQuote> fallbackRate(String orderId, Throwable throwable) {
return Mono.just(new ShippingQuote(orderId, BigDecimal.valueOf(15)));
}
}
Es fundamental monitorear el estado del circuito y ajustar los umbrales según los acuerdos de nivel de servicio.
La configuración centralizada permite externalizar propiedades sensibles y valores dependientes del entorno. Los servicios obtienen su configuración al iniciarse y pueden refrescarla en caliente cuando cambian. Soluciones comunes incluyen Spring Cloud Config, Azure App Configuration y AWS AppConfig.
El Config Server suele versionar la configuración en un repositorio Git. Esto facilita revisar cambios, auditar accesos y aplicar despliegues controlados. También se integran gestores de secretos para proteger credenciales y certificados.
El siguiente componente recupera un mensaje de bienvenida desde el Config Server y se actualiza mediante el bus de eventos.
@RefreshScope
@RestController
public class GreetingController {
@Value("${greetings.message}")
private String message;
@GetMapping("/greetings")
public String getGreeting() {
return message;
}
}
Cuando se publica un cambio en el repositorio central, el Config Server notifica al bus y las instancias refrescan sus propiedades sin reiniciar.
El patrón sidecar añade un proceso auxiliar junto al microservicio principal en el mismo pod o host. Ese contenedor complementario brinda capacidades como proxying, configuración dinámica, métricas o seguridad sin modificar el código. Tecnologías como Istio utilizan Envoy como sidecar para implementar un servicio mesh.
El sidecar actúa como gestor de responsabilidades transversales, estandariza las políticas entre servicios y facilita la evolución independiente. Es importante monitorear el consumo de recursos del sidecar para evitar que compita con la aplicación principal.
Aunque la configuración se aplica con manifiestos, es útil visualizar cómo un sidecar puede dirigir el tráfico a diferentes versiones de un servicio para realizar un canary release.
// Ejemplo conceptual: el sidecar Envoy realiza el ruteo
public class CanaryRouting {
public HttpResponse route(HttpRequest request, TrafficPolicy policy) {
if (policy.isCanaryEnabled() && policy.matches(request)) {
return send(request, policy.getCanaryEndpoint());
}
return send(request, policy.getPrimaryEndpoint());
}
private HttpResponse send(HttpRequest request, URI endpoint) {
// Envía la petición al destino seleccionado y agrega telemetría
return httpClient.send(request.withHeader("x-envoy-attempt-count", "1"), endpoint);
}
}
En la práctica, Envoy ejecuta esta lógica declarada en una configuración que el plano de control actualiza de forma dinámica.
La observabilidad es el cimiento que permite entender el comportamiento de un sistema distribuido. Combina registros estructurados, métricas agregadas y trazas distribuidas para diagnosticar incidentes y analizar tendencias. Herramientas como OpenTelemetry, Prometheus y Grafana son de uso extendido.
Los microservicios deben emitir registros en formato estructurado (JSON), incluir el identificador de correlación y exponer métricas con etiquetas que permitan segmentar por servicio, instancia y versión. Las trazas distribuidas correlacionan las llamadas entre servicios y permiten visualizar los tiempos de cada segmento.
El siguiente fragmento ejemplifica cómo generar métricas, registros y trazas dentro de un servicio.
@Service
public class RecommendationService {
private final Counter recommendationsCounter;
private final Tracer tracer;
private static final Logger LOGGER = LoggerFactory.getLogger(RecommendationService.class);
public RecommendationService(MeterRegistry registry, Tracer tracer) {
this.recommendationsCounter = Counter.builder("recommendations.generated")
.description("Total de recomendaciones generadas")
.register(registry);
this.tracer = tracer;
}
public List<String> recommend(String userId) {
Span span = tracer.nextSpan().name("recommendation_flow").start();
try (Tracer.SpanInScope scope = tracer.withSpan(span)) {
recommendationsCounter.increment();
LOGGER.info("Generando recomendación para {}", userId);
return algorithm(userId);
} finally {
span.end();
}
}
private List<String> algorithm(String userId) {
return List.of("curso-microservicios", "libro-arquitectura");
}
}
Las métricas se exponen en un endpoint /actuator/prometheus, los registros se envían a un agregador central y las trazas se exportan según la configuración de OpenTelemetry.