Layer Composition

Layers are Effect’s dependency injection mechanism. A Layer describes how to build one or more services, what services it requires, and how to clean up when done. You compose layers into a dependency graph, then provide them all at once to your program.

How Layers Compose

  • Layer A builds Service A with no dependencies
  • Layer B consumes Service A and produces Service B
  • The program only declares it needs Service B - the full dependency chain is resolved transitively

Building a Layer

Use Layer.effect to build a service from an effect. The effect can itself require other services, establishing the dependency chain:

import { Context, Effect, Layer, Schema } from "effect"

export class Database extends Context.Service<Database, {
  query: (sql: string) => Effect.Effect<unknown[]>
}>()("myapp/Database") {
  static readonly Live = Layer.effect(Database, Effect.gen(function*() {
    const query = Effect.fnUntraced(function*(sql: string) {
      yield* Effect.log("Executing:", sql)
      return [{ id: 1, name: "Alice" }]
    })
    return Database.of({ query })
  }))
}

export class UserService extends Context.Service<UserService, {
  getUser: (id: number) => Effect.Effect<{ id: number; name: string }>
}>()("myapp/UserService") {
  static readonly Live = Layer.effect(UserService, Effect.gen(function*() {
    const db = yield* Database
    const getUser = Effect.fn("UserService.getUser")(function*(id: number) {
      const rows = yield* db.query(`SELECT * FROM users WHERE id = ${id}`)
      return rows[0] as { id: number; name: string }
    })
    return UserService.of({ getUser })
  }))
}

Wiring Dependencies

Layer.provide feeds one layer’s outputs into another layer’s inputs:

const UserServiceLive = Layer.provide(UserService.Live, Database.Live)

You can also provide layers at the program level with Effect.provide:

const program = Effect.gen(function*() {
  const users = yield* UserService
  const user = yield* users.getUser(1)
  yield* Effect.log(`Found: ${user.name}`)
})

const runnable = program.pipe(
  Effect.provide(UserServiceLive)
)

Effect.runPromise(runnable)

Tip: Compose all layers into one MainLive layer and provide it once at the entry point. Avoid scattering Effect.provide calls throughout your code.

Layer.provide vs Layer.provideMerge

Layer.provide gives the inner layer’s dependencies to the outer layer. The result exposes only the outer layer’s outputs:

// Exposes UserService, consumes nothing (Database is hidden inside)
const Live = Layer.provide(UserService.Live, Database.Live)

Layer.provideMerge does the same but also merges the inner layer’s outputs into the result. Use it when you want both services available downstream:

// Exposes both UserService AND Database
const Live = Layer.provideMerge(UserService.Live, Database.Live)

Memoization (v4)

In v4, layers are automatically memoized. A layer is built once per runtime even if it’s provided in multiple places. You no longer need manual Layer.memoize calls:

// Both services share the same Database instance - automatically
const MainLive = Layer.mergeAll(
  UserService.Live,
  OrderService.Live,  // also depends on Database
  Database.Live
)

Gotcha: Before v4, you had to call Layer.memoize to avoid building the same layer twice. In v4 this is automatic, but if you’re migrating from v3, remove the old manual memoization calls.

Layer Lifecycle

Layers built with Layer.effect run their construction effect once. If the effect acquires resources (via Effect.acquireRelease), those resources live for the runtime’s lifetime and their finalizers run when the runtime shuts down.

const DatabaseLive = Layer.effect(Database, Effect.gen(function*() {
  // acquireRelease ties cleanup to the layer's scope
  const conn = yield* Effect.acquireRelease(
    Effect.log("Opening DB connection"),
    () => Effect.log("Closing DB connection")
  )
  return Database.of({ query: () => Effect.succeed([]) })
}))

Dynamic Layers with Layer.unwrap

When a layer needs to be built dynamically (e.g., based on configuration), use Layer.unwrap:

import { Config, Layer } from "effect"

const DatabaseLive = Layer.unwrap(
  Effect.gen(function*() {
    const env = yield* Config.string("DB_ENV")
    if (env === "test") {
      return Layer.succeed(Database, Database.of({ query: () => Effect.succeed([]) }))
    }
    return Database.Live
  })
)

See Dependency Injection for the full Context.Service pattern, and Runtime and Fibers for how layers feed into runtime creation.