بنقرة واحدة
effect-layer-design
Design and compose Effect layers for clean dependency management
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Design and compose Effect layers for clean dependency management
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Define type-safe RPC contracts with effect/unstable/rpc — Rpc.make payload/success/error/defect schemas, RpcSchema.Stream streaming responses, RpcGroup composition (add/merge/omit/prefix/annotate), and RpcMiddleware.Service definitions shared by client and server. Use when declaring or evolving RPC procedures, building a shared contract package, adding streaming endpoints, or defining auth/observability middleware types.
Consume typed RPC services with Effect's RpcClient — protocol layers (HTTP, WebSocket, TCP, worker, in-memory), RpcSerialization codecs, per-call and ambient headers, streaming calls, interruption, reconnection, and RpcClientError handling. Use when calling an RpcGroup from a client, wiring a client transport + serialization stack, debugging RPC transport failures or reconnects, or testing RPC consumers with RpcTest.
Serve RpcGroup contracts with Effect's RpcServer — handler layers (RpcGroup.toLayer/toLayerHandler), protocol layers (HTTP, WebSocket, TCP, stdio, worker), RpcSerialization, server middleware, streaming results, interruption and shutdown semantics, RpcTest. Use when implementing the server side of an Effect RPC API, mounting RPC on an HttpRouter or existing HTTP app, choosing a wire format, implementing RpcMiddleware, handling client aborts, or testing RPC handlers.
Make outgoing HTTP requests with Effect's HttpClient — HttpClientRequest builders, schema-decoded HttpClientResponse bodies, the HttpClientError taxonomy, retryTransient/rate limiting/cookies/redirects, streaming uploads and downloads, and FetchHttpClient/NodeHttpClient transport layers. Use when calling external REST/JSON APIs, uploading or downloading files and streams, adding retries/auth/tracing to outbound HTTP, or mocking HTTP responses in tests.
Build HTTP servers with effect/unstable/http — HttpRouter routes and middleware, HttpServerRequest schema decoding, HttpServerResponse constructors, multipart uploads, websocket upgrades, static files, NodeHttpServer/BunHttpServer layers, and in-memory web handlers. Use when serving raw HTTP routes, reading request bodies/cookies/uploads, writing server middleware, streaming responses, or testing handlers without a real port.
Build bidirectional socket transports with effect/unstable/socket — the Socket run/writer surface, WebSocket-backed sockets, TCP/Unix-domain clients via NodeSocket, SocketServer accept loops, channel adapters, the SocketError taxonomy, and reconnect patterns. Use when connecting to or serving raw TCP, Unix-domain, or WebSocket endpoints, framing socket bytes with Stream/Channel (NDJSON), building reconnecting socket clients, or providing socket transports to RPC/devtools layers.
| name | effect-layer-design |
| description | Design and compose Effect layers for clean dependency management |
Create layers that construct services while managing their dependencies cleanly.
The Effect v4 source is available at ~/.cache/effect-v4/.
Browse and read files there directly to look up APIs, types, and implementations.
Reference this for:
packages/effect/src/Layer.tspackages/effect/src/Context.tsMIGRATION.mdpackages/effect/src/import { Layer } from 'effect';
// Layer<RequirementsOut, Error, RequirementsIn>
// ▲ ▲ ▲
// │ │ └─ What this layer needs
// │ └─ Errors during construction
// └─ What this layer produces
import { Context, Effect, Layer } from 'effect';
interface ConfigData {
readonly logLevel: string;
readonly connection: string;
}
export class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<ConfigData>;
}
>()('Config') {}
// Layer<Config, never, never>
// ▲ ▲ ▲
// │ │ └─ No dependencies
// │ └─ Cannot fail
// └─ Produces Config
export const ConfigLive = Layer.succeed(
Config,
Config.of({
getConfig: Effect.succeed({
logLevel: 'INFO',
connection: 'mysql://localhost/db'
})
})
);
import { Context, Effect, Layer, Console } from 'effect';
interface ConfigData {
readonly logLevel: string;
readonly connection: string;
}
export class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<ConfigData>;
}
>()('Config') {}
export class Logger extends Context.Service<
Logger,
{
readonly log: (message: string) => Effect.Effect<void>;
}
>()('Logger') {}
// Layer<Logger, never, Config>
// ▲ ▲ ▲
// │ │ └─ Needs Config
// │ └─ Cannot fail
// └─ Produces Logger
export const LoggerLive = Layer.effect(
Logger,
Effect.gen(function* () {
const config = yield* Config; // Access dependency
return Logger.of({
log: (message) =>
Effect.gen(function* () {
const { logLevel } = yield* config.getConfig;
yield* Console.log(`[${logLevel}] ${message}`);
})
});
})
);
Use Layer.effect for all layers, including those with resources that need cleanup. In Effect v4, Layer.effect automatically handles Scope lifecycle — Layer.scoped is no longer needed.
Resources are acquired and released using Effect.acquireRelease or Effect.addFinalizer inside the Layer.effect constructor:
import { Context, Effect, Layer } from 'effect';
interface ConfigData {
readonly logLevel: string;
readonly connection: string;
}
interface Connection {
readonly close: () => void;
}
interface DatabaseError {
readonly _tag: 'DatabaseError';
}
export class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<ConfigData>;
}
>()('Config') {}
export class Database extends Context.Service<
Database,
{
readonly query: (sql: string) => Effect.Effect<unknown, DatabaseError>;
}
>()('Database') {}
declare const connectToDatabase: (
config: ConfigData
) => Effect.Effect<Connection, DatabaseError>;
declare const executeQuery: (
connection: Connection,
sql: string
) => Effect.Effect<unknown, DatabaseError>;
// Layer<Database, DatabaseError, Config>
export const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const config = yield* Config;
const configData = yield* config.getConfig;
// Acquire resource with automatic release — Layer.effect handles Scope
const connection = yield* Effect.acquireRelease(
connectToDatabase(configData),
(conn) => Effect.sync(() => conn.close()) // Cleanup
);
return Database.of({
query: (sql) => executeQuery(connection, sql)
});
})
);
Combine independent layers:
import { Context, Layer } from 'effect';
declare class Config extends Context.Service<Config, {}>()('Config') {}
declare class Logger extends Context.Service<Logger, {}>()('Logger') {}
declare const ConfigLive: Layer.Layer<Config, never, never>;
declare const LoggerLive: Layer.Layer<Logger, never, Config>;
// Layer<Config | Logger, never, Config>
// ▲ ▲ ▲
// │ │ └─ LoggerLive needs Config
// │ └─ No errors
// └─ Produces both Config and Logger
const AppConfigLive = Layer.merge(ConfigLive, LoggerLive);
Result combines:
never | Config = Config)Config | Logger)Chain dependent layers:
import { Context, Layer } from 'effect';
declare class Config extends Context.Service<Config, {}>()('Config') {}
declare class Logger extends Context.Service<Logger, {}>()('Logger') {}
declare const ConfigLive: Layer.Layer<Config, never, never>;
declare const LoggerLive: Layer.Layer<Logger, never, Config>;
// Layer<Logger, never, never>
// ▲ ▲ ▲
// │ │ └─ ConfigLive satisfies LoggerLive's requirement
// │ └─ No errors
// └─ Only Logger in output
const FullLoggerLive = Layer.provide(LoggerLive, ConfigLive);
Result:
never)Logger)Compose defaultLayer directly unless you have a real module-evaluation or circular-import problem. Most services do not need deferred composition.
import { Layer } from 'effect';
// Raw layer — declares its dependencies in the type
export const layer: Layer.Layer<MyService, never, DepA | DepB> = Layer.effect(
MyService,
Effect.gen(function* () {
const depA = yield* DepA;
const depB = yield* DepB;
return MyService.of({
/* ... */
});
})
);
// Fully-wired layer — compose directly in the normal case
export const defaultLayer = layer.pipe(
Layer.provide(DepA.defaultLayer),
Layer.provide(DepB.defaultLayer)
);
Use Layer.suspend(() => ...) when import evaluation order genuinely requires deferral:
import { Layer } from 'effect';
export const defaultLayer = Layer.suspend(() =>
layer.pipe(Layer.provide(Dep.defaultLayer))
);
Layer.unwrap(Effect.sync(...)) still works, but it is not the universal default. Reach for deferred composition only when the dependency graph actually needs it.
Naming convention:
layer — exposes the service's true dependency graph in its type signature. Tests compose against layer directly, providing mock layers.defaultLayer — the fully-wired production composition with all dependencies satisfied. Only define defaultLayer when layer has unsatisfied requirements. Self-contained layers (no external dependencies) export just layer.Use deferred composition only for:
If none of those apply, compose directly.
Build applications in layers:
import { Context, Layer } from 'effect';
declare class Config extends Context.Service<Config, {}>()('Config') {}
declare class Database extends Context.Service<Database, {}>()('Database') {}
declare class Cache extends Context.Service<Cache, {}>()('Cache') {}
declare class PaymentDomain extends Context.Service<PaymentDomain, {}>()(
'PaymentDomain'
) {}
declare class OrderDomain extends Context.Service<OrderDomain, {}>()(
'OrderDomain'
) {}
declare class PaymentGateway extends Context.Service<PaymentGateway, {}>()(
'PaymentGateway'
) {}
declare class NotificationService extends Context.Service<
NotificationService,
{}
>()('NotificationService') {}
declare const ConfigLive: Layer.Layer<Config, never, never>;
declare const DatabaseLive: Layer.Layer<Database, never, Config>;
declare const CacheLive: Layer.Layer<Cache, never, Config>;
declare const PaymentDomainLive: Layer.Layer<PaymentDomain, never, Database>;
declare const OrderDomainLive: Layer.Layer<OrderDomain, never, Database>;
declare const PaymentGatewayLive: Layer.Layer<
PaymentGateway,
never,
PaymentDomain
>;
declare const NotificationServiceLive: Layer.Layer<
NotificationService,
never,
OrderDomain
>;
// Infrastructure: No dependencies
const InfrastructureLive = Layer.mergeAll(
ConfigLive, // Layer<Config, never, never>
DatabaseLive, // Layer<Database, never, Config>
CacheLive // Layer<Cache, never, Config>
).pipe(
Layer.provide(ConfigLive) // Satisfy Config requirement
);
// Domain: Depends on infrastructure
const DomainLive = Layer.mergeAll(
PaymentDomainLive, // Layer<PaymentDomain, never, Database>
OrderDomainLive // Layer<OrderDomain, never, Database>
).pipe(Layer.provide(InfrastructureLive));
// Application: Depends on domain
const ApplicationLive = Layer.mergeAll(
PaymentGatewayLive,
NotificationServiceLive
).pipe(Layer.provide(DomainLive));
Switch implementations for different environments:
import { Context, Effect, Layer } from 'effect';
interface Connection {
readonly close: () => void;
}
export class Database extends Context.Service<
Database,
{
readonly query: (sql: string) => Effect.Effect<{ rows: unknown[] }>;
}
>()('Database') {}
declare const connectToProduction: () => Effect.Effect<Connection>;
declare const createDatabaseService: (connection: Connection) => {
readonly query: (sql: string) => Effect.Effect<{ rows: unknown[] }>;
};
declare const myProgram: Effect.Effect<void, never, Database>;
// Production
export const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const connection = yield* connectToProduction();
return createDatabaseService(connection);
})
);
// Test
export const DatabaseTest = Layer.succeed(
Database,
Database.of({
query: () => Effect.succeed({ rows: [] })
})
);
// Use in application
const program = Effect.gen(function* () {
const nodeEnv = yield* Config.string('NODE_ENV').pipe(
Config.withDefault('production')
);
yield* myProgram.pipe(
Effect.provide(nodeEnv === 'test' ? DatabaseTest : DatabaseLive)
);
});
Layers are memoized - same instance shared across program:
import { Context, Effect, Layer } from 'effect';
declare class Config extends Context.Service<
Config,
{ readonly value: string }
>()('Config') {}
declare const ConfigLive: Layer.Layer<Config, never, never>;
// Config is constructed once and shared
const program = Effect.all([
Effect.gen(function* () {
const config = yield* Config;
// Uses shared instance
}),
Effect.gen(function* () {
const config = yield* Config;
// Same instance
})
]).pipe(Effect.provide(ConfigLive));
Memo-map fork nuance: Sharing is mediated by a
MemoMap. A root memo map (Layer.makeMemoMapUnsafe(), used implicitly byEffect.provide) shares every layer allocation it builds. A forked memo map (Layer.forkMemoMap/Layer.forkMemoMapUnsafe) can still see allocations its parent already built, but new allocations it builds stay isolated and are not written back to the parent. This is the mechanism@effect/vitestuses to reuse parent layers while isolating nestedit.layersuites.
Handle construction errors:
import { Context, Effect, Layer, Schema } from 'effect';
interface Connection {
readonly close: () => void;
}
class ConnectionError extends Schema.TaggedErrorClass<ConnectionError>()(
'ConnectionError',
{
message: Schema.String
}
) {}
class DatabaseConstructionError extends Schema.TaggedErrorClass<DatabaseConstructionError>()(
'DatabaseConstructionError',
{ cause: ConnectionError }
) {}
export class Database extends Context.Service<
Database,
{
readonly query: (sql: string) => Effect.Effect<unknown>;
}
>()('Database') {}
declare const connectToDatabase: () => Effect.Effect<
Connection,
ConnectionError
>;
declare const createDatabaseService: (connection: Connection) => {
readonly query: (sql: string) => Effect.Effect<unknown>;
};
export const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const connection = yield* connectToDatabase().pipe(
Effect.catchTag('ConnectionError', (error) =>
Effect.fail(new DatabaseConstructionError({ cause: error }))
)
);
return createDatabaseService(connection);
})
);
Layer.provideMerge satisfies dependencies AND passes them through to the output. This is critical for test layer stacks where multiple downstream layers need access to the same upstream services.
import { Layer } from 'effect';
declare const ConfigLayer: Layer.Layer<Config>;
declare const DatabaseLayer: Layer.Layer<Database, never, Config>;
declare const UserServiceLayer: Layer.Layer<UserService, never, Database>;
// Layer.provide — satisfies requirement, REMOVES it from output
const db = DatabaseLayer.pipe(Layer.provide(ConfigLayer));
// db: Layer<Database> — Config is NOT in the output
// Layer.provideMerge — satisfies requirement, KEEPS it in output
const dbWithConfig = DatabaseLayer.pipe(Layer.provideMerge(ConfigLayer));
// dbWithConfig: Layer<Database | Config> — Config remains available
Use Layer.provideMerge when building test layer stacks where multiple layers share the same dependencies:
import { Layer } from 'effect';
// Test layer composition — downstream layers need Config AND Database
const infra = Layer.mergeAll(ConfigLayer, DatabaseLayer).pipe(
Layer.provideMerge(ConfigLayer) // Config stays visible for downstream
);
// Both UserService and OrderService can access Config and Database
const services = Layer.mergeAll(UserServiceLayer, OrderServiceLayer).pipe(
Layer.provideMerge(infra)
);
Without Layer.provideMerge, you would need to manually merge every intermediate layer to keep services visible to downstream consumers.
For services that need atomic state transitions with concurrent callers, use SynchronizedRef.modifyEffect combined with Deferred for result sharing:
import { Deferred, Effect, Fiber, Scope, SynchronizedRef } from 'effect';
type State<A, E> =
| { readonly _tag: 'Idle' }
| {
readonly _tag: 'Running';
readonly done: Deferred.Deferred<A, E>;
readonly fiber: Fiber.Fiber<A, E>;
}
| { readonly _tag: 'Pending'; readonly done: Deferred.Deferred<A, E> };
const make = <A, E>(scope: Scope.Scope) => {
const ref = SynchronizedRef.makeUnsafe<State<A, E>>({ _tag: 'Idle' });
const run = (work: Effect.Effect<A, E>) =>
SynchronizedRef.modifyEffect(
ref,
Effect.fnUntraced(function* (state) {
switch (state._tag) {
case 'Running':
// Already running — share the existing result
return [Deferred.await(state.done), state];
case 'Idle': {
// Start new work
const done = yield* Deferred.make<A, E>();
const fiber = yield* Effect.forkIn(
work.pipe(Effect.intoDeferred(done)),
scope
);
return [
Deferred.await(done),
{ _tag: 'Running' as const, done, fiber }
];
}
case 'Pending': {
// Queued — share the pending result
return [Deferred.await(state.done), state];
}
}
})
).pipe(Effect.flatten);
return { run };
};
Key properties:
SynchronizedRef.modifyEffect — atomically reads state, runs an effect, and updates state in one operation. No other caller can interleave.Deferred — shares the result of in-flight work with concurrent callers who arrive while it's running.Effect.forkIn(work, scope) — ties the worker fiber to the service scope, not the calling fiber.work, with all callers sharing the same result.Use this pattern when:
*Live - Production implementation*Test - Test implementation*Mock - Mock for testingService.of({...}) used when returning from Layer.effect, never a plain objectacquireRelease or addFinalizer if neededdefaultLayer only present when layer has unsatisfied requirementsdefaultLayer composes directly unless deferred evaluation is truly requiredLayers should make dependency management explicit while keeping service interfaces clean and focused.