Service Resolution
Introduction
Section titled “Introduction”If you have already registered a service and need to use it, Xeno.JS provides resolve() on the service container, AppBuilder, and IServiceScope.
The method is type-safe: the service token determines the type returned by the registry.
Use:
AppBuilder.resolve()when resolving an application service from the composition root.IServiceContainer.resolve()when you already have the configured container.IServiceScope.resolve()when the service is scoped.ContainerUtils.resolveServiceScoped()when resolving a scoped service from an integration or transport boundary.
Before you start
Section titled “Before you start”You need:
- A service registered in the container.
- The service token used by that registration.
- An active scope if the service uses the
scopedlifetime.
For example:
const app = new AppBuilder().addServices((services) => { services.addSingleton('USER_SERVICE', () => { return new UserService() })})
await app.build()The service can then be resolved with the same token:
const userService = app.resolve('USER_SERVICE')Resolve a service with AppBuilder
Section titled “Resolve a service with AppBuilder”AppBuilder exposes:
resolve<K extends keyof TRegistry>(token: K): TRegistry[K]This is the simplest option when your code already has access to the application builder.
const app = new AppBuilder().addServices((services) => { services.addSingleton('USER_SERVICE', () => { return new UserService() })})
await app.build()
const userService = app.resolve('USER_SERVICE')
await userService.findById('123')AppBuilder.resolve() delegates resolution to the configured service container.
When to use it
Section titled “When to use it”Use app.resolve() when resolving services from the application composition layer or other code that intentionally has access to the application builder.
For request-scoped application code, prefer resolving through the active scope instead.
Resolve a service from IServiceContainer
Section titled “Resolve a service from IServiceContainer”After building the application, build() returns the configured IServiceContainer.
const app = new AppBuilder().addServices((services) => { services.addSingleton('USER_SERVICE', () => { return new UserService() })})
const container = await app.build()
const userService = container.resolve('USER_SERVICE')This is useful when a component receives the container explicitly:
function createApplication(container: IServiceContainer) { const userService = container.resolve('USER_SERVICE')
return userService}The container’s resolve() method uses the registry to associate the token with its service type.
Resolve a service from a scope
Section titled “Resolve a service from a scope”Scoped services cannot be resolved directly from the root container.
Register the service as scoped:
const app = new AppBuilder().addServices((services) => { services.addScoped('REQUEST_SERVICE', () => { return new RequestService() })})
const container = await app.build()This does not work:
const requestService = container.resolve('REQUEST_SERVICE')Xeno.JS throws:
[DI Container Error]: Scoped service 'REQUEST_SERVICE' requires an active scope.Create a scope and resolve the service through it instead:
const scope = container.createScope()
try { const requestService = scope.resolve('REQUEST_SERVICE')
await requestService.execute()} finally { await scope.dispose()}The scope provides the resolution boundary required by scoped services.
Resolve dependencies inside a service factory
Section titled “Resolve dependencies inside a service factory”Service factories receive the current IServiceScope.
This allows a service to explicitly resolve its dependencies:
services.addScoped('USER_REPOSITORY', (scope) => { return new UserRepository( scope.resolve('USER_DATA_SOURCE'), scope.resolve('USER_MAPPER'), )})The dependency graph is therefore explicit in the registration:
USER_REPOSITORY├── USER_DATA_SOURCE└── USER_MAPPERYou do not need decorators or runtime dependency scanning.
Resolve different service lifetimes
Section titled “Resolve different service lifetimes”The result of resolve() depends on the registered lifetime.
Singleton
Section titled “Singleton”A singleton returns the same instance from the same container:
const first = container.resolve('CONFIGURATION_SERVICE')const second = container.resolve('CONFIGURATION_SERVICE')
console.log(first === second)// trueScoped
Section titled “Scoped”A scoped service returns the same instance within one scope:
const scope = container.createScope()
try { const first = scope.resolve('REQUEST_SERVICE') const second = scope.resolve('REQUEST_SERVICE')
console.log(first === second) // true} finally { await scope.dispose()}A different scope has its own scoped instance:
const firstScope = container.createScope()const secondScope = container.createScope()
try { const first = firstScope.resolve('REQUEST_SERVICE') const second = secondScope.resolve('REQUEST_SERVICE')
console.log(first === second) // false} finally { await firstScope.dispose() await secondScope.dispose()}Transient
Section titled “Transient”A transient service creates a new instance for every resolution:
const first = container.resolve('USER_FACTORY')const second = container.resolve('USER_FACTORY')
console.log(first === second)// falseSee Transient for details.
Resolve a scoped service from the current request
Section titled “Resolve a scoped service from the current request”Xeno.JS creates a service scope for request execution.
When application code runs inside that request scope, scoped services can be resolved through the current IServiceScope.
If you need to access the current scope explicitly, Xeno.JS exposes the IServiceScopeAccessor through the application registry.
const scopeAccessor = container.resolve('SERVICE_SCOPE_ACCESSOR')const scope = scopeAccessor.getScope()
if (!scope) { throw new Error('An active service scope is required.')}
const requestService = scope.resolve('REQUEST_SERVICE')The scope is available only while the request execution is active.
After request execution finishes, the request scope is disposed.
Resolve a scoped service with ContainerUtils
Section titled “Resolve a scoped service with ContainerUtils”When code sits at a transport or integration boundary, ContainerUtils.resolveServiceScoped() provides a convenient way to resolve a service through the current active scope.
import { ContainerUtils } from '@xeno-js/core'
const controller = ContainerUtils.resolveServiceScoped( 'FIND_USER_CONTROLLER', container,)The utility obtains the current service scope and resolves the requested token through that scope.
This is particularly useful in transport adapters:
fastify.get('/users/:id', async (req, reply) => { const controller = ContainerUtils.resolveServiceScoped( 'FIND_USER_CONTROLLER', container, )
const result = await controller.handle(req.params.id)
return reply.send(result)})An active service scope is required.
If no scope is available, Xeno.JS throws:
Active service scope is required to execute Scoped service.Use this approach when the transport layer needs to obtain a request-scoped application component without manually creating and managing the request scope.
Resolve built-in Xeno.JS services
Section titled “Resolve built-in Xeno.JS services”Xeno.JS also registers services identified by its built-in registry tokens.
For example:
const mediator = container.resolve('MEDIATOR')const logger = container.resolve('LOGGER')const requestContext = container.resolve('REQUEST_CONTEXT')The registry associates each token with its corresponding service type.
For example, MEDIATOR resolves to the mediator contract, while REQUEST_CONTEXT resolves to the request-context contract.
This means TypeScript can infer the result of the resolution:
const mediator = container.resolve('MEDIATOR')
await mediator.send(command)No manual type cast is required.
Resolve a service by its custom token
Section titled “Resolve a service by its custom token”Custom services can extend the application registry so their tokens remain type-safe.
For example:
interface UserServiceRegistry { USER_SERVICE: UserService}Your application registry can then include the custom token:
type AppRegistry = XenoRegistry< Dictionary, UserServiceRegistry>The exact registry composition depends on how your application defines its custom services.
Once the token is part of the registry, the same resolution API applies:
const userService = container.resolve('USER_SERVICE')The returned value is typed as UserService.
Common problems
Section titled “Common problems”Registration not found
Section titled “Registration not found”If you resolve a token that has not been registered:
const userService = container.resolve('USER_SERVICE')Xeno.JS throws:
[DI Container Error]: Registration not found for token 'USER_SERVICE'. Ensure the service is registered before resolving.Check that:
- The service is registered.
- The registration runs before resolution.
- The token is spelled exactly the same in registration and resolution.
- The module containing the registration has been configured before the service is resolved.
For application-level registrations, prefer configuring them through addServices():
const app = new AppBuilder().addServices((services) => { services.addScoped('USER_SERVICE', () => { return new UserService() })})
const container = await app.build()Scoped service requires an active scope
Section titled “Scoped service requires an active scope”If you see:
[DI Container Error]: Scoped service 'USER_SERVICE' requires an active scope.the service was resolved from the root container even though it was registered as scoped.
Instead of:
container.resolve('USER_SERVICE')use:
const scope = container.createScope()
try { const userService = scope.resolve('USER_SERVICE')} finally { await scope.dispose()}Or, when already inside request execution, resolve it through the active request scope.
Scope has been closed
Section titled “Scope has been closed”A disposed scope cannot be used for further resolution.
const scope = container.createScope()
await scope.dispose()
scope.resolve('USER_SERVICE')This throws:
[DI Container Error]: Unable to resolve 'USER_SERVICE'. The scope has been closed.Do not retain a scoped service or scope beyond the lifetime in which it is valid.
Duplicate registration
Section titled “Duplicate registration”A token can only be registered once in a container.
For example:
services.addSingleton('USER_SERVICE', () => new UserService())
services.addScoped('USER_SERVICE', () => new UserService())throws:
[DI Container Error]: The token 'USER_SERVICE' is already registered in the container.Choose one registration for the token or use a different token.
Complete example
Section titled “Complete example”The following example registers a repository and a service, then resolves the service from a request scope.
import { AppBuilder } from '@xeno-js/core'
const app = new AppBuilder().addServices((services) => { services.addScoped('USER_REPOSITORY', (scope) => { return new UserRepository( scope.resolve('USER_DATA_SOURCE'), ) })
services.addScoped('USER_SERVICE', (scope) => { return new UserService( scope.resolve('USER_REPOSITORY'), ) })})
const container = await app.build()
const scope = container.createScope()
try { const userService = scope.resolve('USER_SERVICE')
const user = await userService.findById('123')
console.log(user)} finally { await scope.dispose()}The important part is that the resolution follows the lifetime boundary:
container | +-- createScope() | +-- USER_SERVICE | +-- USER_REPOSITORY | +-- USER_DATA_SOURCEThe services registered as scoped are resolved within the same scope.
Choosing the resolution API
Section titled “Choosing the resolution API”| Situation | Use |
|---|---|
| Resolve a root-level singleton or transient | container.resolve(token) |
| Resolve from the application builder | app.resolve(token) |
| Resolve a scoped service | scope.resolve(token) |
| Resolve a scoped service at a transport/integration boundary | ContainerUtils.resolveServiceScoped(token, container) |
| Resolve a dependency inside a registration factory | scope.resolve(token) |
The key rule is simple:
Resolve scoped services through an active scope.
Checklist
Section titled “Checklist”Before resolving a service, verify:
- The service has been registered.
- You are using the same token used during registration.
- The application has been built when using
AppBuilder. - You are using an active scope for scoped services.
- You are not resolving a service from a disposed scope.
- The custom token is included in the application registry when using custom services.
Related docs
Section titled “Related docs”- Service Container — understand the container used to register and resolve services.
- Registration — register services with Xeno.JS.
- Singleton — register services with one shared instance.
- Scoped — register services with one instance per scope.
- Transient — create a new instance on every resolution.
- Dependency Graph — understand how registered services depend on each other.
- Captive Dependencies — troubleshoot invalid lifetime dependencies.
Support Us
Section titled “Support Us”Xeno.JS is an MIT-licensed open source project. It can grow thanks to the support of these awesome people. If you’d like to join them, please read more at support section
