Skip to content
SP StackPractices
intermediate By StackPractices

Context Object Pattern

Encapsulate state and services needed by multiple components into a single context object, reducing method signature bloat and decoupling code from specific environment details.

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Context Object Pattern

Overview

The Context Object Pattern encapsulates state and services needed by multiple components into a single context object that is passed through the call chain. Instead of threading ten parameters through every method signature, components receive a single context object that provides access to request data, user sessions, configuration, logging, and services.

This pattern is ubiquitous in modern frameworks. HTTP request contexts in web frameworks, React’s Context API, and Android’s Context class are all implementations. The key benefit is reducing method signature bloat while keeping components decoupled from the specific environment they run in.

When to Use

Use the Context Object Pattern when:

  • Multiple methods need access to the same set of cross-cutting concerns
  • Method signatures grow unwieldy with repeated parameters (request, user, config, logger)
  • You need to pass implicit data through layers without global variables
  • Components should be decoupled from the specific runtime environment

When to Avoid

  • Simple methods that only need one or two parameters (over-engineering)
  • When the context becomes a God object containing unrelated concerns
  • Deeply nested contexts where mutations at one level affect distant callers
  • Situations where explicit parameter passing makes dependencies clearer

Solution

Python

from dataclasses import dataclass, field
from typing import Dict, Any, Optional
from datetime import datetime
import uuid

@dataclass
class RequestContext:
    """Context object carrying request-scoped state and services"""
    request_id: str = field(default_factory=lambda: str(uuid.uuid4()))
    timestamp: datetime = field(default_factory=datetime.now)
    user_id: Optional[str] = None
    correlation_id: Optional[str] = None
    metadata: Dict[str, Any] = field(default_factory=dict)

    # Cross-cutting services (would be injected in real apps)
    logger = None
    config = None

    def with_user(self, user_id: str) -> 'RequestContext':
        """Immutable copy with user set"""
        new_ctx = RequestContext(
            request_id=self.request_id,
            timestamp=self.timestamp,
            user_id=user_id,
            correlation_id=self.correlation_id,
            metadata=self.metadata.copy()
        )
        return new_ctx

    def add_metadata(self, key: str, value: Any) -> 'RequestContext':
        new_ctx = self.with_user(self.user_id)
        new_ctx.metadata[key] = value
        return new_ctx


class ServiceLayer:
    """Business logic that uses context instead of many parameters"""
    def process_order(self, ctx: RequestContext, order_data: dict) -> dict:
        # Access context data without threading params through every call
        print(f"[{ctx.request_id}] Processing order for user {ctx.user_id}")

        # Pass context down to lower layers
        validated = self._validate(ctx, order_data)
        saved = self._persist(ctx, validated)
        return self._notify(ctx, saved)

    def _validate(self, ctx: RequestContext, data: dict) -> dict:
        print(f"[{ctx.request_id}] Validating order data")
        return {**data, "validated": True}

    def _persist(self, ctx: RequestContext, data: dict) -> dict:
        print(f"[{ctx.request_id}] Persisting to database")
        return {**data, "order_id": "ORD-123"}

    def _notify(self, ctx: RequestContext, data: dict) -> dict:
        print(f"[{ctx.request_id}] Sending notification")
        return {**data, "notified": True}


# Middleware / framework layer creates context
class RequestHandler:
    def __init__(self, service: ServiceLayer):
        self.service = service

    def handle_request(self, raw_request: dict) -> dict:
        # Build context at the boundary
        ctx = RequestContext(
            user_id=raw_request.get("user_id"),
            correlation_id=raw_request.get("correlation_id")
        )

        # Single context object flows through all layers
        return self.service.process_order(ctx, raw_request.get("order_data", {}))


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

Java

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

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 builder) {
        this.requestId = builder.requestId;
        this.timestamp = builder.timestamp;
        this.userId = builder.userId;
        this.correlationId = builder.correlationId;
        this.metadata = Collections.unmodifiableMap(new HashMap<>(builder.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 userId) { this.userId = userId; return this; }
        public Builder correlationId(String id) { this.correlationId = id; return this; }
        public Builder metadata(String key, Object value) { this.metadata.put(key, value); 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() + "] Processing order for user " + ctx.getUserId());
        Map<String, Object> result = new HashMap<>(orderData);
        result.put("order_id", "ORD-123");
        return result;
    }
}

class RequestHandler {
    private final OrderService service;
    public RequestHandler(OrderService service) { this.service = service; }

    public Map<String, Object> handleRequest(Map<String, Object> rawRequest) {
        RequestContext ctx = new RequestContext.Builder()
            .userId((String) rawRequest.get("user_id"))
            .correlationId((String) rawRequest.get("correlation_id"))
            .build();

        return service.processOrder(ctx, (Map<String, Object>) rawRequest.get("order_data"));
    }
}

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

JavaScript

class RequestContext {
  constructor(options = {}) {
    this.requestId = options.requestId || crypto.randomUUID();
    this.timestamp = options.timestamp || new Date();
    this.userId = options.userId || null;
    this.correlationId = options.correlationId || null;
    this.metadata = new Map(options.metadata || []);
  }

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

  withMetadata(key, value) {
    const ctx = this.withUser(this.userId);
    ctx.metadata.set(key, value);
    return ctx;
  }
}

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

class RequestHandler {
  constructor(service) {
    this.service = service;
  }

  handleRequest(rawRequest) {
    const ctx = new RequestContext({
      userId: rawRequest.user_id,
      correlationId: rawRequest.correlation_id,
    });

    return this.service.processOrder(ctx, rawRequest.order_data || {});
  }
}

// Usage
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);

Explanation

The Context Object Pattern replaces scattered parameters with a single carrier object:

  • Before: process(userId, requestId, logger, config, db, cache, data)
  • After: process(ctx, data) where ctx contains everything else

This keeps method signatures focused on business parameters while still giving deep layers access to cross-cutting concerns. The context is typically created at system boundaries (HTTP requests, message handlers) and flows down through service layers.

Variants

VariantScopeUse Case
Request-scopedOne context per HTTP requestWeb frameworks, tracing
Thread-localStored in thread-local storageJava, C# async contexts
Async contextPropagated through async callsNode.js AsyncLocalStorage
Global/singletonSingle app-wide contextCLI tools, desktop apps

Best Practices

  • Keep contexts immutable. Create new instances instead of mutating shared state.
  • Scope contexts narrowly. Request-scoped, not global. Avoid singleton contexts.
  • Do not put business logic in context. It should only carry state and references.
  • Provide factory methods. withUser(), withMetadata() make immutability ergonomic.
  • Use TypeScript/Java generics. Type-safe contexts prevent runtime errors.

Common Mistakes

  • The context becomes a God object. If it has 50 fields, split into focused contexts.
  • Mutating context mid-request. Side effects leak between components unpredictably.
  • Using context to hide dependencies. Explicit parameters are clearer for core business args.
  • Not propagating through async boundaries. Lost context breaks tracing and user association.
  • Storing large objects in context. Heavy objects increase memory pressure and GC overhead.

Real-World Examples

HTTP Request Contexts

Django’s request object, Express.js req objects, and Go’s context.Context all carry request-scoped data through middleware and handlers.

React Context API

React’s createContext / useContext passes data through the component tree without prop drilling, solving the same problem in UI hierarchies.

Android Context

Android’s Context class provides access to resources, preferences, and system services throughout the app lifecycle.

Frequently Asked Questions

Q: What is the difference between Context Object and Dependency Injection? A: DI wires services into objects at construction time. Context Object passes runtime state through the call chain. They often work together.

Q: Is Context Object an anti-pattern? A: It becomes an anti-pattern when abused as a global variable or God object. Used well, it is essential for clean architecture.

Q: How do I propagate context in async code? A: Use language-specific mechanisms: AsyncLocalStorage in Node.js, ThreadLocal in Java, or explicit passing in Python asyncio.