StackPractices
intermediate Por Mathias Paulenko

Patrón Context Object: Ejemplos

Reduce la proliferación de parámetros pasando un único objeto de contexto por la cadena de llamadas. Ejemplos en Python, Java y JavaScript.

Visión general

Cada capa de una app web quiere las mismas cinco cosas: el id de la petición, el usuario autenticado, el logger, la configuración, quizá un correlation id para trazado. La versión ingenua es una firma tipo process(userId, requestId, logger, config, db, cache, data) — y empeora cada vez que aparece una nueva preocupación transversal, porque todas las firmas intermedias tienen que cambiar también.

El patrón Context Object empaqueta ese estado compartido en un único objeto que se crea una vez en el borde del sistema — el middleware, el handler de la petición — y viaja hacia abajo sin cambios. Las firmas pasan a process(ctx, data): los parámetros de negocio siguen visibles y las preocupaciones de infraestructura viajan gratis.

flowchart diagram: REQ[

Cuándo usarlo

  • Varias capas necesitan los mismos datos transversales — ids de trazado, usuario, locale, feature flags
  • Las firmas acumulan los mismos parámetros repetidos
  • Quieres estado con ámbito de petición sin variables globales ni magia de thread-local
  • Los componentes deben seguir desacoplados del entorno donde se ejecutan

Para una fachada más gruesa sobre servicios remotos, el patrón Business Delegate encaja mejor — oculta un subsistema entero en lugar de transportar estado de la petición.

Cuándo evitarlo

  • Métodos que solo necesitan uno o dos parámetros — ahí el contexto es pura ceremonia
  • Cuando el contexto empieza a coleccionar preocupaciones sin relación (un God object disfrazado)
  • Cuando un parámetro explícito deja la dependencia más clara — no escondas entradas reales dentro de ctx
  • Rutas críticas donde la asignación extra pesa más que la legibilidad

Solución

Python

from dataclasses import dataclass, field, replace
from typing import Any
from datetime import datetime
import logging
import uuid

@dataclass(frozen=True)
class RequestContext:
    """Estado con ámbito de petición, inmutable tras construirse."""
    request_id: str = field(default_factory=lambda: str(uuid.uuid4()))
    timestamp: datetime = field(default_factory=datetime.now)
    user_id: str | None = None
    correlation_id: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)

    def with_user(self, user_id: str) -> "RequestContext":
        # dataclass frozen: construye una copia en lugar de mutar
        return replace(self, user_id=user_id, metadata=dict(self.metadata))


class OrderService:
    def process_order(self, ctx: RequestContext, order_data: dict) -> dict:
        logging.info("[%s] Procesando pedido de %s", ctx.request_id, ctx.user_id)
        validated = self._validate(order_data)
        return self._persist(ctx, validated)

    def _validate(self, data: dict) -> dict:
        return {**data, "validated": True}

    def _persist(self, ctx: RequestContext, data: dict) -> dict:
        logging.info("[%s] Persistiendo pedido", ctx.request_id)
        return {**data, "order_id": "ORD-123"}


# La capa de borde construye el contexto una sola vez
class RequestHandler:
    def __init__(self, service: OrderService):
        self.service = service

    def handle_request(self, raw_request: dict) -> dict:
        ctx = RequestContext(
            user_id=raw_request.get("user_id"),
            correlation_id=raw_request.get("correlation_id"),
        )
        return self.service.process_order(ctx, raw_request.get("order_data", {}))


handler = RequestHandler(OrderService())
result = handler.handle_request({
    "user_id": "user-42",
    "correlation_id": "corr-abc",
    "order_data": {"items": ["book", "pen"]},
})
print(result["order_id"])  # ORD-123

Java

import java.time.Instant;
import java.util.*;

public class RequestContext {
    private final String requestId;
    private final Instant timestamp;
    private final String userId;
    private final String correlationId;
    private final Map<String, Object> metadata;

    private RequestContext(Builder b) {
        this.requestId = b.requestId;
        this.timestamp = b.timestamp;
        this.userId = b.userId;
        this.correlationId = b.correlationId;
        this.metadata = Collections.unmodifiableMap(new HashMap<>(b.metadata));
    }

    public String getRequestId() { return requestId; }
    public String getUserId() { return userId; }
    public String getCorrelationId() { return correlationId; }
    public Map<String, Object> getMetadata() { return metadata; }

    public static class Builder {
        private String requestId = UUID.randomUUID().toString();
        private Instant timestamp = Instant.now();
        private String userId;
        private String correlationId;
        private Map<String, Object> metadata = new HashMap<>();

        public Builder userId(String v) { this.userId = v; return this; }
        public Builder correlationId(String v) { this.correlationId = v; return this; }
        public Builder metadata(String k, Object v) { this.metadata.put(k, v); return this; }
        public RequestContext build() { return new RequestContext(this); }
    }
}

class OrderService {
    public Map<String, Object> processOrder(RequestContext ctx, Map<String, Object> orderData) {
        System.out.println("[" + ctx.getRequestId() + "] Pedido de " + ctx.getUserId());
        Map<String, Object> result = new HashMap<>(orderData);
        result.put("order_id", "ORD-123");
        return result;
    }
}

// Borde: construir el contexto una vez y pasarlo hacia abajo
class RequestHandler {
    private final OrderService service;
    RequestHandler(OrderService service) { this.service = service; }

    public Map<String, Object> handleRequest(Map<String, Object> raw) {
        RequestContext ctx = new RequestContext.Builder()
            .userId((String) raw.get("user_id"))
            .correlationId((String) raw.get("correlation_id"))
            .build();
        return service.processOrder(ctx, (Map<String, Object>) raw.get("order_data"));
    }
}

RequestHandler handler = new RequestHandler(new OrderService());
Map<String, Object> request = Map.of(
    "user_id", "user-42",
    "order_data", Map.of("items", List.of("book", "pen"))
);
System.out.println(handler.handleRequest(request).get("order_id"));

JavaScript

class RequestContext {
  constructor({ userId = null, correlationId = null, metadata = new Map() } = {}) {
    this.requestId = crypto.randomUUID();
    this.timestamp = new Date();
    this.userId = userId;
    this.correlationId = correlationId;
    this.metadata = metadata;
    Object.freeze(this); // sin mutaciones a mitad de petición
  }

  withUser(userId) {
    return new RequestContext({ userId, correlationId: this.correlationId, metadata: new Map(this.metadata) });
  }
}

class OrderService {
  processOrder(ctx, orderData) {
    console.log(`[${ctx.requestId}] Pedido de ${ctx.userId}`);
    return { ...orderData, orderId: 'ORD-123' };
  }
}

// Borde: se construye una vez en el middleware y se pasa hacia abajo
class RequestHandler {
  constructor(service) { this.service = service; }
  handleRequest(raw) {
    const ctx = new RequestContext({ userId: raw.user_id, correlationId: raw.correlation_id });
    return this.service.processOrder(ctx, raw.order_data || {});
  }
}

const handler = new RequestHandler(new OrderService());
const result = handler.handleRequest({
  user_id: 'user-42',
  correlation_id: 'corr-abc',
  order_data: { items: ['book', 'pen'] },
});
console.log(result.orderId); // ORD-123

Código complementario: el repo companion de context-object-pattern tiene las tres implementaciones listas para ejecutar.

Explicación

La mecánica es sencilla — la disciplina está en dónde vive el objeto:

  • Se crea una vez en el borde. El middleware, el handler de la petición o el consumidor del mensaje construyen el contexto; las capas profundas solo lo leen. Si algún componente de abajo se está construyendo su propio contexto, el patrón ya está roto.
  • Inmutable por defecto. Mutar contexto compartido a mitad de petición filtra efectos secundarios entre llamadores. Mejor copias estilo withUser() — en Python, dataclass frozen con replace(); en JS, Object.freeze; en Java, clase inmutable con builder.
  • Se lee como infraestructura, no como datos de negocio. Ids de trazado, locale, tenant, usuario autenticado — bien. El pedido que se está procesando es un parámetro real y debe seguir siéndolo. El patrón Dependency Injection resuelve la mitad de servicios de este problema; el objeto de contexto resuelve la mitad del estado por petición.

La advertencia async

El fallo clásico del patrón es perder el contexto al cruzar un await. Node.js lo resuelve con AsyncLocalStorage, Python asyncio con contextvars, Java históricamente con ThreadLocal (o el contexto de Reactor). Elijas lo que elijas, la regla es la misma: el contexto tiene que sobrevivir los saltos async o tus trazas pierden el request id en silencio.

Variantes

VarianteÁmbitoUso típico
Request-scopedUn contexto por petición HTTPFrameworks web, trazado
Thread-localGuardado en almacenamiento thread-localJava, contextos async en C#
Async contextPropagado por llamadas asyncAsyncLocalStorage de Node.js, contextvars de Python
Global/singletonUn contexto para toda la appHerramientas CLI, apps de escritorio

Lo que funciona

  • Mantén los contextos inmutables. Instancias nuevas en lugar de estado compartido mutable.
  • Ámbito estrecho. Por petición, no global — los contextos singleton son donde este patrón se convierte en el anti-patrón del que todos advierten.
  • Nada de lógica de negocio en el contexto. Lleva estado y referencias, no reglas.
  • Métodos de fábrica para la inmutabilidad. withUser(), withMetadata().
  • Tipado. Los genéricos de TypeScript o los campos tipados de Java ganan al Map<String, Object> genérico — un contexto tipado además se autodocumenta.

Errores comunes

  • El contexto se convierte en God object. Cincuenta campos significa que toca partirlo en contextos más pequeños, no hacer la clase más grande.
  • Mutarlo a mitad de petición. Los efectos secundarios se filtran entre componentes y depurar se convierte en arqueología.
  • Esconder entradas reales dentro. Si un valor es dato de negocio, va en la firma — así quien lee el método sabe qué necesita.
  • Perderlo en los límites async. Un contexto que no sobrevive un await/then rompe el trazado justo donde más falta hace.
  • Guardar objetos pesados. Un blob grande en un objeto con ámbito de petición es un impuesto de GC en cada llamada.

Ejemplos del mundo real

Contextos de petición HTTP

El request de Django, el req de Express y el context.Context de Go son este patrón: portadores con ámbito de petición que recorren middleware y handlers.

React Context API

createContext/useContext mueven datos por el árbol de componentes sin prop drilling — el mismo problema un nivel más arriba, en la jerarquía de UI.

Android Context

La clase Context de Android expone recursos, preferencias y servicios del sistema a lo largo del ciclo de vida de la app — la variante de ámbito global, con los contrapesos que eso implica.

Ver también

Lectura adicional

Preguntas frecuentes

¿Qué diferencia hay entre Context Object e Inyección de Dependencias?

DI conecta servicios en tiempo de construcción; el objeto de contexto transporta estado runtime por la cadena de llamadas. La mayoría de apps reales usan ambos — DI para el grafo de servicios, contexto para la petición.

¿Es Context Object un anti-patrón?

Solo cuando se convierte en una variable global con nombre bonito. Mantenido con ámbito de petición e inmutable, es la columna vertebral de la mayoría de frameworks web.

¿Cómo propago contexto en código async?

AsyncLocalStorage en Node.js, contextvars en Python asyncio, contexto de Reactor o ThreadLocal en Java. O pásalo como argumento — explícito siempre funciona.

¿Cuándo es mejor un parámetro normal que un objeto de contexto?

Cuando el valor es entrada de negocio, no infraestructura. process(order) dice más que process(ctx) si el método realmente necesita el pedido. El contexto es para lo que todas las capas necesitan, no para ocultar lo que consume una función.

¿Cómo testeo código que recibe un objeto de contexto?

Construye un contexto mínimo en el test (RequestContext(userId="t") o una factory de test) — esa es la ventaja: un objeto que stubar en vez de siete argumentos de constructor.