| name | effect-layers |
| description | Effect Layer construction, composition, and provision. Use when creating layers, wiring dependencies, composing service graphs, or providing layers to programs. |
| user-invocable | false |
Effect Layers
Layers are blueprints for constructing services: Layer<Out, Error, In>.
Out = service produced, Error = construction failure, In = dependencies
needed to build it.
See /effect-service for service interface design and error types.
See /effect-testing for test layer provision patterns.
Module Structure
Colocate the tag, types, and live layer within the same module directory.
Test layers live in the test tree, mirroring the source structure.
src/
services/
DocumentStore/
index.ts # Tag, types, and re-exports
DocumentStoreLive.ts # Live layer
Notifications/
index.ts
NotificationsLive.ts
test/
services/
DocumentStore/
DocumentStoreTest.ts # Test layer
Notifications/
NotificationsTest.ts
Naming Conventions
| Layer variant | Suffix | Example |
|---|
| Production | Live | DocumentStoreLive |
| Test / Fake | Test | DocumentStoreTest |
| In-memory variant | Memory | DocumentStoreMemory |
| Development / local | Dev | EmailDev |
When a service has multiple real implementations, name each layer after the
backing technology:
const DocumentStoreS3 = Layer.effect(DocumentStore)();
const DocumentStorePostgres = Layer.effect(DocumentStore)();
Name layers by their variant, not by their role in a specific test.
EmailTest is reusable across test suites; emailLayerForNotificationTest
is not.
Construction
Choose the simplest constructor that fits.
| Constructor | When to use |
|---|
Layer.succeed | Static value, no setup, no dependencies |
Layer.sync | Synchronous setup (e.g. mutable state init) |
Layer.effect | Needs dependencies, effectful setup, or cleanup |
Layer.effectServices | Produces multiple service tags from one effect |
Note: v3's Layer.scoped is merged into Layer.effect in v4. Layer.effect
handles both effectful setup and scoped resource acquisition.
Layer.succeed — static values
Use when the implementation is pure or only needs static config.
export const DocumentStoreLive = Layer.succeed(DocumentStore, {
get: (id) => ,
put: (doc) => ,
delete: (id) => ,
});
Layer.sync — synchronous setup with local state
static readonly testLayer = Layer.sync(Counter, () => {
let count = 0;
return {
increment: () => Effect.sync(() => { count++ }),
get: () => Effect.succeed(count),
};
});
Layer.effect — effectful setup with dependencies
The primary pattern. Yield dependencies, return the service. Also handles
scoped resources (connections, temp dirs, file handles) — the scope manages
release automatically.
export const EmailLive = Layer.effect(
Email,
Effect.gen(function* () {
const config = yield* AppConfig;
const client = yield* createSmtpClient(config.smtp);
return {
send: (to, subject, body) =>
Effect.tryPromise(() => client.sendMail({ to, subject, html: body })),
};
}),
);
Resource lifecycle example:
export const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const config = yield* Config;
const pool = yield* Effect.acquireRelease(createPool(config.connectionString), (pool) =>
Effect.promise(() => pool.end()),
);
return {
query: (sql, params) => Effect.tryPromise(() => pool.query(sql, params)),
};
}),
);
Layer.effectServices — multiple tags from one effect
Use when a single setup produces multiple services.
export const PromptServicesLive: Layer.Layer<Confirm | Select | Multiselect, never, BasePrompt> =
Layer.effectServices(
Effect.map(BasePrompt, (base) =>
ServiceMap.make(Confirm, makeConfirm(base)).pipe(
ServiceMap.add(Select, makeSelect(base)),
ServiceMap.add(Multiselect, makeMultiselect(base)),
),
),
);
Composition
Layer.merge / Layer.mergeAll — combine independent layers
Merges layers that have no dependency on each other. Runs construction
concurrently.
const base = Layer.merge(NodeServices.layer, FetchHttpClient.layer);
const AppLayer = Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer, ClackLive);
Layer.provide — wire dependencies
Supplies the In requirements of a layer. Read as "give this layer its
dependencies."
const LoggerWithConfig = Layer.provide(LoggerLive, ConfigLive);
For multiple dependencies:
const DatabaseReady = Layer.provide(DatabaseLive, Layer.mergeAll(ConfigLive, LoggerLive));
Layer.provideMerge — provide + keep dependencies visible
Like Layer.provide, but the dependency layer's output remains in the
composite output. Useful when downstream code also needs the dependency
directly.
const appLayer = UserServiceLive.pipe(
Layer.provideMerge(DatabaseLive),
Layer.provideMerge(ConfigLive),
);
Compose at the edges
Individual modules export their own layers. Composition into a full
application layer happens at the entrypoint, not deep inside service modules.
Avoid deeply nested Layer.provide chains inside service modules.
const AppLive = Layer.mergeAll(DocumentStoreLive, NotificationsLive, EmailLive).pipe(
Layer.provide(AppConfigLive),
Layer.provide(DatabaseLive),
);
Test files compose their own layer, substituting test doubles where needed.
This makes the substitution boundary explicit — you can see at a glance
what's real and what's faked:
const TestEnv = Layer.mergeAll(
NotificationsLive,
EmailTest,
DocumentStoreMemory,
);
Providing Layers to Programs
Effect.provide — at the call site
const program = Effect.gen(function* () {
const db = yield* Database;
return yield* db.query("SELECT 1");
});
const runnable = program.pipe(Effect.provide(AppLayer));
ManagedRuntime — at the application edge
For CLI apps, create a ManagedRuntime from the top-level layer for proper
lifecycle management and resource cleanup.
export const AppLayer = Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer, ClackLive);
export const Runtime = ManagedRuntime.make(AppLayer);
Runtime.runPromise(program);
Conditional layer composition
When some layers depend on runtime options (e.g. workspace scope), compose
them at the entry point:
export function run(program, options?) {
if (options?.workspace) {
const wsLayer = workspaceLayer(options.workspace);
const sourcesLayer = Layer.provide(SourcesLive, wsLayer);
return program.pipe(
Effect.provide(Layer.mergeAll(wsLayer, sourcesLayer)),
Effect.scoped,
Runtime.runPromise,
);
}
return Runtime.runPromise(program);
}
Memoization
In v4, layers are automatically memoized across Effect.provide calls via a
shared MemoMap. This is a safety net — proper layer composition is still
the recommended pattern.
const app = Layer.merge(
Layer.provide(RepoA, Postgres.layer({ url, poolSize: 10 })),
Layer.provide(RepoB, Postgres.layer({ url, poolSize: 10 })),
);
const postgres = Postgres.layer({ url, poolSize: 10 });
const app = Layer.merge(Layer.provide(RepoA, postgres), Layer.provide(RepoB, postgres));
Rule: When using parameterized layer constructors, store the result in a
variable before using it in multiple places.
Layer.fresh — opt out of memoization
Use Layer.fresh when each consumer needs its own instance (e.g. isolated
test state):
const freshCounter = Layer.fresh(Counter.layer);
Effect.provide with { local: true } — isolated memoization
New in v4. Builds the provided layer with a local memo map instead of the
fiber's shared one. Useful for test isolation where each test needs
independent resources:
const main = program.pipe(
Effect.provide(MyServiceLayer),
Effect.provide(MyServiceLayer, { local: true }),
);
Hiding Dependencies (R = never)
Service methods should not leak implementation dependencies. Dependencies
belong in the layer, not the interface.
interface MyService {
readonly query: (sql: string) => Effect.Effect<Row[], Error, Config | Logger>;
}
interface MyService {
readonly query: (sql: string) => Effect.Effect<Row[], Error>;
}
export const MyServiceLive = Layer.effect(
MyService,
Effect.gen(function* () {
const config = yield* Config;
const logger = yield* Logger;
return {
query: (sql) =>
Effect.gen(function* () {
yield* logger.log(`Executing: ${sql}`);
return yield* runQuery(config.connectionString, sql);
}),
};
}),
);
When inner effects still need context
If service methods call functions that require services (e.g. FileSystem),
there are two approaches:
Approach A: Close over the service value directly. Preferred when methods
use the service inline.
export const SkillManagerLive = Layer.effect(
SkillManager,
Effect.gen(function* () {
const fs = yield* FileSystem;
const path = yield* Path;
return {
copySkill: (src, dst) =>
fs.copy(src, dst).pipe(Effect.flatMap(() => fs.exists(path.join(dst, "SKILL.md")))),
};
}),
);
Approach B: Capture a dependency layer for re-provision. Use when methods
delegate to standalone effectful functions that declare their own R.
export const SourcesLive = Layer.effect(
Sources,
Effect.gen(function* () {
const fs = yield* FileSystem;
const path = yield* Path;
const ws = yield* Workspace;
const deps = Layer.mergeAll(
Layer.succeed(FileSystem, fs),
Layer.succeed(Path, path),
Layer.succeed(Workspace, ws),
);
return {
find: (source, options) => findExtensions(source, options).pipe(Effect.provide(deps)),
};
}),
);
Tradeoffs:
- Approach A is simpler and type-safe. Prefer it when practical.
- Approach B is necessary when delegating to functions with their own
R
requirements. Keep the provide helper typed against the actual layer to
catch missing dependencies at compile time. Avoid any casts.
Test Layers
Simple test fakes
For straightforward test doubles, use Layer.succeed with minimal or
stubbed behavior:
export const EmailTest = Layer.succeed(Email, {
send: () => Effect.void,
});
Stateful test fakes
When tests need to inspect what happened (assert on sent messages, stored
records, etc.), use a Ref to accumulate state and expose an inspection API:
export interface TestEmail extends ServiceMap.Service.Shape<typeof Email> {
readonly sentMessages: Effect.Effect<ReadonlyArray<SentMessage>>;
}
export const EmailTest = Layer.effect(
Email,
Effect.gen(function* () {
const ref = yield* Ref.make<ReadonlyArray<SentMessage>>([]);
return {
send: (to, subject, body) => Ref.update(ref, (msgs) => [...msgs, { to, subject, body }]),
sentMessages: Ref.get(ref),
};
}),
);
In-memory test variants
For repository-style services, an in-memory implementation gives
integration-level confidence without external infrastructure:
export const DocumentStoreMemory = Layer.succeed(DocumentStore, {
});
Per-test vs shared layers
Per-test (preferred): Fresh layers prevent state leakage.
it.effect("works", () =>
Effect.gen(function* () {
const svc = yield* MyService;
}).pipe(Effect.provide(MyService.testLayer)),
);
Suite-shared: Only for expensive resources (DB connections). Beware state
leakage between tests.
Checklist