04·02Layers

A `Layer<ROut, E, RIn>` is a recipe for constructing one or more services. It declares what it provides (`ROut`), what it requires (`RIn`), and how construction can fail (`E`).

22 - Layers: Building Services

Previous · Next

A Layer<ROut, E, RIn> is a recipe for constructing one or more services. It declares what it provides (ROut), what it requires (RIn), and how construction can fail (E).

Layers compose: you wire dependencies, merge independent services, and provide one top-level AppLayer at the application boundary.

What you’ll work through

index.test.ts walks through the three Layer constructors (succeed, sync, effect), then the two composition operators (merge, provide), and finishes with mergeAll for three-or-more services. Each beat builds a small service graph and runs a program against it.

By the end you should be able to look at any Effect<A, E, R> and answer: which Layers do I need to satisfy R, and in what order do they compose?


What a Layer Is

Layer<ROut, E, RIn>
│ │ │
│ │ └── Services consumed during construction
│ └────── Errors construction can produce
└────────── Services produced by this layer

Three constructors cover almost every case: Layer.succeed for a static value, Layer.sync for a lazy synchronous factory, and Layer.effect for a constructor that itself runs as an Effect (async, fallible, may use other services).

ConfigLive ────────────┐
▼
DatabaseLive (needs Config)
│
▼
UserServiceLive (needs Database)
│
▼
AppLayer (RIn = never)
│
▼
Effect.provide(AppLayer)

Effect resolves the graph at startup, constructing each service exactly once and sharing the instance with every dependent.


Layer.succeed

Creates a Layer from a pre-built implementation. No construction effect, no dependencies.

import { Effect, Layer } from "effect"
declare const Logger: Context.Service<Logger, { log: (msg: string) => Effect.Effect<void> }>
// ┌─── Layer<Logger, never, never>
// ▼
const LoggerLive = Layer.succeed(Logger, {
log: (msg) => Effect.sync(() => console.log(msg)),
})

When to Use

When the implementation is a value you already have, a constant object, a closure captured from the surrounding scope, a test double.

Your turn wrap a pre-built `Logger` value with `Layer.succeed` and provide it to the program.


Layer.sync

Creates a Layer from a lazy synchronous factory. The factory runs once on first construction.

import { Effect, Layer } from "effect"
// ┌─── Layer<Logger, never, never>
// ▼
const InMemoryLogger = Layer.sync(Logger, () => {
const buffer: Array<string> = []
return {
log: (msg) => Effect.sync(() => buffer.push(msg)),
}
})

Details

Layer.sync differs from Layer.succeed in laziness: the factory is invoked at construction time, allowing per-construction state (a fresh buffer, a counter). The service itself is still synchronously built, no Effect runs.

When to Use

When the service captures fresh mutable state that must be created per layer instantiation, not shared at module load time.

Your turn use `Layer.sync` so the factory body runs exactly once and the construction counter advances by one.


Layer.effect

Creates a Layer from an Effect. Construction can await Promises, fail with typed errors, and consume other services.

import { Effect, Layer } from "effect"
// ┌─── Layer<Database, ConnectionError, Config>
// ▼
const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const config = yield* Config
const connection = yield* openConnection(config.url)
return {
query: (sql: string) => runQuery(connection, sql),
}
})
)

Details

yield* Config inside the constructor adds Config to the Layer’s RIn. Any error the constructor can fail with appears in E.

This is the only constructor that can produce non-trivial RIn or E.

When to Use

Any service whose construction itself is effectful, opening a connection pool, reading configuration, registering finalizers (use Layer.effect for the last case, see step 37).

Your turn build `UserService` with `Layer.effect`, yielding `Logger` during construction, then wire it with `Layer.provide`.


Composing Layers

Layer.provide: wire a dependency

import { Layer } from "effect"
// ┌─── Layer<Database, never, never>
// ▼
const DatabaseWithConfig = DatabaseLive.pipe(Layer.provide(ConfigLive))

Layer.provide(dependency) plugs dependency into the layer’s RIn, removing it from the result’s RIn.

Layer.provideMerge: wire and expose

import { Layer } from "effect"
const AppLayer = UserServiceLive.pipe(
Layer.provideMerge(DatabaseLive),
Layer.provideMerge(ConfigLive)
)
// Layer<UserService | Database | Config, ..., never>

Layer.provideMerge keeps the wired-in services in the output, so callers can use them directly.

Layer.merge and Layer.mergeAll: combine independents

import { Layer } from "effect"
const AppLayer = Layer.mergeAll(LoggerLive, DatabaseWithConfig, CacheLive)

merge and mergeAll combine services that do not depend on each other. For services with dependencies between them, use Layer.provide first.

Your turn use `Layer.merge` to combine independent `Logger`/`Database` test layers, then `Layer.provide` to wire `Config` into `DatabaseLive`.


Layer.mergeAll for three or more independent layers

Layer.merge takes two layers. Once you have three or more independent services to combine into an application layer, reach for Layer.mergeAll — it’s the variadic form and reads better than a chain of merges.

import { Layer } from "effect"
// ┌─── Layer<Logger | Database | Config, never, never>
// ▼
const AppLayer = Layer.mergeAll(LoggerTest, DatabaseTest, ConfigLive)
const program = Effect.gen(function* () {
const logger = yield* Logger
const db = yield* Database
const config = yield* Config
yield* logger.log(config.dbUrl)
return yield* db.query("SELECT 1")
})
await Effect.runPromise(program.pipe(Effect.provide(AppLayer)))

The arguments to mergeAll must be independent — none of them should depend on services produced by another in the list. For services that do have dependencies on each other, wire them with Layer.provide first, then hand the resulting layer to mergeAll.

Your turn combine three independent layers (`LoggerTest`, `DatabaseTest`, `ConfigLive`) into a single `AppLayer` and provide it once to the program.


A Full Example

A UserService that depends on a Logger and a Database, with Database itself depending on Config.

import { Effect, Context, Layer } from "effect"
class Config extends Context.Service<Config, { dbUrl: string }>()("app/Config") {}
class Logger extends Context.Service<Logger, { log: (msg: string) => Effect.Effect<void> }>()("app/Logger") {}
class Database extends Context.Service<Database, { query: (sql: string) => Effect.Effect<Array<string>> }>()("app/Database") {}
class UserService extends Context.Service<UserService, { getUser: (id: string) => Effect.Effect<{ id: string; name: string }> }>()("app/UserService") {}
const ConfigLive = Layer.succeed(Config, { dbUrl: "postgres://localhost" })
const LoggerLive = Layer.succeed(Logger, {
log: (msg) => Effect.sync(() => console.log(msg)),
})
const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const { dbUrl } = yield* Config
return {
query: (sql: string) => Effect.succeed([`${dbUrl}: ${sql}`]),
}
})
)
const UserServiceLive = Layer.effect(
UserService,
Effect.gen(function* () {
const logger = yield* Logger
const db = yield* Database
return {
getUser: (id) =>
Effect.gen(function* () {
yield* logger.log(`fetch ${id}`)
const rows = yield* db.query(`SELECT * FROM users WHERE id = '${id}'`)
return { id, name: rows[0] ?? "unknown" }
}),
}
})
)
// ┌─── Layer<UserService, never, never>
// ▼
const AppLayer = UserServiceLive.pipe(
Layer.provide(
Layer.mergeAll(
LoggerLive,
DatabaseLive.pipe(Layer.provide(ConfigLive))
)
)
)

Effect.provide(AppLayer) at the entry point satisfies every requirement in one call.


Rules of thumb

  • Layers are values. Compose with Layer.merge (parallel) and Layer.provide / Layer.provideMerge (dependency); pass them around like any other value.
  • Effects depend on services; services depend on Layers. Don’t conflate the two. The R channel asks for services, and Effect.provide(layer) satisfies the ask.
  • Build the runtime once at the program edge. Inside business logic, effects should require services, not assemble them.

Common Pitfalls

1, Providing layers inside business logic. Layers should be wired at the application boundary, not inside helpers. Otherwise each call site rebuilds the graph.

2, Confusing Layer.succeed with Layer.sync. succeed takes a value; sync takes a factory. Passing a factory to succeed stores the function as the service.

3, Using Layer.merge to satisfy dependencies. merge only combines independent layers. If A needs B, use A.pipe(Layer.provide(B)).

4, Re-declaring shared layers. Defining Layer.sync(Database, makeDb) twice produces two separate Database instances. Name the layer once and reuse the value.


in each beat. - [ ] Work through the three constructors first: `Layer.succeed` (eager value), `Layer.sync` (lazy factory — note the construction-count assertion), `Layer.effect` (constructor that uses other services). - [ ] Then the two composition operators: `Layer.merge` for siblings, `Layer.provide` for "A depends on B". Watch the `RIn` parameter in the type comment collapse to `never` after `Layer.provide`. - [ ] Finish with `Layer.mergeAll` for three-or-more independents — that's the form an `AppLayer` usually takes.

See Also

  • Previous, 21-context-basics: Tags and the R parameter.
  • Next, 23-service-driven: the modern Context.Service pattern.
  • Later, 24-advanced-di: shared instances, larger graphs, provideMerge.
  • Later, 46-scope: Layer.effect for services with finalizers.

Run the tests: pnpm test 22-layers


The full test suite

The index.test.ts for this lesson, verbatim from steps/22-layers/.

index.test.ts
import { describe, it, expect } from "vite-plus/test"
import { Effect, Context, Layer } from "effect"
// =============================================================================
// 12 - Layers
// =============================================================================
//
// A Layer is a recipe for constructing services. Layer.succeed wraps a value,
// Layer.sync wraps a lazy synchronous factory, Layer.effect wraps an effectful
// constructor. Layer.provide wires dependencies; Layer.merge/mergeAll combines
// independent layers.
class Config extends Context.Service<Config, { readonly dbUrl: string }>()("app/Config") {}
class Logger extends Context.Service<Logger, {
readonly log: (message: string) => Effect.Effect<void>
readonly getMessages: () => Effect.Effect<ReadonlyArray<string>>
}>()("app/Logger") {}
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<ReadonlyArray<string>>
}>()("app/Database") {}
class UserService extends Context.Service<UserService, {
readonly getUser: (id: string) => Effect.Effect<{ id: string; name: string }>
}>()("app/UserService") {}
describe("12 - Layers", () => {
// @beat
// id: layer-succeed
// shape: one-todo
// title: Wrap a pre-built implementation with Layer.succeed
// teaches: Layer.succeed lifts an existing service value into a Layer with no construction effect.
// hint: Layer.succeed(Tag, implementation)
describe("Layer.succeed", () => {
it("wraps a pre-built implementation", async () => {
const messages: Array<string> = []
const LoggerLive = /* @stub */ Layer.succeed(Logger, {
log: (msg) =>
Effect.sync(() => {
messages.push(msg)
}),
getMessages: () => Effect.succeed(messages),
}) /* @end */
const program = Effect.gen(function* () {
const logger = yield* Logger
yield* logger.log("hello")
yield* logger.log("world")
return yield* logger.getMessages()
})
const result = await Effect.runPromise(program.pipe(Effect.provide(LoggerLive)))
expect(result).toEqual(["hello", "world"])
})
})
// @end-beat
// @beat
// id: layer-sync
// shape: one-todo
// title: Build per-construction state with Layer.sync
// teaches: Layer.sync runs a lazy factory once per construction; fresh state is captured at startup.
// hint: Layer.sync(Tag, factory) — the factory returns the service
describe("Layer.sync", () => {
it("calls the factory lazily, once, with fresh per-construction state", async () => {
let constructions = 0
const DatabaseTest = /* @stub */ Layer.sync(Database, () => {
constructions += 1
return {
query: (sql: string) => Effect.succeed([`row for ${sql}`]),
}
}) /* @end */
const program = Effect.gen(function* () {
const db = yield* Database
const a = yield* db.query("a")
const b = yield* db.query("b")
return [a, b]
})
await Effect.runPromise(program.pipe(Effect.provide(DatabaseTest)))
expect(constructions).toBe(1)
})
})
// @end-beat
// @beat
// id: layer-effect
// shape: multi-todo
// title: Build a service that needs another service with Layer.effect
// teaches: Layer.effect lets the constructor yield* other services; Layer.provide wires the dependency.
// hint: Layer.effect(Tag, Effect.gen(...)) and then pipe Layer.provide(dependency)
describe("Layer.effect", () => {
it("constructs a service that depends on another service", async () => {
const captured: Array<string> = []
const LoggerTest = Layer.succeed(Logger, {
log: (msg) =>
Effect.sync(() => {
captured.push(msg)
}),
getMessages: () => Effect.succeed(captured),
})
// ┌─── Layer<UserService, never, Logger>
// ▼
const UserServiceTest = /* @stub */ Layer.effect(
UserService,
Effect.gen(function* () {
const logger = yield* Logger
return {
getUser: (id) =>
Effect.gen(function* () {
yield* logger.log(`build ${id}`)
return { id, name: "Alice" }
}),
}
})
) /* @end */
const AppLayer = UserServiceTest.pipe(/* @stub */ Layer.provide(LoggerTest) /* @end */)
const program = Effect.gen(function* () {
const users = yield* UserService
return yield* users.getUser("42")
})
const result = await Effect.runPromise(program.pipe(Effect.provide(AppLayer)))
expect(result).toEqual({ id: "42", name: "Alice" })
expect(captured).toEqual(["build 42"])
})
})
// @end-beat
// @beat
// id: merge-vs-provide
// shape: multi-todo
// title: Merge independents, provide dependencies
// teaches: Layer.merge combines unrelated services; Layer.provide satisfies a dependency.
// hint: use Layer.merge for siblings; pipe Layer.provide to wire a child's input
describe("composing layers", () => {
it("Layer.merge combines independent services", async () => {
const messages: Array<string> = []
const LoggerTest = Layer.succeed(Logger, {
log: (msg) =>
Effect.sync(() => {
messages.push(msg)
}),
getMessages: () => Effect.succeed(messages),
})
const DatabaseTest = Layer.sync(Database, () => ({
query: () => Effect.succeed(["user1", "user2"]),
}))
const AppLayer = /* @stub */ Layer.merge(LoggerTest, DatabaseTest) /* @end */
const program = Effect.gen(function* () {
const logger = yield* Logger
const db = yield* Database
yield* logger.log("query")
return yield* db.query("SELECT *")
})
const result = await Effect.runPromise(program.pipe(Effect.provide(AppLayer)))
expect(result).toEqual(["user1", "user2"])
expect(messages).toEqual(["query"])
})
it("Layer.provide wires a dependency into a layer that requires it", async () => {
const ConfigLive = Layer.succeed(Config, { dbUrl: "postgres://test" })
const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const { dbUrl } = yield* Config
return {
query: (sql: string) => Effect.succeed([`${dbUrl}: ${sql}`]),
}
})
)
// ┌─── Layer<Database, never, never>
// ▼
const DatabaseWithConfig = DatabaseLive.pipe(/* @stub */ Layer.provide(ConfigLive) /* @end */)
const program = Effect.gen(function* () {
const db = yield* Database
return yield* db.query("SELECT 1")
})
const result = await Effect.runPromise(
program.pipe(Effect.provide(DatabaseWithConfig))
)
expect(result[0]).toContain("postgres://test")
})
})
// @end-beat
// @beat
// id: merge-all
// shape: one-todo
// title: Combine N independent layers with Layer.mergeAll
// teaches: Layer.mergeAll is the variadic form of Layer.merge — use it when composing three or more independent services into one application layer.
// hint: Layer.mergeAll(LoggerTest, DatabaseTest, ConfigLive)
describe("Layer.mergeAll", () => {
it("combines three independent layers into one", async () => {
const messages: Array<string> = []
const LoggerTest = Layer.succeed(Logger, {
log: (msg) => Effect.sync(() => { messages.push(msg) }),
getMessages: () => Effect.succeed(messages),
})
const DatabaseTest = Layer.sync(Database, () => ({
query: () => Effect.succeed(["row"]),
}))
const ConfigLive = Layer.succeed(Config, { dbUrl: "postgres://test" })
// ┌─── Layer<Logger | Database | Config, never, never>
// ▼
const AppLayer = /* @stub */ Layer.mergeAll(LoggerTest, DatabaseTest, ConfigLive) /* @end */
const program = Effect.gen(function* () {
const logger = yield* Logger
const db = yield* Database
const config = yield* Config
yield* logger.log(config.dbUrl)
return yield* db.query("SELECT 1")
})
const result = await Effect.runPromise(program.pipe(Effect.provide(AppLayer)))
expect(result).toEqual(["row"])
expect(messages).toEqual(["postgres://test"])
})
})
// @end-beat
})
← PreviousContext BasicsNext →Service-Driven Development