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)wherectxcontains 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
| Variant | Scope | Use Case |
|---|---|---|
| Request-scoped | One context per HTTP request | Web frameworks, tracing |
| Thread-local | Stored in thread-local storage | Java, C# async contexts |
| Async context | Propagated through async calls | Node.js AsyncLocalStorage |
| Global/singleton | Single app-wide context | CLI 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.