better-effect

Backends and Adapters

Connect Layers to ITI or your own dependency resolver.

The core depends on one small contract:

interface LayerBackend {
  register(registration: LayerRegistration): MaybePromise<void>
  resolve<T extends AnyServiceToken>(token: T): InstanceType<T> | PromiseLike<InstanceType<T>>
  disposeAll(): MaybePromise<void>
}

The backend translates Layer registrations into a container-specific graph, resolves providers, caches according to its policy and disposes its owned state. Core types never expose ITI keys or container types.

ITI adapter

Install the optional peer and import the adapter from its subpath:

bun add iti
import { ItiLayerBackend } from 'better-effect/adapters/iti'

const runtime = await Runtime.make(AppLive, new ItiLayerBackend())

ITI registrations use an internal deterministic key derived from better-effect:${serviceTag}. That translation stays inside the adapter. The application continues to yield the Service constructor.

ITI providers are lazy and cached by the container. disposeAll() calls ITI's container disposal after the Runtime root Scope closes.

Memory backend

For tests and small local programs:

import { MemoryLayerBackend } from 'better-effect/testing'

const backend = new MemoryLayerBackend()
const runtime = await Runtime.make(AppLive, backend)

It rejects duplicate/colliding tags, acquires providers lazily, caches resolved instances, waits for concurrent acquisition of the same tag and clears all state in disposeAll().

Write an adapter

An adapter should keep the Service token → instance relationship intact:

class MyBackend implements LayerBackend {
  async register(registration: LayerRegistration) {
    // translate registration.service.serviceTag into a local identifier
  }

  resolve<T extends AnyServiceToken>(token: T): InstanceType<T> {
    const value = this.lookup(token.serviceTag)
    return value as InstanceType<T>
  }

  async disposeAll() {
    // release container-owned instances and clear registrations
  }
}

Use a Map keyed by AnyServiceToken or by the literal tag internally, then cast only at the lookup boundary. Do not weaken the public resolver to an unrelated generic result such as resolve<A>(token): A.

Adapter responsibilities

On this page