- Agile 2
- Alta disponibilidad 1
- Alternativas cloud 1
- Aop 1
- Arquitectura 3
- Arquitectura distribuida 2
- Automatizacion 3
- Azure devops 1
- Base de datos 1
- Buenas practicas 19
- Cloud 1
- Colas 7
- Competing consumers 1
- Convenciones 11
- Copilot 1
- Diseno 6
- Docker 2
- Docker compose 1
- Documentacion 1
- Eda 11
- Equipos 1
- Escalabilidad 1
- Flujo de negocio 1
- Flujo de trabajo 3
- Flyway 1
- Git 4
- Gradle 3
- Herramientas digitales 1
- Ia 1
- Iam 1
- Infraestructura 2
- Java 14
- Jerarquia tecnica 1
- Jpa 1
- Jsonb 1
- Kafka 7
- Kubernetes 1
- Liderazgo en software 1
- Lineamientos 1
- Log 1
- Logging 3
- Microservicios 3
- Mongodb 1
- Monitoreo 1
- Nosql 3
- Observabilidad 4
- Open source 1
- Plugins 3
- Postgresql 1
- Privacidad 1
- Programacion funcional 1
- Programacion reactiva 4
- Rabbitmq 6
- Rotacion de talento 1
- Saga 2
- Scrum 2
- Security 1
- Seguridad 1
- Self hosting 1
- Sistemas legados 1
- Spring boot 3
- Spring mvc 2
- Sql 3
- Streams 1
- Threadlocal 1
- Trazabilidad 2
- Versionado 2
- Web 1
- Webflux 2
- Websockets 1
- Zero trust 1
Spring mvc
2 artículos
El Arte de la Consistencia: Creando Respuestas API Predecibles en Spring MVC
- Mauricio ECR
- Snippets
- 24 Sep, 2025
El Arte de la Consistencia: Creando Respuestas API Predecibles en Spring MVC Imagina a un desarrollador frontend consumiendo tu API. En un endpoint, recibe un objeto JSON. En otro, una simple lista. S
El Arte de la Consistencia: Creando Respuestas API Predecibles en Spring MVC
- Mauricio ECR
- Snippets
- 24 Sep, 2025
El Arte de la Consistencia: Creando Respuestas API Predecibles en Spring MVC Imagina a un desarrollador frontend consumiendo tu API. En un endpoint, recibe un objeto JSON. En otro, una simple lista. Si ocurre un error de validación, obtiene una estructura compleja; si el servidor falla, recibe un texto plano. Cada variación, por pequeña que sea, introduce una nueva lógica condicional en el cliente. Rápidamente, esa falta de estándar se convierte en un caos silencioso, una deuda técnica que frena la innovación y fragiliza el sistema.
La estandarización de las respuestas de una API no es una cuestión de estética, sino una decisión de arquitectura fundamental. El verdadero desafío es cómo lograr esta uniformidad sin contaminar nuestra lógica de negocio con código repetitivo. Afortunadamente, Spring MVC nos ofrece herramientas de una elegancia sorprendente, @RestControllerAdvice y ResponseBodyAdvice, diseñadas precisamente para resolver estos problemas transversales de forma limpia y centralizada.
Este artículo te guiará en la implementación de un patrón de respuesta robusto y unificado en un entorno Spring Boot con Lombok, cubriendo tanto los casos de éxito como los de error de manera consistente y profesional.
El Contrato: La Piedra Angular de la Previsibilidad
Antes de escribir una sola línea de lógica, debemos definir nuestro objetivo: un formato de respuesta único que sirva tanto para éxitos como para errores. Esta es la base de la predictibilidad. En lugar de improvisar, diseñaremos una estructura genérica que actúe como un contrato inmutable con nuestros clientes.
La clave de nuestra estrategia es la clase ApiResponse. Este DTO (Data Transfer Object) genérico contendrá tres componentes principales:
meta: Un objeto con metadatos de la solicitud (timestamp, ID de la petición, etc.), útil para la depuración y el monitoreo.data: El payload real de la respuesta en caso de éxito. Será de tipo genérico (T).errors: Una lista de errores detallados si algo sale mal.
Una de las decisiones más importantes aquí es el uso de la anotación @JsonInclude(JsonInclude.Include.NON_NULL). Esta simple línea le indica a Jackson (el serializador JSON de Spring) que omita cualquier campo con valor nulo. ¿El resultado? Las respuestas exitosas no tendrán el campo errors y las de error no tendrán el campo data, manteniendo así los JSON limpios y relevantes sin necesidad de crear múltiples clases.
Para construir este contrato, asegúrate de tener las dependencias esenciales en tu pom.xml:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
Y aquí está el diseño de nuestro contrato unificado:
// src/main/java/com/example/demo/common/ApiResponse.java
package com.example.demo.common;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Data;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponse<T> {
private Meta meta;
private T data;
private List<ErrorDetail> errors;
@Data
@Builder
public static class Meta {
private String timestamp = Instant.now().toString();
@Builder.Default
private String requestId = UUID.randomUUID().toString().substring(0, 10);
private String path;
private int status;
}
@Data
@Builder
public static class ErrorDetail {
private String code;
private String message;
}
public static <T> ApiResponse<T> success(T data, String path, int status) {
return ApiResponse.<T>builder()
.meta(Meta.builder().path(path).status(status).build())
.data(data)
.build();
}
public static ApiResponse<?> error(List<ErrorDetail> errors, String path, int status) {
return ApiResponse.builder()
.meta(Meta.builder().path(path).status(status).build())
.errors(errors)
.build();
}
}
La Arquitectura de la Consistencia: Separando Responsabilidades
Para una solución robusta y mantenible, aplicaremos el Principio de Responsabilidad Única. En lugar de una sola clase monolítica, dividiremos nuestra lógica en dos componentes especializados, ambos anotados con @RestControllerAdvice. Spring es lo suficientemente inteligente como para detectar y aplicar ambos.
Primero, crearemos una configuración para permitirnos habilitar, deshabilitar o excluir rutas de este comportamiento, dándonos flexibilidad para casos especiales como los endpoints de Actuator o Swagger.
// src/main/java/com/example/demo/common/ResponseWrapperProperties.java
package com.example.demo.common;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Data
@Component
@ConfigurationProperties(prefix = "api.response.wrapper")
public class ResponseWrapperProperties {
private boolean enabled = true;
private List<String> excludedPaths = new ArrayList<>();
}
1. El Guardián de Respuestas Exitosas
Nuestra primera clase, GlobalResponseHandler, tendrá una sola misión: interceptar las respuestas exitosas de los controladores y envolverlas en nuestra estructura ApiResponse. Utiliza la interfaz ResponseBodyAdvice para modificar el cuerpo de la respuesta justo antes de que se envíe.
// src/main/java/com/example/demo/common/GlobalResponseHandler.java
package com.example.demo.common;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalResponseHandler implements ResponseBodyAdvice<Object> {
private final ResponseWrapperProperties properties;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
String path = servletRequest.getRequestURI();
// Si el cuerpo ya es un ApiResponse (creado por el manejador de excepciones)
// o la ruta está excluida, no hacemos nada.
if (body instanceof ApiResponse || isExcluded(path)) {
return body;
}
int status = ((ServletServerHttpResponse) response).getServletResponse().getStatus();
return ApiResponse.success(body, path, status);
}
private boolean isExcluded(String path) {
return !properties.isEnabled() || properties.getExcludedPaths().stream()
.anyMatch(pattern -> pathMatcher.match(pattern, path));
}
}
2. El Centinela Central de Errores
La segunda clase, GlobalExceptionHandler, se dedicará exclusivamente a capturar excepciones lanzadas desde cualquier controlador. Usando @ExceptionHandler, las convierte en nuestra respuesta ApiResponse estandarizada. Este aislamiento hace que el código de manejo de errores sea fácil de encontrar, mantener y extender.
// src/main/java/com/example/demo/common/GlobalExceptionHandler.java
package com.example.demo.common;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<?> handleValidationExceptions(MethodArgumentNotValidException ex, HttpServletRequest request) {
List<ApiResponse.ErrorDetail> errors = ex.getBindingResult().getFieldErrors().stream()
.map(error -> ApiResponse.ErrorDetail.builder()
.code("VALIDATION_ERROR")
.message(String.format("'%s': %s", error.getField(), error.getDefaultMessage()))
.build())
.collect(Collectors.toList());
return ApiResponse.error(errors, request.getRequestURI(), HttpStatus.BAD_REQUEST.value());
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiResponse<?> handleAllUncaughtException(Exception ex, HttpServletRequest request) {
log.error("Error no controlado en la ruta {}: {}", request.getRequestURI(), ex.getMessage(), ex);
ApiResponse.ErrorDetail error = ApiResponse.ErrorDetail.builder()
.code("INTERNAL_SERVER_ERROR")
.message("Ocurrió un error inesperado. Por favor, contacte al soporte.")
.build();
return ApiResponse.error(Collections.singletonList(error), request.getRequestURI(), HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
Ganando Confianza: Pruebas que Validan la Arquitectura
Probar componentes transversales es crucial. Con @WebMvcTest, creamos un contexto de prueba ligero que se enfoca en la capa web. La clave es importar ambas clases de Advice en nuestro test para asegurar que el comportamiento combinado (formateo de éxito y manejo de errores) se verifica correctamente.
// src/test/java/com/example/demo/common/GlobalResponseHandlerTest.java
package com.example.demo.common;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(controllers = GlobalResponseHandlerTest.TestController.class)
// Importamos AMBAS clases para que el contexto de prueba refleje la configuración real.
@Import({GlobalResponseHandler.class, GlobalExceptionHandler.class})
class GlobalResponseHandlerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ResponseWrapperProperties responseWrapperProperties;
// ... (El resto de la clase de prueba, incluyendo setUp, TestController, TestDto y los métodos de prueba, permanece igual) ...
@BeforeEach
void setUp() {
when(responseWrapperProperties.isEnabled()).thenReturn(true);
when(responseWrapperProperties.getExcludedPaths()).thenReturn(List.of("/excluded/**"));
}
@RestController
static class TestController {
@GetMapping("/test/success")
public TestDto getSuccess() { return new TestDto("ok"); }
@PostMapping("/test/validation")
public TestDto postValidation(@Valid @RequestBody TestDto dto) { return dto; }
@GetMapping("/excluded/path")
public TestDto getExcluded() { return new TestDto("excluded"); }
}
@Data
@AllArgsConstructor
static class TestDto {
@NotEmpty
private String message;
}
@Test
@DisplayName("Debería envolver una respuesta exitosa en el formato ApiResponse")
void shouldWrapSuccessResponse() throws Exception {
mockMvc.perform(get("/test/success"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.meta").exists())
.andExpect(jsonPath("$.data.message").value("ok"))
.andExpect(jsonPath("$.errors").doesNotExist());
}
@Test
@DisplayName("No debería envolver una respuesta si la ruta está excluida")
void shouldNotWrapExcludedPath() throws Exception {
mockMvc.perform(get("/excluded/path"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.meta").doesNotExist())
.andExpect(jsonPath("$.message").value("excluded"));
}
@Test
@DisplayName("Debería manejar un error de validación y devolver ApiResponse con detalles de error")
void shouldHandleValidationError() throws Exception {
String invalidDtoJson = "{\"message\":\"\"}";
mockMvc.perform(post("/test/validation")
.contentType(MediaType.APPLICATION_JSON)
.content(invalidDtoJson))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.meta").exists())
.andExpect(jsonPath("$.data").doesNotExist())
.andExpect(jsonPath("$.errors").isArray())
.andExpect(jsonPath("$.errors[0].code").value("VALIDATION_ERROR"));
}
}
Más Allá del Código: El Impacto de una Arquitectura Consistente
Hemos recorrido un camino que va más allá de un simple truco de código. Partimos de un problema real —el caos de las respuestas inconsistentes— y, en lugar de aplicar parches, diseñamos una solución arquitectónica limpia basada en la separación de responsabilidades.
El resultado es un patrón robusto y no invasivo que unifica todas las respuestas bajo un contrato predecible. La lógica está aislada, es configurable y completamente testeable. Esta inversión en diseño reduce la carga cognitiva para todos, desde los desarrolladores del backend hasta los consumidores de la API, creando sistemas más mantenibles, escalables y, en definitiva, más sencillos de razonar.
Este patrón no es un punto final, sino una base sólida. Las posibilidades futuras son claras:
- Documentación de API: El siguiente paso es asegurar que herramientas como OpenAPI/Swagger reflejen esta estructura
ApiResponseautomáticamente, proporcionando una documentación precisa del contrato real. - Trazabilidad Distribuida: El
requestIden los metadatos es la semilla para una trazabilidad completa. Integrarlo con herramientas como Micrometer Tracing permitiría seguir una petición a través de múltiples microservicios, simplificando la depuración en entornos complejos. - Observabilidad Mejorada: El bloque
metapuede enriquecerse con más datos, como el tiempo de procesamiento, para alimentar dashboards en herramientas como Grafana y Prometheus, ofreciendo una visión más profunda del rendimiento de la API.
El Arte del Contexto: Diseño Flexible en Arquitecturas DDD con Java y Spring Boot
- Mauricio ECR
- Snippets
- 23 Sep, 2025
En el universo del desarrollo de software empresarial, nos enfrentamos a un dilema constante: cómo manejar información transversal —ese rastro de datos vitales como IDs de correlación, información del
El Arte del Contexto: Diseño Flexible en Arquitecturas DDD con Java y Spring Boot
- Mauricio ECR
- Snippets
- 23 Sep, 2025
En el universo del desarrollo de software empresarial, nos enfrentamos a un dilema constante: cómo manejar información transversal —ese rastro de datos vitales como IDs de correlación, información del usuario o el tenant de un sistema multi-inquilino— sin que contamine la pureza de nuestro dominio. Estos datos son el sistema nervioso de la aplicación, esenciales para la trazabilidad, la auditoría y la seguridad, pero no son parte del lenguaje de negocio. Incluirlos como parámetros en cada método del dominio es una solución rápida que, a la larga, genera un código verboso y acoplado.
Imagina que estás construyendo un sistema complejo con Java 21, Spring Boot y una filosofía Domain-Driven Design (DDD). Tu arquitectura está elegantemente separada en capas de dominio, infraestructura y aplicación. ¿Cómo logras que ese "contexto" de ejecución fluya mágicamente a través de todas las capas, disponible cuando se necesita, pero invisible cuando no? El objetivo es crear un mecanismo que sea a la vez transparente y robusto, que funcione igual de bien para una petición REST, un proceso batch o un mensaje de una cola.
La encrucijada del diseño: ¿Parámetro o Magia?
Cuando nos enfrentamos a la propagación de contexto, hay dos caminos. El primero es el de la explicitud: si un dato es parte del lenguaje de negocio (como el "autor" de una acción), debe ser un parámetro explícito en el dominio. No hay discusión. El segundo camino es el de la transversalidad, reservado para metadatos puramente técnicos. Aquí es donde queremos un poco de "magia controlada", una forma de acceder a la información sin que ensucie nuestras interfaces de negocio.
Para esta magia, ThreadLocal se presenta como un candidato ideal en el ecosistema tradicional de Spring. Es, en esencia, una caja de almacenamiento que cada hilo de ejecución lleva consigo. Lo que un hilo guarda en su ThreadLocal, solo ese hilo puede verlo, garantizando un aislamiento perfecto en entornos concurrentes.
Nuestra herramienta será un ExecutionContext, un contenedor simple que vivirá en ese ThreadLocal. Usamos un Map<String, Object> en su interior por una razón clave: flexibilidad. Podríamos crear una clase con campos fijos (correlationId, userId, etc.), pero un mapa nos permite añadir nuevos datos al contexto en el futuro sin modificar la clase base. Es un compromiso consciente entre la seguridad de tipos y la extensibilidad.
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
// Un contenedor simple y flexible para los datos de nuestro contexto.
public class ExecutionContext {
private static final ThreadLocal<ExecutionContext> CONTEXT = new ThreadLocal<>();
private final Map<String, Object> values = new ConcurrentHashMap<>();
// ... (métodos init, current, clear, put, get)
public static ExecutionContext current() { return CONTEXT.get(); }
public static void set(ExecutionContext context) { CONTEXT.set(context); }
public static void clear() { CONTEXT.remove(); }
public static ExecutionContext init() {
ExecutionContext ctx = new ExecutionContext();
set(ctx);
return ctx;
}
public void put(String key, Object value) { values.put(key, value); }
@SuppressWarnings("unchecked")
public <T> T get(String key, Class<T> type) { return (T) values.get(key); }
}
El guardián del ciclo de vida: Automatización con un Filter
Tener el ExecutionContext es solo el primer paso. El mayor riesgo de ThreadLocal es el olvido. En un servidor como Tomcat, los hilos se reciclan. Si no limpiamos el contexto al final de una petición, ese hilo reutilizado podría servir a otro usuario con los datos del anterior, una brecha de seguridad y de datos catastrófica.
Aquí es donde entra en juego el Filter de Servlet, el guardián de nuestro contexto. Un Filter es perfecto porque opera a un nivel más bajo que los controladores de Spring. Intercepta toda petición entrante, dándonos el lugar ideal para:
- Inicializar el contexto al empezar.
- Poblarlo con datos de la petición, como cabeceras.
- Garantizar su limpieza al terminar, pase lo que pase.
El bloque try...finally no es una opción, es una obligación. Es el seguro de vida que nos protege contra las fugas de memoria y la contaminación de datos. En el siguiente código, no solo capturamos un ID de correlación, sino también un token JWT, demostrando la flexibilidad del mapa.
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.logging.log4j.ThreadContext;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.UUID;
@Component
public class ExecutionContextFilter implements Filter {
// ... (constantes para las claves)
private static final String CORRELATION_ID_HEADER = "X-Correlation-ID";
private static final String AUTH_HEADER = "Authorization";
private static final String CORRELATION_ID_KEY = "correlationId";
private static final String JWT_KEY = "jwtToken";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
ExecutionContext.init(); // Nace el contexto
try {
// Se enriquece con datos de la petición
if (request instanceof HttpServletRequest httpRequest) {
String correlationId = httpRequest.getHeader(CORRELATION_ID_HEADER);
// ... (lógica para generar correlationId si no existe)
ExecutionContext.current().put(CORRELATION_ID_KEY, correlationId);
ThreadContext.put(CORRELATION_ID_KEY, correlationId); // Se lo pasamos a Log4j2
String token = httpRequest.getHeader(AUTH_HEADER);
if (token != null) {
ExecutionContext.current().put(JWT_KEY, token);
}
}
chain.doFilter(request, response);
} finally {
// Se limpia, garantizando que el hilo reciclado esté impoluto
ThreadContext.clearMap();
ExecutionContext.clear();
}
}
}
Observabilidad sin esfuerzo: El poder del Logging contextual
Depurar un problema en producción sin un ID de correlación es como buscar una aguja en un pajar. Al integrar nuestro contexto con el sistema de logging (vía el MDC de Log4j2, que es su propia versión de ThreadLocal), cada línea de log generada durante la petición quedará marcada con ese identificador único. De repente, el pajar se organiza en hilos de paja perfectamente trazables.
Solo necesitamos decirle a Log4j2 que muestre esa información en su patrón:
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - [%X{correlationId}] - %msg%n"/>
</Console>
</Appenders>
</Configuration>
Un ejemplo práctico para unirlo todo
La teoría está muy bien, pero veámoslo en acción. Imaginemos un flujo simple: un controlador recibe una petición, llama a un caso de uso que a su vez depende de un repositorio para hablar con un servicio externo.
El Dominio, un oasis de pureza:
El código del dominio no sabe nada del contexto. Define los contratos (HelloWorldRepository) y orquesta la lógica (GetHelloWorldUseCase), manteniéndose limpio y enfocado.
// Caso de Uso: depende de una abstracción y no sabe de dónde saldrán los datos.
public class GetHelloWorldUseCase {
private final HelloWorldRepository helloWorldRepository;
// ... (constructor)
public String execute() {
// ... (log de inicio)
String externalGreeting = helloWorldRepository.getGreeting();
// ¡Aquí es donde el dominio se beneficia de la "magia"!
// Accede al contexto de forma segura y opcional.
String correlationId = ExecutionContext.current().get("correlationId", String.class);
System.out.println("DESDE CASO DE USO: Mensaje del repo: '" + externalGreeting +
"'. Trazabilidad: " + correlationId);
return externalGreeting;
}
}
La Infraestructura, donde ocurre la magia:
Aquí es donde implementamos los detalles. Nuestro RestHelloWorldRepository simula ser un cliente REST. Antes de hacer su "llamada", consulta el ExecutionContext para obtener el token JWT que necesita para autenticarse. ¡El caso de uso nunca tuvo que pasárselo!
// Implementación del Repositorio: el "fontanero" que conecta con el mundo exterior.
public class RestHelloWorldRepository implements HelloWorldRepository {
@Override
public String getGreeting() {
// Obtiene el token que el Filter puso en el contexto.
String jwt = ExecutionContext.current().get("jwtToken", String.class);
System.out.println("DESDE REPOSITORIO (CLIENTE REST): Usando el token para la llamada -> " + jwt);
return "Hola desde el servicio externo!";
}
}
Al ejecutar una petición con curl que incluya las cabeceras X-Correlation-ID y Authorization, la salida en la consola nos cuenta la historia completa: el log con el ID, la implementación del repositorio usando el token, y el caso de uso accediendo de nuevo al ID. Todo fluyó sin que una sola firma de método se viera alterada.
Conclusión: Un patrón poderoso con responsabilidades
Hemos construido un sistema robusto para manejar el contexto. Sin embargo, este poder conlleva responsabilidades. El patrón ThreadLocal es una herramienta fantástica para arquitecturas síncronas basadas en el modelo "un hilo por petición", pero es el enfoque incorrecto para sistemas reactivos (como WebFlux), donde la ejecución no está atada a un único hilo. Allí, la solución nativa es el Context de Project Reactor.
Además, con la llegada de los Hilos Virtuales en Java, el uso masivo de ThreadLocal puede limitar sus beneficios de escalabilidad. El futuro apunta a los Scoped Values (JEP 446), diseñados precisamente para este tipo de propagación de datos de forma más eficiente.
Entender estas fronteras es tan importante como conocer el patrón en sí. La clave del éxito es siempre la misma: mantener el dominio puro y delegar las complejidades técnicas a una capa de infraestructura bien diseñada.