Type something to search...
El Arte del Contexto: Diseño Flexible en Arquitecturas DDD con Java y Spring Boot

El Arte del Contexto: Diseño Flexible en Arquitecturas DDD con Java y Spring Boot

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:

  1. Inicializar el contexto al empezar.
  2. Poblarlo con datos de la petición, como cabeceras.
  3. 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.

Related Posts

Diseñando un Wrapper de Respuesta en Java con Funcionalidades de Optional y Gestión de Estado

Diseñando un Wrapper de Respuesta en Java con Funcionalidades de Optional y Gestión de Estado

En el desarrollo de aplicaciones Java, el manejo de respuestas a solicitudes —especialmente aquellas que involucran operaciones asincrónicas, procesamiento de datos o comunicación con servicios extern

Leer más
Una Propuesta para Estandarizar la Seguridad en APIs REST con Arquitectura Hexagonal y Spring Security

Una Propuesta para Estandarizar la Seguridad en APIs REST con Arquitectura Hexagonal y Spring Security

En el desarrollo de aplicaciones empresariales modernas, la seguridad es un pilar fundamental. Sin embargo, lograr una arquitectura de seguridad que sea reutilizable, desacoplada y, al mismo t

Leer más
Manejando el Caos: Una Guía Definitiva sobre Excepciones de Dominio 🎯

Manejando el Caos: Una Guía Definitiva sobre Excepciones de Dominio 🎯

En el desarrollo de software moderno, escribir código que funciona perfectamente en escenarios ideales es solo el primer paso. La verdadera fortaleza de un sistema se manifiesta cuando debe enfrentar

Leer más
El Arte de Conectar: Forjando un DataSource Dinámico en Spring Boot

El Arte de Conectar: Forjando un DataSource Dinámico en Spring Boot

En el vertiginoso universo del desarrollo de software, donde la seguridad es un pilar innegociable y la agilidad es la moneda de cambio, nos enfrentamos a desafíos que van más allá de la simple lógica

Leer más
El Arte de la Consistencia: Creando Respuestas API Predecibles en Spring MVC

El Arte de la Consistencia: Creando Respuestas API Predecibles en Spring MVC

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

Leer más
Transformando Colecciones con Java Streams: 15 Métodos Esenciales

Transformando Colecciones con Java Streams: 15 Métodos Esenciales

Introducción En el mundo de Java, trabajar con colecciones de datos solía ser sinónimo de bucles interminables, condicionales anidados y código repetitivo. Pero con la llegada de Java Streams

Leer más
Optimizando el Acceso a Datos: La Importancia de las Proyecciones JPA en Spring Boot

Optimizando el Acceso a Datos: La Importancia de las Proyecciones JPA en Spring Boot

El Costo Oculto de Traer Demasiada Información En el desarrollo de aplicaciones que interactúan con bases de datos, una tarea fundamental es la recuperación de datos. Al usar Object-Relational Map

Leer más
Desmitificando Gradle: El Primer Paso para Automatizar tu Mundo Java

Desmitificando Gradle: El Primer Paso para Automatizar tu Mundo Java

En el vertiginoso universo del desarrollo de software, la eficiencia no es un lujo, es una necesidad. Dedicar tiempo a tareas repetitivas como compilar código, ejecutar pruebas, empaquetar la aplicaci

Leer más
Del Dicho al Hecho: Generando Proyectos Java con Plantillas y FreeMarker

Del Dicho al Hecho: Generando Proyectos Java con Plantillas y FreeMarker

En el artículo anterior, alcanzamos un hito crucial: construimos un plugin binario funcional en Java, completo con su propia configuración y tarea. Nuestro plugin "saludador" demostró que dominamos la

Leer más
De Consumidor a Creador: Construyendo tu Primer Plugin Binario de Gradle

De Consumidor a Creador: Construyendo tu Primer Plugin Binario de Gradle

En nuestro artículo anterior, desmitificamos Gradle y sentamos las bases para entender su funcionamiento. Aprendimos a crear proyectos, ejecutar tareas y comprendimos el rol fundamental de los plugins

Leer más