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
MainLivelayer and provide it once at the entry point. Avoid scatteringEffect.providecalls 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.memoizeto 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.