---
url: https://masyaka.github.io/injecute/guide/recipes.md
description: >-
  A unit of work per operation, a scope, configuration-driven implementations,
  feature modules, a module's private services, an SDK for other teams, sharing
  services between containers.
---

# Recipes

## A unit of work per operation

A service creates what one operation owns with a factory, and `using` releases it:

```ts
/** The repositories of one business operation, bound to one transaction. */
class UnitOfWork {
  constructor(private readonly tx: Transaction) {}
  insertOrder(item: string): void {
    this.tx.query(`insert ${item}`);
  }
  commit(): void {
    this.tx.commit();
  }
  [Symbol.dispose](): void {
    this.tx[Symbol.dispose](); // rolls back unless committed
  }
}

class Checkout {
  constructor(private readonly work: { begin(): UnitOfWork }) {}

  placeOrder(item: string, quantity: number): void {
    // the unit of work lives in this block and is disposed at its end, also when it throws
    using work = this.work.begin();
    work.insertOrder(item);
    if (quantity < 1) throw new Error('quantity must be at least 1');
    work.commit();
  }
}

// The container provides the factory, a singleton; the caller owns what it creates.
const app = new DIContainer()
  .addSingleton('db', Database)
  .addSingleton(
    'unitOfWork',
    (db) => ({ begin: () => new UnitOfWork(db.begin()) }),
    ['db'],
  )
  .addSingleton('checkout', Checkout, ['unitOfWork']);

app.get('checkout').placeOrder('book', 1); // tx1: insert book, tx1: commit
```

[Open in the playground](../playground?example=recipes/unit-of-work)

More patterns for state that belongs to one unit of work, and a table for choosing between them, are
in [Scoped state](./request-state.md).

## A scope

A fork for one unit of work, such as a request or a transaction, is a scope: several services that
belong to it share its instances and use the app's services, and the scope disposes them together:

```ts
const app = new DIContainer()
  .addSingleton('db', Database)
  .addSingleton('gateway', Gateway);

// Several services share one transaction and also need the app's services: the fork wires them,
// and disposes the transaction at the end (rolled back unless committed).
export async function placeOrder(item: string, quantity: number) {
  await using scope = app
    .fork()
    .addSingleton('tx', (db) => db.begin(), ['db'])
    .addSingleton('orders', OrderRepository, ['tx'])
    .addSingleton('payments', Payments, ['tx', 'gateway']);

  scope.get('orders').insert(item);
  if (quantity < 1) throw new Error('quantity must be at least 1');
  scope.get('payments').charge(quantity * 10);
  scope.get('tx').commit();
}
```

[Open in the playground](../playground?example=recipes/request-scope)

Create the scope in one place (the handler or a framework hook) and dispose it at the end. Its services
may depend on the root's, never the reverse. See [Scopes](./request-state.md#scopes) for scope modules
and how scopes compare with the other patterns.

## Choosing implementations from configuration

```ts
// Hey, try change mailer value to 'console', see the graph on right side
const config = { mailer: 'smtp' as 'smtp' | 'console' };

const app = new DIContainer()
  .addSingleton('smtpMailer', (): Mailer => ({ send: (to) => `smtp → ${to}` }))
  .addSingleton('consoleMailer', (): Mailer => ({
    send: (to) => `console → ${to}`,
  }))
  // choose the implementation once, at the composition root
  .addAlias(
    'mailer',
    config.mailer === 'smtp' ? 'smtpMailer' : 'consoleMailer',
  );

app.get('mailer').send('ada@example.com'); // "smtp → ada@example.com"
```

## Feature modules

Give each feature a module and compose them at the root:

```ts
const app = new DIContainer()
  .addInstance('config', loadConfig())
  .extend(addDatabase) // (c: ServiceRegistry<{ config: Config }>) => …
  .extend(addUsers) // (c: ServiceRegistry<{ db: Database }>) => …
  .extend(addBilling);
```

Keep each module in a wiring file of its own; see
[Keep the wiring in files of its own](./app-structure.md#keep-the-wiring-in-files-of-its-own).

When a module expects a service under another key or in another shape, see
[Adapting a module's dependencies](./containers.md#adapting-a-module-s-dependencies).

## A module's private services

Register services only a module uses under symbol keys, and apply the module as a namespace:

```ts
const cache = Symbol('cache'); // not exported

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

app.namespace('Catalog', addCatalog); // 'Catalog.products' resolves; the cache doesn't
```

See [Private services](./containers.md#private-services).

## An SDK for other teams

Build the SDK's services in a container of its own (a new one per call), and give consumers an object
typed by the SDK's interface:

```ts
/** The services the SDK object exposes, and what it needs to start and stop. */
type Core = ServiceProvider<{
  api: ApiClient;
  'Flags.flags': FeatureFlags;
  'Telemetry.telemetry': Telemetry;
}> & { dispose(): Promise<void> };

/** Creates the SDK. Nothing runs until a service is used or `start()` is called. */
export function createPlatform(options: PlatformOptions): Platform {
  return toPlatform(createCore(options));
}

function toPlatform(core: Core): Platform {
  let running: Promise<RunningLifecycle> | undefined;
  let stopped: Promise<void> | undefined;
  const stop = () =>
    (stopped ??= (async () => {
      // a failed start already undid its hooks and disposed the container
      const run = await running?.catch(() => undefined);
      await (run ? run.stop() : running ? undefined : core.dispose());
    })());
  return {
    get api() {
      return core.get('api');
    },
    get flags() {
      return core.get('Flags.flags');
    },
    get telemetry() {
      return core.get('Telemetry.telemetry');
    },
    async start() {
      running ??= startLifecycle(core);
      await running;
    },
    stop,
    [Symbol.asyncDispose]: stop,
  };
}
```

Consumers get lazy services, `start()` and `stop()`, plugins that replace, decorate or extend its
services, and a test kit. See [Building an SDK](./sdk.md).

## Sharing services between independent containers

```ts
import { addNamedResolvers, createNamedResolvers, DIContainer } from 'injecute';

const shared = createNamedResolvers(core, ['db', 'logger']);
const feature = new DIContainer().extend(addNamedResolvers(shared));
feature.get('db'); // resolved by `core`
```

## Exposing a container as a plain object

```ts
import { createProxyAccessor } from 'injecute';

const services = createProxyAccessor(app, {
  keys: ['users', ['mailer', 'email']],
});
services.users; // resolves 'users'
services.email; // resolves 'mailer'
```

## Checking the wiring at startup

```ts
import { preload } from 'injecute';

await preload(app); // resolves every service now, so configuration errors surface at boot
```

`preload()` stops at the first failure, which is what a starting app needs. In a test, use
[`checkWiring()`](./testing.md#check-the-wiring) instead: it reports every problem at once.

To run modules' startup work (routes, subscriptions, consumers) and stop it gracefully, see
[Startup and shutdown](./startup-shutdown.md).

`buildServicesGraph(app)` returns every service with its dependencies, for tooling and diagrams; the
[playground](../playground.md) uses it.
