Runtime Execution Flow
This document describes how Ambiten executes an operation from entry to persistence.
It connects the major components of the runtime into one execution path so you can understand how execution context, tenant identity, models, infrastructure resolution, clients, transactions, and MongoDB interact.
The host environment may change.
The underlying runtime path remains consistent.
Core Idea
An Ambiten execution follows a context-driven sequence:
Conceptually:
Execution Entry
↓
Execution Boundary
↓
AmbitenContext
↓
Application Logic
↓
AmbitenModel
↓
Infrastructure Resolution
↓
AmbitenClient
↓
MongoDBFor tenant-aware execution, infrastructure resolution may additionally involve:
AmbitenContext.tenantId
↓
MultiTenantManager
↓
Tenant Configuration
↓
Tenant MongoDB Client
↓
Tenant DatabaseEach layer owns a distinct responsibility.
Execution moves through those layers without requiring application code to manually propagate runtime infrastructure state.
Execution Boundary
Before examining the individual stages, it is important to distinguish two ways execution can enter Ambiten.
Adapter-Managed Execution
Supported frameworks establish the execution boundary through their adapters.
Express
Fastify
NestJS
GraphQL
AWS Lambda
↓
Framework Adapter
↓
Adapter Runtime
↓
AmbitenContextExplicit Execution
Execution without a framework adapter establishes context directly.
For example:
await AmbitenContext.run(
{
tenantId: "tenant5",
requestId: "job-123"
},
async () => {
await UserModel.find({});
}
);This is appropriate for:
- background workers,
- queue consumers,
- scheduled jobs,
- migrations,
- maintenance tools,
- custom execution environments.
The common rule is:
Every runtime-aware operation
must execute inside
an Ambiten execution boundary.How that boundary is established depends on the entry environment.
Execution Lifecycle
1. Ingress
Execution begins at an external entry point.
Examples include:
HTTP request
GraphQL operation
NestJS controller request
Lambda invocation
queue message
scheduled job
worker task
CLI operationFor supported frameworks, the corresponding adapter owns the ingress boundary.
For non-adapter execution, the application establishes AmbitenContext explicitly.
Conceptually:
External Work
↓
Adapter
or
Explicit Context
↓
Ambiten RuntimeIngress identifies the beginning of one logical execution.
2. Adapter Normalization
For adapter-managed execution, the framework-specific adapter translates its host request or invocation into the normalized request representation understood by @ambiten/adapter-runtime.
Conceptually:
Express Request
Fastify Request
NestJS Request
GraphQL Request
Lambda Event
↓
Framework Adapter
↓
AmbitenRequestLikeThe normalized boundary can expose information such as:
headers
URL
HTTP method
route parameters
cookies
query parameters
body
request metadataThe exact source depends on the host environment.
Core does not need to understand framework-specific request types.
3. Tenant and Metadata Resolution
The adapter runtime resolves execution-scoped information before application logic begins.
For example:
await adapter.install(app, {
tenancy: {
header: "x-tenant-id"
},
requestIdHeader:
"x-request-id"
});A request:
GET /users
x-tenant-id: tenant5
x-request-id: req-123may resolve to:
{
tenantId: "tenant5",
requestId: "req-123"
}The important distinction is:
TenantResolver
→ resolves execution identity
MultiTenantManager
→ resolves tenant infrastructureThe adapter resolves who the execution belongs to.
It does not need to know how the runtime reaches that tenant's database.
4. Tenant Validation
When tenancy validation is configured, the resolved identity can be checked before downstream execution begins.
For example:
validate: async (tenantId) => {
const tenant =
await MultiTenantManager
.resolveTenant(tenantId);
if (!tenant) {
throw new Error(
`Tenant with ID "${tenantId}" not found.`
);
}
return true;
}The flow becomes:
tenantId
↓
validation
↓
valid?
┌──┴──┐
yes no
↓ ↓
continue rejectValidation may include dynamic tenant discovery.
The tenant therefore does not need to have been registered during application startup.
5. Context Initialization
After execution metadata has been resolved, Ambiten establishes the active runtime context.
Conceptually:
AmbitenContext.run(
{
tenantId,
requestId,
dbName,
collectionName,
debug,
loggerMeta,
meta
},
handler
);This creates the execution-scoped runtime boundary.
The context may carry values such as:
tenantId
requestId
database override
collection override
debug state
logger metadata
custom metadata
transaction sessionThese values belong to the active execution.
They are not process-global state.
Context Resolution Model
Runtime-aware components resolve execution state from the active AmbitenContext rather than requiring that information to be manually passed through every application function.
Instead of:
await service.createUser(
tenantId,
requestId,
session,
data
);application code can remain focused on domain input:
await service.createUser(
data
);while infrastructure-aware components can resolve:
const context =
AmbitenContext.get();when runtime state is required.
This keeps execution state available without coupling domain APIs to infrastructure propagation.
6. Framework-Specific Execution Continuity
Different frameworks enter the runtime differently.
Express
Express middleware
↓
AmbitenContext
↓
downstream middleware/routesFastify
Fastify lifecycle
↓
AmbitenContext
↓
route handlerNestJS
NestJS interceptor
↓
AmbitenContext
↓
Observable subscription
↓
controller
↓
serviceFor NestJS, the actual downstream Observable execution remains inside the Ambiten boundary rather than only creating the Observable inside it.
GraphQL
GraphQL context factory
↓
AmbitenContext
↓
resolver executionAWS Lambda
Lambda invocation
↓
adapter wrapper
↓
fresh AmbitenContext
↓
handlerThe integration mechanism changes.
The execution contract remains the same.
7. Application Execution
Controllers, handlers, resolvers, services, or workers now execute inside the active runtime boundary.
For example:
await UserModel.find({});Application logic does not need to manually forward:
tenantId
requestId
MongoDB URI
database name
transaction session
tenant clientthrough every layer.
The active runtime boundary already carries the execution-scoped information required downstream.
8. Async Context Continuity
Execution context remains associated with asynchronous operations belonging to the active scope.
For example:
const before =
AmbitenContext
.get()
.tenantId;
await someAsyncOperation();
const after =
AmbitenContext
.get()
.tenantId;For one execution:
before === after;Conceptually:
Controller
↓
Service
↓
await
↓
Repository
↓
ModelThe execution remains associated with the same runtime context throughout that asynchronous chain.
9. Model Execution
When execution reaches AmbitenModel, the operation enters the model execution pipeline.
For example:
await UserModel.find({});or:
await UserModel.create({
username: "Alice"
});The model coordinates behavior such as:
schema validation
operation preparation
middleware
execution metadata
context resolution
collection resolution
infrastructure selection
result processingThe model does not need an Express request, Fastify request, NestJS ExecutionContext, GraphQL context object, or Lambda event.
It operates against Ambiten's runtime abstractions.
10. Model Context Resolution
Before resolving persistence infrastructure, the model determines the effective operation context.
Conceptually, runtime values follow this precedence:
explicit operation context
↓
active AmbitenContext
↓
model defaultsThis allows explicit operation-level overrides where supported while preserving the request-scoped execution state established by the runtime.
For tenant-aware operations:
AmbitenContext.get().tenantId;
// "tenant5"becomes part of the infrastructure-resolution decision.
11. Infrastructure Resolution
The model now resolves the database and collection required for the operation.
Conceptually:
Model Operation
↓
Effective Context
↓
Provider / Client Resolution
↓
Database
↓
CollectionFor non-tenant execution, this may resolve against the configured application database.
For tenant-aware execution:
AmbitenContext
tenantId = tenant5
↓
MultiTenantManager
↓
tenant5 configuration
↓
tenant5 client
↓
db_tenant5This is where execution identity becomes concrete infrastructure selection.
Provider Resolution
Where provider abstractions participate in model execution, they resolve infrastructure bindings from the active operation context.
Conceptually:
const db =
await provider.db(ctx);The provider layer can participate in decisions such as:
database selection
collection resolution
session resolution
runtime overrides
client selectionProvider resolution does not determine the request tenant identity.
It consumes the execution state already established upstream.
12. Multi-Tenant Resolution
When the effective context contains a tenant ID, Ambiten can resolve tenant infrastructure through MultiTenantManager.
Conceptually:
tenantId = tenant5
↓
MultiTenantManager.getTenant()
↓
registered?
┌────┴────┐
yes no
↓ ↓
use resolveTenant()
config ↓
TenantConfigResolver
↓
register tenantIf database access requires a live client:
tenant configuration
↓
getClient()
↓
connected clientThe distinction remains:
getTenant()
→ local registry lookup
resolveTenant()
→ registry + dynamic discovery
getClient()
→ resolution + usable client13. Dynamic Tenant Discovery
If the tenant does not exist in the local runtime registry and a TenantConfigResolver is configured, Ambiten can discover it dynamically.
tenant5
↓
not locally registered
↓
TenantConfigResolver
↓
external tenant source
↓
TenantConfig
↓
register tenant5The source may be application-defined infrastructure such as:
control database
tenant registry
configuration service
account service
infrastructure APIApplication model code remains unchanged.
The model does not need to know whether a tenant was static or dynamic.
14. Lazy Client Activation
Tenant registration and connection establishment are separate lifecycle stages.
A tenant may be:
registered
+
lazy
+
not connecteduntil an operation requires database access.
The lifecycle can therefore be:
tenant registered
↓
lazy
↓
model requires database
↓
MultiTenantManager.getClient()
↓
MongoDB client established
↓
tenant activeThis allows runtime resource usage to follow actual tenant demand.
15. Client Execution
After infrastructure resolution, AmbitenClient participates in the MongoDB execution path.
At this stage, the runtime has resolved the required:
client
database
collection
session
operation optionsThe finalized operation can then be expressed through MongoDB driver behavior.
Conceptually:
db
.collection("users")
.find(...);The execution has now moved from runtime coordination into persistence execution.
16. Transaction Session Resolution
If the active execution is inside a transaction boundary, the corresponding transaction session remains associated with that execution.
Conceptually:
AmbitenContext
↓
transaction session
↓
Model A
↓
Model B
↓
Model CNested model operations can participate in the same active session without manually passing the session through every application layer.
The transaction lifecycle belongs to the enclosing transaction boundary.
It does not commit or roll back independently after every model operation.
17. Persistence
The MongoDB driver executes the finalized operation against the resolved target.
For a tenant-aware request:
tenant5
↓
db_tenant5
↓
users collection
↓
MongoDB operationAmbiten maintains a high-fidelity relationship with MongoDB rather than attempting to hide the underlying database semantics behind an unrelated persistence model.
MongoDB behavior therefore remains relevant to:
queries
indexes
sessions
transactions
aggregations
write concerns
read behavior
connection management18. Model Post-Processing
After the MongoDB operation completes, control returns through the model execution pipeline.
Depending on the operation and configured runtime behavior, this may include:
post-middleware
result transformation
instrumentation finalization
metrics
logging
operation cleanupThe model operation then returns its result to the application layer.
Conceptually:
MongoDB
↓
Result
↓
Post-Middleware
↓
Instrumentation
↓
Model Result
↓
Application19. Transaction Completion
Transaction completion occurs at the boundary that created the transaction.
For explicit transactions:
await AmbitenContext.withTransaction(
async () => {
await UserModel.create(...);
await AuditModel.create(...);
}
);the lifecycle is:
withTransaction()
↓
Model A
↓
Model B
↓
callback succeeds
↓
commitor:
withTransaction()
↓
Model A
↓
Model B fails
↓
callback rejects
↓
rollbackFor adapter-managed request-wide transactions:
Adapter
↓
Transaction Boundary
↓
entire downstream execution
↓
execution completes
↓
commit / rollbackTransaction completion therefore belongs to the enclosing execution boundary rather than an individual model's post-processing stage.
20. Egress
Once application execution finishes, control exits the Ambiten execution boundary.
For adapter-managed execution:
Application Result
↓
Adapter Runtime
↓
Framework
↓
Response / CompletionFor explicit execution:
AmbitenContext.run(...)
↓
callback completes
↓
control returns to callerThe active execution context no longer applies once control has left that boundary.
Long-lived runtime infrastructure remains available.
This distinction is important:
Execution context
→ scoped to one request/job/invocation
Runtime infrastructure
→ may outlive many executionsVisual Flow
A request, operation, invocation, job, or event enters the host environment.
A framework adapter normalizes execution and resolves identity, or an explicit runtime boundary is established.
Execution-scoped state such as tenant identity, request metadata, and transaction state becomes active.
Handlers, controllers, resolvers, services, or workers execute inside the active runtime boundary.
Validation, middleware, effective context, and operation coordination are applied.
Providers and MultiTenantManager resolve the client, tenant, database, collection, session, and runtime overrides required by the operation.
The finalized runtime operation is bridged to the MongoDB driver using the resolved infrastructure.
Persistence executes against the selected tenant, database, collection, and active transaction session.
Results return through post-processing and the enclosing transaction and execution boundary complete according to the outcome.
End-to-End Example
Consider an Express route:
app.get(
"/users",
async (_req, res) => {
const users =
await UserModel.find({});
res.json(users);
}
);with:
GET /users
x-tenant-id: tenant5The internal execution can be understood as:
HTTP Request
↓
Express Adapter
↓
normalize request
↓
resolve tenant5
↓
validate tenant5
↓
AmbitenContext
tenantId = tenant5
↓
route handler
↓
UserModel.find(...)
↓
effective model context
↓
MultiTenantManager
↓
tenant5 configuration
↓
getClient("tenant5")
↓
db_tenant5
↓
users collection
↓
MongoDB find
↓
model post-processing
↓
route result
↓
Express responseIf tenant5 is not yet registered, dynamic discovery may occur between:
MultiTenantManagerand:
tenant5 configurationwithout requiring any change to the route or model call.
Background Job Example
A worker follows the same model but establishes execution explicitly.
await AmbitenContext.run(
{
tenantId: "tenant5",
requestId: "job-123"
},
async () => {
await UserModel.updateMany(
{},
{
$set: {
processed: true
}
}
);
}
);Internally:
Worker
↓
AmbitenContext.run(...)
↓
tenantId = tenant5
↓
UserModel.updateMany(...)
↓
MultiTenantManager
↓
db_tenant5
↓
MongoDBThe ingress mechanism changed.
The runtime execution model did not.
Key Properties of the Flow
Context-Driven Execution
Execution state is resolved from the active runtime boundary rather than manually propagated through application code.
execution boundary
↓
AmbitenContext
↓
runtime-aware componentsThis allows asynchronous application code to remain context-aware without making every business API infrastructure-aware.
Execution Isolation
Each request, operation, job, or invocation receives its own execution scope.
Execution A
→ Context A
Execution B
→ Context BLong-lived resources may be reused.
Execution-specific state remains isolated.
Layered Responsibility
Each runtime layer owns a distinct concern.
Adapter
→ ingress
Adapter Runtime
→ execution setup
AmbitenContext
→ execution state
AmbitenModel
→ operation lifecycle
Provider
→ infrastructure binding abstraction
MultiTenantManager
→ tenant registry and client lifecycle
AmbitenClient
→ database infrastructure access
MongoDB
→ persistenceThis prevents transport, execution state, tenant infrastructure, and persistence concerns from collapsing into one layer.
Runtime Portability
The execution model remains consistent across:
Express
Fastify
NestJS
GraphQL
AWS Lambda
workers
scheduled jobs
queue consumersThe entry mechanism changes.
The runtime semantics downstream remain stable.
Tenant-Aware Resolution
Tenant identity is resolved before application execution and later translated into tenant infrastructure when persistence requires it.
request identity
↓
tenantId
↓
AmbitenContext
↓
MultiTenantManager
↓
tenant databaseThis keeps request resolution separate from infrastructure ownership.
Dynamic Infrastructure Resolution
Tenant infrastructure does not need to be completely known at startup.
Unknown tenants can be discovered through TenantConfigResolver and added to the runtime registry when required.
unknown tenant
↓
dynamic discovery
↓
registration
↓
lazy connection
↓
database operationTransaction Continuity
Transaction state belongs to the active execution boundary.
Model operations executed inside that boundary can participate in the same transaction session.
Transaction
↓
Service
↓
Model A
↓
Model BCommit or rollback occurs when the enclosing transaction scope completes.
Deterministic Resolution
For a given execution context and configured resolution rules, runtime infrastructure selection remains stable throughout that execution.
For example:
tenantId = tenant5should consistently route tenant-aware operations to the infrastructure associated with tenant5.
Explicit operation-level overrides may alter resolution where the public API permits them.
The runtime does not silently switch tenant identity during an active execution.
Async Continuity
Ambiten preserves runtime context through supported asynchronous execution belonging to the active scope.
This allows:
controller
→ service
→ await
→ repository
→ modelto remain associated with the same execution state.
Cross-process, network, queue, or future detached boundaries require explicit propagation and reconstruction of the required identity.
Package-Boundary Consistency
Framework adapters, adapter-runtime, and Core participate in a compatible package graph.
For ESM:
ESM Application
↓
ESM Adapter
↓
ESM Adapter Runtime
↓
ESM CoreFor CommonJS:
CommonJS Application
↓
CommonJS Adapter
↓
CommonJS Adapter Runtime
↓
CommonJS CoreThis matters because context-aware execution depends on all participating packages using the compatible runtime context boundary.
Why This Matters
Ambiten's execution architecture removes infrastructure plumbing from application code without removing infrastructure discipline.
Application code can remain focused on:
await UserModel.find({});while the runtime coordinates:
execution identity
tenant identity
request metadata
async context
transaction session
tenant configuration
client resolution
database selection
collection selection
middleware
instrumentation
persistenceThe architectural goal is not to hide those concerns.
It is to place them in the runtime layers that own them.
This allows tenant isolation, transaction continuity, framework portability, dynamic infrastructure resolution, and operational observability to remain consistent without forcing those concerns into every controller, resolver, service, or worker.
Mental Model
The application defines the operation.
The context defines the execution.
The runtime resolves the infrastructure.
The model coordinates the operation.
The client reaches the database.Or, in one flow:
Entry
↓
Context
↓
Application
↓
Model
↓
Infrastructure Resolution
↓
Client
↓
MongoDB