Microservicios con Java y Spring Boot: Arquitectura Empresarial en la Nube
Lo Que Dominarás en Este Tutorial
- Leverage Java 21 Virtual Threads to handle high-throughput I/O with classic synchronous code.
1. High Concurrency with Java 21 Virtual Threads
Virtual Threads enable millions of lightweight concurrent tasks managed by the JVM.
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
@PostMapping("/process")
public ResponseEntity<String> processPayment() {
return ResponseEntity.ok("Processed via Virtual Thread");
}
}
Evaluación Rápida: Pon a Prueba tus Conocimientos
Preguntas Frecuentes
2. Paso a paso: llama a un servicio descendente con RestClient
Los hilos virtuales hacen que la concurrencia sea barata, pero no vuelven seguras las llamadas a otros servicios. La primera habilidad real en microservicios es justamente esa llamada saliente, y la herramienta para hacerla en Spring es RestClient, el cliente síncrono moderno incluido en Spring Framework 6. En esta sección vas a construir un cliente de inventario con tiempos de espera explícitos, lo vas a exponer desde un controlador y vas a mantener el estilo bloqueante que los hilos virtuales aprovechan tan bien.
Paso 1: activa los hilos virtuales y ponle límites al cliente
En las preguntas frecuentes ya viste el interruptor: spring.threads.virtual.enabled=true en Spring Boot 3.2 o superior. La disciplina empieza en el cliente: un RestClient construido sobre el HttpClient del JDK, con un tiempo de conexión corto y un tiempo máximo de lectura estricto, para que un servicio descendente con problemas falle en segundos en lugar de acumular miles de hilos virtuales estacionados.
@Service
public class InventoryClient {
private final RestClient restClient;
public InventoryClient(RestClient.Builder builder) {
JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory();
factory.setReadTimeout(Duration.ofSeconds(5));
this.restClient = builder
.baseUrl("http://inventory-service")
.requestFactory(factory)
.build();
}
public StockLevel getStock(String sku) {
return restClient.get()
.uri("/api/stock/{sku}", sku)
.retrieve()
.body(StockLevel.class);
}
}
record StockLevel(String sku, int available) {}
Paso 2: expón el cliente a través de un controlador
El controlador queda tan simple como el PaymentController de la sección 1: un método síncrono que se ejecuta en un hilo virtual. Toda la honestidad propia de los sistemas distribuidos vive en el cliente: tiempos de espera, un record tipado para la respuesta y excepciones que se propagan en vez de perderse convertidas en un null.
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final InventoryClient inventory;
public OrderController(InventoryClient inventory) {
this.inventory = inventory;
}
@GetMapping("/stock/{sku}")
public ResponseEntity<StockLevel> stock(@PathVariable String sku) {
return ResponseEntity.ok(inventory.getStock(sku));
}
}
3. Un ejemplo completo
Ahora armemos el flujo completo: un POST a /api/orders primero consulta el stock y después cobra a través del servicio de pagos de la sección 1. Lo más interesante es el camino de error. Cuando el servicio de inventario supera su tiempo de espera, quien llama debería recibir un 502 limpio con un cuerpo ProblemDetail —Spring 6 admite RFC 9457 de forma nativa—, no un stack trace ni una petición que queda colgada:
public class DownstreamUnavailableException extends RuntimeException {
public DownstreamUnavailableException(String service, Throwable cause) {
super(service + " is unavailable", cause);
}
}
@RestControllerAdvice
class ApiErrorHandler {
@ExceptionHandler(DownstreamUnavailableException.class)
ProblemDetail handleDownstream(DownstreamUnavailableException ex) {
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_GATEWAY);
problem.setTitle("Downstream service unavailable");
problem.setDetail(ex.getMessage());
return problem;
}
@ExceptionHandler(ResourceAccessException.class)
ProblemDetail handleTimeout(ResourceAccessException ex) {
return ProblemDetail.forStatusAndDetail(
HttpStatus.GATEWAY_TIMEOUT, "A downstream call timed out");
}
}
Envuelve la llamada del cliente en un try/catch que traduzca la ResourceAccessException de RestClient a DownstreamUnavailableException y el ejemplo queda completo: los hilos virtuales absorben la concurrencia, los tiempos de espera acotan cada llamada y los errores cruzan la frontera entre servicios como un contrato documentado. Esas tres costumbres son las que separan una demo de un microservicio listo para producción.
Paso 3: haz visible la dependencia para Actuator
Un servicio que llama a otros le debe a su equipo de operaciones un endpoint de salud honesto. Spring Boot Actuator convierte cualquier bean que implemente HealthIndicator en parte de /actuator/health, así que conecta ahí el cliente de inventario: de ese modo, los balanceadores y los paneles verán la dependencia tal como la experimentan tus usuarios:
@Component
public class InventoryHealthIndicator implements HealthIndicator {
private final InventoryClient inventory;
public InventoryHealthIndicator(InventoryClient inventory) {
this.inventory = inventory;
}
@Override
public Health health() {
try {
inventory.getStock("PING");
return Health.up()
.withDetail("inventory-service", "reachable")
.build();
} catch (Exception ex) {
return Health.down(ex)
.withDetail("inventory-service", "unreachable")
.build();
}
}
}
Mantén esa comprobación barata y fácil de cachear: se ejecuta en cada sondeo de salud, así que apúntala a la lectura más ligera que ofrezca el servicio descendente. Y decide a conciencia si una dependencia caída debería reportar DOWN —sacando esta instancia de la rotación— o mantenerse en UP con un detalle de degradación. Para una dependencia dura como pagos, la respuesta sincera es DOWN.
Completa la configuración en application.yml
Dos bloques de configuración hacen real el código anterior. El primero es el interruptor de hilos virtuales de la sección 1, escrito donde corresponde en lugar de pasarlo por línea de comandos; el segundo expone el endpoint de salud —y solo los endpoints seguros— para que el InventoryHealthIndicator sea accesible en /actuator/health sin abrir toda la superficie de Actuator:
spring:
application:
name: order-service
threads:
virtual:
enabled: true # Spring Boot 3.2+ on Java 21
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: when-authorized
Con show-details en when-authorized, quien llama sin credenciales solo ve UP o DOWN, mientras que tu equipo obtiene el detalle por dependencia que entrega el indicador. Esa es la diferencia entre un endpoint de salud y una fuga de información.
4. Solución de errores
- El rendimiento no mejoró después de activar los hilos virtuales. Busca el fenómeno conocido como pinning: un hilo virtual que se bloquea dentro de un bloque synchronized o de una llamada nativa fija su hilo portador. Ejecuta las pruebas de carga con -Djdk.tracePinnedThreads=full, localiza las pilas fijadas y reemplaza las rutas calientes con synchronized por ReentrantLock; es exactamente la trampa de la que advierte la sección en profundidad de este tutorial.
- Las peticiones se quedan colgadas durante minutos cuando un servicio descendente está caído. No hay tiempos de espera configurados. Los valores por defecto de la factoría del JDK esperan mucho más de lo que tus usuarios van a esperar. Define un tiempo máximo de lectura en la factoría de peticiones (paso 1) y un tiempo de conexión en el HttpClient subyacente; cada llamada saliente del sistema necesita ambos.
- El servicio se cae con mucha concurrencia a pesar de que los hilos son baratos. Los hilos virtuales eliminaron ese cuello de botella, así que el problema se movió a otro lado: casi siempre al pool de conexiones a la base de datos. Diez mil hilos virtuales con una conexión cada uno son un problema de cuentas, no de modelo de hilos. Dimensiona el pool pensando en la base de datos y acota la dispersión con un Semaphore frente a los recursos escasos.
- Respuestas 406 o cuerpos vacíos al llamar a otro servicio Spring. El record de respuesta no coincide con el JSON: un campo renombrado, un objeto anidado que viene aplanado o, en configuraciones antiguas, la falta de un constructor por defecto para Jackson. Registra el cuerpo crudo una vez —usa exchange() en lugar de retrieve() mientras depuras—, alinea el record y protege el contrato con una prueba.
Profundizando: los microservicios con Spring Boot en la práctica
Los hilos virtuales cambian la economía de un servicio Spring Boot: en Java 21, una aplicación con Spring Boot 3 puede sostener un volumen muy alto de solicitudes concurrentes con mucha espera de E/S manteniendo el código síncrono y directo de los controladores que escribe este tutorial. Pero la preparación para producción vive en todo lo que rodea al endpoint: saber en qué cargas ayudan de verdad los hilos virtuales, a dónde se mueven los cuellos de botella reales cuando los hilos salen baratos y cómo se comporta cada servicio cuando una dependencia descendente se vuelve lenta. Los equipos que dominan esta pila tratan la concurrencia como una propiedad de todo el sistema, no como un ajuste del pool de hilos.
Errores comunes
- Bloquear hilos virtuales con bloques synchronized. Cuando un hilo virtual se bloquea dentro de una sección synchronized puede fijar (pin) su hilo portador, borrando en silencio la escalabilidad por la que adoptaste Java 21. En las rutas calientes conviene preferir ReentrantLock y revisar si tus librerías hacen llamadas de red sincronizadas.
- Dividir en microservicios antes de que el monolito duela. Cada frontera de servicio suma transacciones distribuidas, versionado de APIs y sobrecarga de observabilidad. Muchos equipos pagan ese impuesto desde el primer día por un problema de escala que todavía no tienen.
- Llamar a servicios descendentes sin tiempos de espera. Los hilos baratos tientan a abrir llamadas sin límite. Una sola dependencia lenta, sin timeout ni contención, seguirá agotando tu servicio: solo que agotará conexiones y memoria en vez de un pool de hilos.
- Ajustar pools de hilos gigantes por costumbre. Con hilos virtuales, la concurrencia se limita en el recurso, no en el pool: el número de conexiones a la base de datos y los límites de tasa de los servicios descendentes pasan a ser las perillas, idealmente protegidas con semáforos o compartimentos por dependencia.
Preguntas frecuentes
¿Debo elegir hilos virtuales o WebFlux?
Si tu base de código es el Spring MVC síncrono clásico, los hilos virtuales entregan un rendimiento parecido al no bloqueante sin reescribir todo como una cadena reactiva. WebFlux sigue ganándose su lugar en streaming y pilas totalmente reactivas, pero la mayoría de los servicios de CRUD e integración son más simples de operar —y de depurar— con hilos virtuales.
¿Los hilos virtuales aceleran el trabajo intensivo en CPU?
No. Los hilos virtuales brillan cuando las tareas pasan el tiempo esperando E/S: llamadas a la base de datos, solicitudes HTTP, acceso a archivos. El cálculo intensivo en CPU sigue limitado por la cantidad de núcleos físicos, por livianos que sean los hilos.
¿Qué versiones necesito para seguir este enfoque?
El tutorial se construye sobre Java 21 y Spring Boot 3, la combinación que entrega los hilos virtuales como función terminada y con soporte de primera clase en Spring. Las versiones anteriores de Java solo los tenían en forma preliminar.