#421 · bluetape4k-projects · tenant context

Carry the tenant. Respect the boundary.

Compare four tenant carriers by the value a caller can read, the scope that owns it, and what survives a boundary.

Carrier

Explore the carrier boundary

Choose a carrier and replay six scenarios: what is visible, who owns it, and what remains after the boundary.

Carrier
ThreadLocal

This carrier instance owns the binding on the current thread.

    Current step · 01

    In progress

    Progress1 / 3

    Keyboard: tab to a step and press Enter to inspect it.

    Compare the ownership contract

    Compare the ownership contract

    Compare four tenant carriers by the value a caller can read, the scope that owns it, and what survives a boundary.

    ThreadLocal

    This carrier instance owns the binding on the current thread.

    Binding
    withTenant(tenantId) sets the value before the block.
    Read
    Read from the same thread with currentOrNull or requireCurrent.
    After the boundary
    finally restores the previous value or removes it.

    ScopedValue

    The lexical ScopedValue carrier owns the binding.

    Binding
    ScopedValue.where(...).run opens a lexical binding.
    Read
    Read only while the lexical carrier is bound.
    After the boundary
    Leaving run restores the previous lexical state.

    Reactor Context

    Each subscriber owns an immutable Context chain.

    Binding
    contextWrite derives a Context for this subscription.
    Read
    deferContextual reads the subscriber Context explicitly.
    After the boundary
    Cancellation ends the subscription; the outer Context is unchanged.

    Ktor Call

    The ApplicationCall attributes own the binding.

    Binding
    bindTenant writes a private, write-once call attribute.
    Read
    Read from the same ApplicationCall, even after a dispatcher hop.
    After the boundary
    The call retains its tenant until the call is released.

    Caller responsibilities

    Caller responsibilities

    No carrier invents a tenant.

    ThreadLocal

    A dispatcher or suspend hop has no automatic propagation.

    ScopedValue

    StructuredTaskScope.fork inherits; an independent virtual thread and coroutine bridge do not.

    Reactor Context

    Scheduler work stays in the chain; hooks and coroutine bridges are not implied.

    Ktor Call

    A new call is unbound; pass the call explicitly across a dispatcher.

    After the boundary: Authentication, authorization, tenant resolution, and business-effect safety remain caller-owned.

    Pinned source

    Pinned source

    5954b6329a3e11c70ef12b6d4bd8480e7b38be1b