Layer Composition
Real applications are graphs of services: a repository needs a database client,
the client needs a connection pool, the pool needs configuration. Layer
composition is how you wire that graph. The two essential operators are
Layer.provide — feed one layer’s output into another’s requirements and keep
it private — and Layer.provideMerge — do the same but also expose the
dependency.
import { Array, Config, Context, Effect, type Option, Layer, Schema } from "effect"import { SqlClient, SqlError } from "effect/unstable/sql"import { PgClient } from "@effect/sql-pg"
// A leaf layer: the Postgres client, built from configuration. It produces a// SqlClient and may fail if config or the connection is bad.const SqlClientLayer: Layer.Layer< PgClient.PgClient | SqlClient.SqlClient, Config.ConfigError | SqlError.SqlError> = PgClient.layerConfig({ url: Config.redacted("DATABASE_URL")})
class UserRepoError extends Schema.TaggedErrorClass<UserRepoError>()( "UserRepoError", { reason: SqlError.SqlError }) {}
export class UserRepo extends Context.Service<UserRepo, { findById(id: string): Effect.Effect< Option.Option<{ readonly id: string; readonly name: string }>, UserRepoError >}>()("myapp/UserRepo") { // The "naked" layer: it produces UserRepo but still *requires* a SqlClient. // Note `SqlClient.SqlClient` in the third type-parameter slot. static readonly layerNoDeps: Layer.Layer< UserRepo, never, SqlClient.SqlClient > = Layer.effect( UserRepo, Effect.gen(function*() { const sql = yield* SqlClient.SqlClient
const findById = Effect.fn("UserRepo.findById")(function*(id: string) { const rows = yield* sql<{ readonly id: string; readonly name: string }>` SELECT * FROM users WHERE id = ${id} ` return Array.head(rows) }, Effect.mapError((reason) => new UserRepoError({ reason })))
return UserRepo.of({ findById }) }) )
// `Layer.provide` satisfies the SqlClient requirement *and hides it*. The // result exposes only UserRepo — callers can't reach the SqlClient. static readonly layer: Layer.Layer< UserRepo, Config.ConfigError | SqlError.SqlError > = this.layerNoDeps.pipe(Layer.provide(SqlClientLayer))
// `Layer.provideMerge` satisfies the requirement *and re-exposes* it, so the // result provides both UserRepo and SqlClient. static readonly layerWithSql: Layer.Layer< UserRepo | SqlClient.SqlClient, Config.ConfigError | SqlError.SqlError > = this.layerNoDeps.pipe(Layer.provideMerge(SqlClientLayer))}provide vs. provideMerge
Section titled “provide vs. provideMerge”Both feed SqlClientLayer into layerNoDeps, removing SqlClient from its
requirements. The difference is what the result exposes:
Layer.provide(dep)— the dependency is an implementation detail. The output type drops the dependency (UserRepoonly). This is the default: keep internals private so other parts of the app can’t accidentally couple to your database client.Layer.provideMerge(dep)— the dependency is part of your public surface. The output type unions both (UserRepo | SqlClient). Use it when callers legitimately need both — for example a health-check that pings the database directly while also using the repository.
Merging independent layers
Section titled “Merging independent layers”When several layers don’t depend on one another, combine them into a single
application layer with Layer.mergeAll. The result provides the union of every
service and requires the union of their dependencies:
import { Layer } from "effect"import { Database } from "./Database.ts"import { Mailer } from "./Mailer.ts"import { Metrics } from "./Metrics.ts"
// One layer that provides Database, Mailer, and Metrics together.export const AppLayer = Layer.mergeAll( Database.layer, Mailer.layer, Metrics.layer)Memoization
Section titled “Memoization”A layer included more than once in the graph is built only once by default,
and the same instance is shared. If UserRepo.layer and OrderRepo.layer both
provide SqlClientLayer, exactly one Postgres client is created and both repos
share it. This is what makes deep dependency graphs efficient — you don’t have
to hoist shared dependencies by hand.
If you genuinely need a fresh copy of a layer (a second, isolated connection
pool, say), wrap it in Layer.fresh to opt out of sharing.
import { Layer } from "effect"import { SqlClientLayer } from "./sql.ts"
// A second, independent SqlClient instance, not shared with the memoized one.const IsolatedSql = Layer.fresh(SqlClientLayer)Next: build a layer whose implementation is chosen at runtime with Layers from config.