Context Binding
Context binding is the mechanism that allows AmbitenModel to execute against the active runtime state without forcing infrastructure concerns through every layer of an application.
In Ambiten, models do not operate as isolated collection wrappers.
They execute inside a managed runtime boundary where execution-specific information such as:
tenant identity
request metadata
database overrides
collection overrides
transaction session
logger metadata
custom runtime metadatacan participate in model execution automatically.
This allows a model call to remain structurally simple:
await UserModel.find({});while the runtime determines the effective context and infrastructure required for that operation.
Context binding is therefore the connection between:
execution stateand:
model executionWhy Context Binding Exists
In conventional application architectures, runtime state often leaks progressively into application APIs.
A service may begin with:
createUser(data);and eventually become:
createUser(
tenantId,
dbName,
session,
requestId,
data
);The same infrastructure values then propagate through:
controller
↓
service
↓
repository
↓
modelThis creates several problems:
- tenant identity becomes application plumbing,
- transaction sessions depend on manual propagation,
- database selection spreads into business services,
- framework-specific request state leaks into domain APIs,
- execution rules become inconsistent between application paths.
Ambiten moves those concerns into the runtime.
Instead of requiring every layer to forward:
tenantId
session
dbName
requestIdthe execution boundary establishes runtime state once.
Model execution can then consume that state when required.
The Binding Model
Context binding is produced by several runtime layers working together.
Execution Boundary
↓
AmbitenContext
↓
AmbitenModel
↓
Effective Context
↓
Infrastructure Resolution
↓
AmbitenClient
↓
MongoDBExecution enters an Ambiten runtime boundary
Execution may begin from an HTTP request, GraphQL operation, Lambda invocation, scheduled job, queue consumer, teaching example, script, or explicit application task.
Adapter or explicit runtime helper establishes context
Ambiten establishes execution through a framework adapter, an explicit AmbitenContext.run(...) boundary, or another context-aware runtime helper.
AmbitenContext preserves active execution state
Runtime state is bound to the active execution surface
AmbitenModel merges active AmbitenContext state into an effective ModelContext. Direct AmbitenClient workflows may instead use explicit ModelContext, scoped clients, or client helpers that read ambient context.
await UserModel.find({})Effective operation state resolves persistence infrastructure
The model resolves its effective collection boundary while providers, MultiTenantManager, and AmbitenClient resolve the database, MongoDB client, tenant infrastructure, transaction session, and supported runtime overrides required by the operation.
MongoDB executes against the resolved operation scope
Persistence executes through MongoDB using the resolved database, collection, client, and transaction session produced by the active Ambiten execution path.
Each layer owns a distinct responsibility.
Execution Boundary
→ establishes execution
AmbitenContext
→ carries execution state
AmbitenModel
→ consumes and resolves effective context
Provider
→ resolves infrastructure bindings
MultiTenantManager
→ owns tenant infrastructure
AmbitenClient
→ bridges resolved infrastructure to MongoDBContext binding is therefore not simply:
Context → ClientIt is the process by which execution state becomes meaningful to an individual model operation.
Bound Execution State
An Ambiten execution boundary may carry values such as:
tenantId
requestId
dbName
collectionName
transaction session
debug state
logger metadata
custom metadataThose values may originate from different runtime entry mechanisms.
Adapter-Managed Execution
For supported frameworks:
Express
Fastify
NestJS
GraphQL
AWS Lambda
↓
Adapter
↓
Adapter Runtime
↓
AmbitenContextExplicit Execution
Outside adapter-managed environments:
await AmbitenContext.run(
{
tenantId: "tenant-a",
requestId: "job-123"
},
async () => {
await UserModel.find({});
}
);This is useful for:
workers
scheduled jobs
queue consumers
maintenance tasks
custom runtimesTransaction Execution
A transaction can add an active MongoDB session to the execution state:
await AmbitenContext.withTransaction(
async () => {
await UserModel.create(data);
}
);Context binding allows all of these execution styles to feed the same model runtime.
Context-Aware Execution
Consider an explicitly established execution boundary:
await AmbitenContext.run(
{
tenantId: "tenant-a",
requestId: "req-123"
},
async () => {
return UserModel.find({});
}
);The model call remains:
UserModel.find({});but the active execution contains:
{
tenantId: "tenant-a",
requestId: "req-123"
}The model does not need to rediscover where that tenant identity came from.
It simply participates in the active runtime.
Conceptually:
External Input
↓
TenantResolver / Explicit Context
↓
AmbitenContext
tenantId = tenant-a
↓
AmbitenModelThis is one of Ambiten's central architectural principles:
Static model definition. Dynamic runtime execution.
Effective Context Resolution
AmbitenModel does not simply copy the active AmbitenContext.
It resolves an effective operation context.
The general precedence model is:
explicit operation context
↓
active AmbitenContext
↓
model defaultsConceptually:
Operation Starts
↓
explicit values
↓
active execution state
↓
model defaults
↓
Effective ContextWhere the public API permits an explicit operation override, that value takes precedence over the corresponding active context or model default.
This makes model execution predictable while still allowing controlled operation-level customization.
Why Effective Context Matters
A model may be structurally configured with:
default collection
default database behavior
model-specific metadatawhile the active execution contributes:
tenantId
requestId
transaction sessionand an individual operation may provide a supported override.
Context binding combines those sources into one effective state for that operation.
The model definition itself does not need to mutate.
Shared Model Definition
│
├── Request A → Effective Context A
│
├── Request B → Effective Context B
│
└── Worker C → Effective Context CThis allows one model definition to serve many isolated executions.
Tenant Binding
Tenant binding begins before the model operation.
For adapter-managed requests:
Request
↓
TenantResolver
↓
tenantId
↓
AmbitenContextFor example:
x-tenant-id: tenant-amay resolve to:
AmbitenContext.get().tenantId;
// "tenant-a"The model then consumes that active identity as part of effective context resolution.
AmbitenContext
tenantId = tenant-a
↓
AmbitenModel
↓
Effective Context
tenantId = tenant-aThe model does not own the original tenant-resolution mechanism.
Tenant Identity vs Tenant Infrastructure
Context binding carries tenant identity.
It does not itself represent the tenant's MongoDB infrastructure.
This distinction is fundamental.
TenantResolver
→ Who is this execution for?
AmbitenContext
→ Which tenant belongs to this execution?
AmbitenModel
→ What effective context applies to this operation?
MultiTenantManager
→ What infrastructure belongs to that tenant?A bound context may contain:
tenantId = tenant-awithout containing:
MongoDB URI
MongoClient
database handle
connection stateThose resources remain infrastructure concerns.
Tenant-Aware Infrastructure Resolution
Once a model operation requires persistence, the effective tenant identity can be translated into infrastructure.
Conceptually:
AmbitenModel
↓
Effective Context
tenantId = tenant-a
↓
MultiTenantManager
↓
TenantConfig
↓
Tenant MongoClient
↓
Tenant DatabaseThis allows tenant-aware execution without forcing controllers or services to select databases manually.
Dynamic Tenant Binding
Context binding does not require every tenant to be registered during application startup.
For example, the active context may contain:
tenantId = tenant5while tenant5 is not currently registered.
Infrastructure resolution can then follow:
tenant5
↓
MultiTenantManager.resolveTenant()
↓
not registered
↓
TenantConfigResolver
↓
external lookup
↓
register tenant5
↓
getClient()The original model call remains unchanged:
await UserModel.find({});Context binding identifies which tenant belongs to the operation.
Dynamic tenant resolution determines how that tenant's infrastructure becomes available.
Provider-Driven Infrastructure Resolution
Providers remain part of the infrastructure-resolution model.
At execution time, a provider may consume effective operation context and resolve resources such as:
database
collection
client
session
runtime overridesConceptually:
const db =
await provider.db(ctx);For tenant-aware operations, provider resolution may cooperate with MultiTenantManager.
Effective Context
↓
Provider
↓
MultiTenantManager
↓
Tenant InfrastructureThe provider therefore consumes context.
It does not determine the original tenant identity of the request.
AmbitenClient Relationship
AmbitenClient participates after the required runtime infrastructure has been resolved.
Conceptually:
Effective Context
↓
Infrastructure Resolution
↓
AmbitenClient
↓
MongoDBBy this stage, the runtime should already understand the relevant:
client
database
collection
session
operation optionsAmbitenClient provides the MongoDB-facing bridge.
It should not be confused with request tenant resolution or tenant registry ownership.
Transaction Binding
Transaction continuity depends on the same context-binding mechanism.
For example:
await AmbitenContext.withTransaction(
async () => {
await UserModel.create(
data
);
await AuditLogModel.create(
log
);
}
);The active transaction session becomes part of the runtime execution state.
Conceptually:
Transaction Boundary
↓
session S1
↓
AmbitenContext
↓
UserModel
↓
session S1
↓
AuditLogModel
↓
session S1Participating Ambiten operations can resolve that session without manually passing it through service APIs.
Transaction Ownership
Context binding makes the transaction session available to participating operations.
It does not make each model operation responsible for transaction completion.
The surrounding transaction boundary owns:
commit
rollback
session lifecycleFor example:
withTransaction()
↓
Model A
↓
Model B
↓
callback resolves
↓
commitor:
withTransaction()
↓
Model A
↓
Model B fails
↓
callback rejects
↓
rollbackThis keeps transaction participation separate from transaction ownership.
Adapter-Managed Transaction Binding
Adapters may establish request-wide transaction-aware execution when configured.
Conceptually:
Request
↓
Adapter
↓
AmbitenContext
↓
Transaction Boundary
↓
Application
↓
Model OperationsFrom the model's perspective, the source of the transaction does not matter.
It consumes the active transaction state available through effective context.
Request Metadata Binding
Context binding is not limited to persistence routing.
The active execution may also carry:
requestId
debug metadata
logger metadata
custom execution metadataFor example:
const {
tenantId,
requestId
} = AmbitenContext.get();can provide runtime correlation information without requiring those values to appear in every service method signature.
This allows application services to remain focused on application data rather than execution plumbing.
Observability Context
Runtime instrumentation can consume the same execution context used by model operations.
Conceptually:
AmbitenContext
├── tenantId
├── requestId
├── database
├── collection
└── metadata
↓
instrumentationThis makes context-aware logging and telemetry possible across different execution environments.
The runtime provides the metadata required for correlation.
Delivery guarantees of a specific logging or telemetry backend remain part of that backend's own contract.
Middleware Binding
Middleware executes as part of the same model operation and can therefore observe its effective execution state.
Conceptually:
Effective Context
↓
before middleware
↓
persistence operation
↓
after middlewareThis allows policies such as:
validation
auditing
normalization
soft-delete behavior
logging
instrumentation
access shapingto operate consistently without requiring framework-specific context propagation.
Adapter-Managed Binding
For supported frameworks, the adapter establishes the execution boundary before downstream application logic runs.
Framework
↓
Adapter
↓
Adapter Runtime
↓
AmbitenContext
↓
Application
↓
AmbitenModelThis allows the same model to execute behind:
Express
Fastify
NestJS
GraphQL
AWS Lambdawithout requiring framework-specific model behavior.
Adapters therefore provide runtime ingress.
They do not change the model contract.
Explicit Binding Outside Requests
Not every execution begins inside a framework adapter.
Background work can establish context explicitly:
await AmbitenContext.run(
{
tenantId: "tenant-a",
requestId: "job-42"
},
async () => {
await UserModel.updateMany(
{},
{
$set: {
processed: true
}
}
);
}
);This pattern is appropriate for:
queue consumers
scheduled jobs
maintenance workflows
workers
internal toolingThe model remains unchanged.
Only the execution boundary is established differently.
Scoped Providers
Where scoped providers such as withTenant(...) are used, infrastructure can also be intentionally bound outside an adapter-managed request flow.
For example:
const tenantProvider =
client.withTenant(
"tenant-a"
);
const UserModel =
new AmbitenModel({
collectionName:
"users",
schema:
userSchema,
provider:
tenantProvider
});This represents an explicitly scoped infrastructure configuration.
It is useful when an application deliberately wants a model/provider relationship bound to a known tenant or execution environment.
It should not be confused with request tenant resolution.
For ordinary request-aware multi-tenant execution, AmbitenContext and MultiTenantManager remain the primary runtime path.
Context Binding Across Concurrent Executions
The same model definition can participate in multiple concurrent runtime boundaries.
Request A
tenantId = tenantA
↓
UserModel
↓
Effective Context A
Request B
tenantId = tenantB
↓
UserModel
↓
Effective Context BThe model definition is shared.
The execution context is not.
This is one of the key properties that allows Ambiten to support multi-tenant concurrency without storing request-specific state directly on shared model instances.
What Context Binding Prevents
A well-defined binding model reduces several common architectural failure modes.
Manual Tenant Propagation
Avoid:
service.execute(
tenantId,
data
);solely because the persistence layer needs tenant identity.
Manual Session Propagation
Avoid:
service.execute(
session,
data
);solely because nested model operations need the active transaction.
Framework Request Propagation
Avoid passing:
Express Request
Fastify Request
NestJS ExecutionContext
GraphQL Context
Lambda Eventthrough application layers just so a model can determine runtime infrastructure.
Shared Mutable Execution State
Avoid storing request-specific values in process-global or singleton mutable state.
Context binding gives those values an execution-scoped home.
What Context Binding Does Not Mean
Context binding does not mean every value in the runtime is immutable.
Supported operation-level context may override lower-precedence context or model defaults where the public API permits it.
Context binding also does not mean:
tenant resolution = authorizationor:
tenant identity = physical database topologyThose are separate concerns.
The runtime binds execution identity.
Infrastructure resolution interprets that identity according to configured rules.
Design Guidance
Context-driven model execution should remain the default pattern.
Prefer:
await UserModel.create(
data
);inside a valid execution boundary.
Avoid manually attaching runtime infrastructure to every model call merely because the runtime is already able to resolve it.
For example, do not routinely write:
await UserModel.create(
data,
{
tenantId,
session,
dbName
}
);when those values already belong to the active execution.
Explicit operation context should be used when the operation intentionally needs to override the normal runtime resolution rules.
Models Should Remain Framework-Independent
An AmbitenModel should not need to know whether execution originated from:
Express
Fastify
NestJS
GraphQL
Lambda
workerThe host environment establishes the boundary.
The context carries execution state.
The model resolves the effective operation context.
Infrastructure layers determine where persistence occurs.
This separation allows model behavior to remain stable across execution environments.
Runtime Relationship
The full relationship is:
Execution Boundary
↓
AmbitenContext
↓
AmbitenModel
↓
Effective Context
↓
Provider / MultiTenantManager
↓
AmbitenClient
↓
MongoDBThe model consumes execution state.
The infrastructure layer interprets it.
MongoDB performs persistence.
Mental Model
A useful way to think about context binding is:
The boundary establishes state.
The context carries state.
The model resolves effective state.
Infrastructure interprets state.
The client reaches MongoDB.Or, more compactly:
Context defines execution.
Model binds execution to an operation.
Infrastructure determines where it runs.Summary
Context binding is the mechanism that connects Ambiten's execution context to model operations without forcing runtime infrastructure through application APIs.
It allows model execution to:
- consume tenant identity from the active execution,
- preserve request metadata across supported async execution,
- participate in active transaction sessions,
- combine explicit operation context with runtime state and model defaults,
- resolve tenant infrastructure through
MultiTenantManager, - resolve database, collection, client, and session resources through runtime infrastructure,
- remain independent from the host framework.
The complete binding path is:
Execution Boundary
↓
AmbitenContext
↓
AmbitenModel
↓
Effective Context
↓
Infrastructure Resolution
↓
AmbitenClient
↓
MongoDBThe model remains structurally stable.
The context and infrastructure surrounding each operation can change dynamically.
Static model definition. Dynamic runtime execution.
