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.