---
url: https://masyaka.github.io/injecute/guide/containers.md
description: Forks, isolated forks for tests, modules with extend(), and namespaces.
---

# Containers, forks and modules

## Forks

`fork()` creates a child container. The child sees every service of its parent, including ones the
parent registers later. What you add to the child stays in the child.

```ts
const app = new DIContainer()
  .addInstance('dbUrl', 'postgres://prod')
  .addSingleton('db', Database, ['dbUrl'])
  .addSingleton('users', UserRepository, ['db']);

// A fork sees every service of its parent; what it adds stays in the fork.
const requestScope = app.fork().addInstance('requestId', 'req-1');
requestScope.get('users') === app.get('users'); // true: parent singletons are shared
```

[Open in the playground](../playground?example=containers)

A service registered in the parent **runs in the parent** and is shared by all forks: the database pool
is created once, and what a fork adds stays in the fork. Parent services can't depend on what a fork
adds.

### Scopes

A fork created for one unit of work, such as a user session, a request, a transaction or a job, is a
**scope**: it registers the services that belong to that unit of work, uses the root's services, and is
disposed when the work is done. The direction is fixed: a scope's services may depend on the root's,
the root's never on a scope's.

* The compiler checks it: a root service can't name a key that only a scope registers.
* A scope that registers a key the root already has needs `replace: true`, and the error says which
  root services keep the root's instance (see below).
* Each feature with scoped services gives them their own module, applied to the scope in one place:
  see [Scoped state](./request-state.md#scopes), which also compares scopes with patterns that keep
  every service in the root.

### Overriding a parent's service in a fork

To register a key its parent has, a fork needs `{ replace: true }`, like a replacement in the same
container. The override reaches only the services resolved in the fork: a service registered in the
parent runs in the parent and keeps the parent's instance. Without `replace: true` the fork throws
`INJECUTE_ALREADY_REGISTERED`, and the message names the parent services that would keep the parent's
one:

```ts
// `users` (registered in `app`) depends on `db`: an override in a plain fork doesn't reach it.
const overridden = app
  .fork()
  .addSingleton('db', Database, { dependencies: ['dbUrl'], replace: true });
overridden.get('users') === app.get('users'); // true: `users` keeps the parent's `db`
// without { replace: true }: INJECUTE_ALREADY_REGISTERED,
// "users" (registered in a parent) depends on "db" and keeps the parent's "db"
```

To give a parent service the fork's instance, register that service in the fork too. To replace a
dependency for every service, use an isolated fork.

### Decorating a root service in a scope

A scope can decorate a root service for its own services, with a dependency on the key itself and
`replace: true`. Here the scope's services log with the request id, and root services keep the root's
logger:

```ts
class PrefixLogger {
  constructor(readonly prefix = '') {}
  child(prefix: string) {
    return new PrefixLogger(`${this.prefix}${prefix} `);
  }
}

const logged = new DIContainer()
  .addSingleton('logger', PrefixLogger)
  .addSingleton('audit', (logger) => ({ logger }), ['logger']); // a root service

// A scope decorates the root's logger for its own services: they log with the request id.
const loggedScope = logged
  .fork()
  .addInstance('requestId', 'req-7')
  .addSingleton('logger', (logger, id) => logger.child(id), {
    dependencies: ['logger', 'requestId'],
    replace: true,
  })
  .addSingleton('handler', (logger) => ({ logger }), ['logger']); // a scope service

loggedScope.get('handler').logger.prefix; // "req-7 ": the scope's services get the decorated logger
loggedScope.get('audit').logger.prefix; // "": root services keep the root's logger
```

## Isolated forks

`fork({ isolated: true })` runs **every** service it resolves inside the fork, including services
registered in its parents. Overrides then reach the whole graph, and nothing leaks back into the parent.

```ts
// An isolated fork runs every service it resolves itself, so overrides reach the whole graph
// and nothing leaks back into the parent.
const testScope = app
  .fork({ isolated: true })
  .addInstance('dbUrl', 'postgres://test', { replace: true });

testScope.get('users').db.url; // "postgres://test"
app.get('users').db.url; // "postgres://prod"
```

Use isolated forks for tests and for per-tenant variants of an app container. Namespaces are re-created
inside the fork, and forks of an isolated fork share its instances.

## Modules

A module is a plain function that registers services. It receives a
[`ServiceRegistry`](../concepts/roles.md): it can register, but not add middlewares, fork or dispose.
Declare only the services the module needs; `extend()` checks that the container has them.

```ts
// A module declares only the services it needs.
const addBilling = (c: ServiceRegistry<{ db: Database }>) =>
  c.addSingleton('invoices', (db) => ({ db, list: () => [] as string[] }), [
    'db',
  ]);

const withBilling = app.extend(addBilling); // type error if `db` were missing
withBilling.get('invoices');
```

A module only registers. Work that must run when the app starts (adding routes to another module's
router, subscribing to events, starting a consumer) goes in a
[lifecycle hook](./startup-shutdown.md), not in the module function. To let other modules contribute
to one of yours (routes, commands, health checks), give it an [extension point](./tags.md).

Keep each module in a wiring file of its own, next to the feature it wires. See
[Keep the wiring in files of its own](./app-structure.md#keep-the-wiring-in-files-of-its-own).

If a required service is missing, the error names it:

```text
Argument of type '(c: ServiceRegistry<{ db: Database; }>) => …' is not assignable to parameter of type
'{ 'injecute: extension requires services that are not registered': "db"; }'.
```

## Namespaces

`namespace(name, callback)` groups the services a callback registers under a prefix. The callback
receives a fork of the container, so it can use every service registered there.

```ts
const withNamespace = app.namespace('Reports', (reports) =>
  reports
    .addSingleton('daily', (users) => ({ users, kind: 'daily' }), ['users'])
    .addInstance('timezone', 'UTC'),
);

withNamespace.get('Reports.daily'); // { users, kind: 'daily' }
withNamespace.get('Reports').get('timezone'); // "UTC"
```

`get('Reports')` returns a read-only provider of the namespace. In the container's service map
(`ContainerServices<typeof app>`), `Reports` is a `Namespace` marker next to the `Reports.*` keys;
`NamespaceServices<typeof app, 'Reports'>` gives the namespace's services.

## Private services

A module often has services that only its own services use: a cache, a client, a retry policy. Register
them under a symbol key, and apply the module as a namespace. Don't export the symbol:

```ts
const cache = Symbol('cache'); // not exported: only this module has the key

const addCatalog = (c: ServiceRegistry<{ db: Database }>) =>
  c
    .addSingleton(cache, () => new Map<string, string>())
    .addSingleton('products', (db, prices) => ({ db, prices }), ['db', cache]);

const shop = new DIContainer()
  .addInstance('dbUrl', 'postgres://prod')
  .addSingleton('db', Database, ['dbUrl'])
  .namespace('Catalog', addCatalog);

shop.get('Catalog.products'); // resolves, with the private cache
shop.keys; // ['dbUrl', 'db', 'Catalog', 'Catalog.products']: no cache
// shop.get(cache); // type error, and INJECUTE_NOT_REGISTERED at runtime
```

* **What makes them private.** A namespace adds its string and number keys to the container as
  `Name.key`, but not its symbol keys. Inside the namespace, services depend on the symbol like on any
  key. Outside it, `get()` with the symbol is a compile error and fails with
  [`INJECUTE_NOT_REGISTERED`](../errors/not-registered.md), and `keys` doesn't list it.
* **Tests still reach them.** Replace a dependency of the module in an
  [isolated fork](#isolated-forks), and its private services use the replacement too. To replace a
  private service itself, the test needs the symbol: replace the public dependencies instead, or export
  the symbol from a file only tests import.
* **Only with `namespace()`.** With `extend()`, a symbol key is registered in the container itself.
  Then only the unexported symbol keeps others out, and `keys` lists it.
* **Not a security boundary.** `get('Catalog')` returns a provider of the namespace for inspection,
  and its `keys` include the symbols. Code that enumerates them can resolve them, as code can read
  symbol properties of an object with `Object.getOwnPropertySymbols()`.

A naming convention such as `_cache` doesn't do this: keys are names
([principle 8](../concepts/principles.md#_8-keys-are-names)), so `Catalog._cache` is linked and resolved
like any other key.

## Adapting a module's dependencies

A module names the services it needs under its own keys. When the container has them under other
keys, or in another shape, register the missing keys before `extend()`: `addAlias()` for the same
service under another key, a factory for a value derived from another service. Do it in a namespace,
so the adapters stay out of the container's root:

```ts
interface Logger {
  info(message: string): void;
}

// The module declares the services it needs under its own keys.
const addAudit = (c: ServiceRegistry<{ dbUrl: string; log: Logger }>) =>
  c.addSingleton('audit', (dbUrl, log) => ({ dbUrl, log }), ['dbUrl', 'log']);

// The host has them under other keys, or in another shape.
const host = new DIContainer()
  .addInstance('config', { auditDb: 'postgres://audit' })
  .addInstance('logger', { info: () => {} } as Logger)
  .namespace('Audit', (audit) =>
    audit
      .addSingleton('dbUrl', (config) => config.auditDb, ['config']) // derive a value
      .addAlias('log', 'logger') // the same service under another key
      .extend(addAudit),
  );

host.get('Audit.audit').dbUrl; // "postgres://audit"
host.has('dbUrl'); // false: the adapters stay in the namespace
```

* The adapters are services of the namespace like any other: `host.get('Audit.dbUrl')` resolves them,
  and `fork({ isolated: true })` overrides of `config` reach them.
* An adapter has a lifetime too. A singleton keeps the first value it derives; register it with
  `addTransient()` when the value must follow a service that changes.
* `extend()` on the container itself works without a namespace, but the adapters then become keys of
  the root, and two modules that need `dbUrl` with different values can't both have it.

## Sealing the composition root

`seal()` ends the registrations of a container: afterwards `add*`, `namespace()` and `extend()` throw
`INJECUTE_SEALED`, and its type has no registration methods. Resolving, middlewares, events, `reset()`
and `dispose()` keep working, and forks are open, so request scopes and tests fork the sealed root.

```ts
// Register everything, then seal the composition root.
const root = new DIContainer()
  .addInstance('dbUrl', 'postgres://prod')
  .addSingleton('db', Database, ['dbUrl'])
  .addSingleton('users', UserRepository, ['db'])
  .seal();

root.get('users'); // resolves as before
// root.addInstance('flag', true); // type error, and INJECUTE_SEALED at runtime

// Per request or per test, fork it: forks are open for registrations.
const perRequest = root.fork().addInstance('requestId', 'req-2');
```

A sealed container's type is also cheaper for TypeScript: its service map is one object instead of an
intersection of every registration, so forks and request scopes built on it typecheck faster and show
readable types (see [TypeScript](./typescript.md#seal-the-composition-root)).

## Which one to use

| You want to…                                                       | Use                                                          |
| ------------------------------------------------------------------ | ------------------------------------------------------------ |
| give services the trace id, tenant or user of the current request  | a [context accessor](./request-context.md)                   |
| register services for one session, request, transaction or job     | a [scope](#scopes): a fork with a scope module               |
| choose between a scope and patterns that keep services in the root | [Scoped state](./request-state.md)                           |
| replace a dependency for everything, without touching the original | `fork({ isolated: true })`                                   |
| split registrations by feature                                     | modules with `extend()`                                      |
| group services under a name, e.g. `Billing.*`                      | `namespace()`                                                |
| keep a module's helper services out of reach of other modules      | [symbol keys in a namespace](#private-services)              |
| apply a module that names its dependencies differently             | [adapters in a namespace](#adapting-a-module-s-dependencies) |
