Skip to content

Composition Root

The composition root is the application boundary where concrete implementations are chosen and connected. Domain code states what it needs; infrastructure supplies an implementation; InferDI appears only in assembly code.

Domain Contract

The domain owns the interface because the use case depends on it. This file has no container or framework import.

ts
export interface UserStore {
  findName(id: string): Promise<string | undefined>
}

export class GetGreeting {
  constructor(private readonly users: UserStore) {}

  async execute(id: string) {
    const name = await this.users.findName(id)
    return name === undefined ? 'Hello, stranger' : `Hello, ${name}`
  }
}

Infrastructure Implementation

Infrastructure implements the domain contract. A real adapter would use a database client; the small example keeps the call visible.

ts
import type { UserStore } from './domain'

export class PostgresUserStore implements UserStore {
  constructor(private readonly dsn: string) {}

  async findName(id: string) {
    console.info(`query ${this.dsn} for ${id}`)
    return id === '42' ? 'Ada' : undefined
  }
}

Application Composition

Only this file imports InferDI. It chooses PostgresUserStore, supplies its DSN, and connects it to GetGreeting.

ts
import { Container } from '@inferdi/inferdi'
import { GetGreeting } from './domain'
import { PostgresUserStore } from './infrastructure'

export const container = new Container()
  .registerValue('dsn', 'postgres://localhost/app')
  .registerClass('users', PostgresUserStore, ['dsn'])
  .registerClass('greeting', GetGreeting, ['users'])

The registration tuple is checked against each constructor. Changing GetGreeting or PostgresUserStore exposes stale wiring at this boundary.

Use the Service at the Boundary

An HTTP route, CLI command, or queue consumer resolves the top-level service. The business operation itself still talks through its ordinary method.

ts
import { container } from './container'

export async function handleUser(id: string) {
  return container.get('greeting').execute(id)
}

Open a child scope here when the operation needs request or job inputs. Dispose that scope in the same boundary or let a framework adapter tie it to the framework lifecycle.

Test the Domain Directly

The unit test does not build a container. It passes a UserStore fake to the same GetGreeting class used in production.

ts
import { GetGreeting, type UserStore } from './domain'

const fakeUsers: UserStore = {
  async findName(id) {
    return id === '42' ? 'Ada' : undefined
  }
}

const service = new GetGreeting(fakeUsers)
const result = await service.execute('42')

if (result !== 'Hello, Ada') {
  throw new Error(`Unexpected greeting: ${result}`)
}

Use .override() for integration tests that need to exercise the real composition graph with one implementation replaced. Direct construction is usually clearer for a single domain service. Continue with Testing and Overrides.