Skip to main content

Request Context

RequestContext is a mutable key-value bag scoped to a single pipeline execution. A fresh instance is created for every mediator.send(...) call — values are never shared between concurrent requests, even when the mediator is a singleton.

:::info Why a fresh context per request, not a shared property? MediatorImpl is a singleton. If RequestContext were a class-level property, concurrent send() calls — two ViewModels firing at the same time, for example — would overwrite each other's locale, auth token, or any other bag value.

Creating it inside send() scopes the context to that single pipeline execution, the same way ASP.NET Core scopes HttpContext per HTTP request. Pipeline behaviors (like LocalePipelineBehavior) populate it, and handlers consume it — all within one isolated request lifecycle. :::


Writing to the context

Pipeline behaviors (typically Stage.Pre) populate the context:

class TraceIdBehavior : PipelineBehavior {
override val stage = Stage.Pre

override suspend fun <TRequest : Request<TResult>, TResult> process(
requestContext: RequestContext,
next: RequestHandlerDelegate<TRequest, TResult>,
request: TRequest,
): TResult {
requestContext.put("traceId", generateTraceId())
requestContext.put("userId", resolveCurrentUser())
return next(request)
}
}

Reading from the context

Handlers and Stage.Post behaviors read values by key:

class CreateOrderHandler : RequestHandler<CreateOrderCommand, Order> {
override suspend fun handle(
mediator: Mediator,
requestContext: RequestContext,
request: CreateOrderCommand,
): Order {
val userId = requestContext.getMetaDate<String>("userId")
?: error("userId not set in context")
return orderService.create(userId, request.cartId)
}
}

getMetaDate<T>(key) returns null if the key is absent or the stored value can't be cast to T.


Typed extension properties

Instead of raw string keys, declare typed extension properties on RequestContext for compile-time safety and IDE autocomplete:

var RequestContext.locale: String
get() = getMetaDate("locale") ?: "en"
set(value) { put("locale", value) }

var RequestContext.userId: String?
get() = getMetaDate("userId")
set(value) { if (value != null) put("userId", value) }

Usage is then clean and type-safe:

// write (in a pre-processor or behavior)
requestContext.locale = "ar"
requestContext.userId = currentUser.id

// read (in a handler)
val lang = requestContext.locale // "ar"
val uid = requestContext.userId // String?

Avoiding key collisions

For teams that prefer raw string keys, use constants or fully-qualified names to avoid collisions between unrelated components:

object ContextKeys {
const val TRACE_ID = "com.myapp.traceId"
const val USER_ID = "com.myapp.userId"
}

// write
requestContext.put(ContextKeys.TRACE_ID, traceId)

Option 2 — read back with a typed call:

val traceId = requestContext.getMetaDate<String>(ContextKeys.TRACE_ID)

Next

Exception Handling