--- name: layer-design description: Design and compose Effect layers for clean dependency management --- # Layer Design Skill Create layers that construct services while managing their dependencies cleanly. ## Layer Structure ```typescript import { Layer } from "effect" // Layer // ▲ ▲ ▲ // │ │ └─ What this layer needs // │ └─ Errors during construction // └─ What this layer produces ``` ## Pattern: Simple Layer (No Dependencies) ```typescript import { Context, Effect, Layer } from "effect" interface ConfigData { readonly logLevel: string readonly connection: string } export class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect } >() {} // Layer // ▲ ▲ ▲ // │ │ └─ No dependencies // │ └─ Cannot fail // └─ Produces Config export const ConfigLive = Layer.succeed( Config, Config.of({ getConfig: Effect.succeed({ logLevel: "INFO", connection: "mysql://localhost/db" }) }) ) ``` ## Pattern: Layer with Dependencies ```typescript import { Context, Effect, Layer, Console } from "effect" interface ConfigData { readonly logLevel: string readonly connection: string } export class Config extends Context.Tag("Config")< Config, { readonly getConfig: Effect.Effect } >() {} export class Logger extends Context.Tag("Logger")< Logger, { readonly log: (message: string) => Effect.Effect } >() {} // Layer // ▲ ▲ ▲ // │ │ └─ 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}`) }) }) }) ) ``` ## Pattern: Layer with Resource Management Use `Layer.scoped` when resources need cleanup: - Database connections - File handles, network connections - Any resource requiring `Effect.acquireRelease` or `addFinalizer` for cleanup Use `Layer.effect` for stateless services without cleanup needs. ```typescript 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.Tag("Config")< Config, { readonly getConfig: Effect.Effect } >() {} export class Database extends Context.Tag("Database")< Database, { readonly query: (sql: string) => Effect.Effect } >() {} declare const connectToDatabase: (config: ConfigData) => Effect.Effect declare const executeQuery: (connection: Connection, sql: string) => Effect.Effect // Layer export const DatabaseLive = Layer.scoped( Database, Effect.gen(function* () { const config = yield* Config const configData = yield* config.getConfig // Acquire resource with automatic release const connection = yield* Effect.acquireRelease( connectToDatabase(configData), (conn) => Effect.sync(() => conn.close()) // Cleanup ) return Database.of({ query: (sql) => executeQuery(connection, sql) }) }) ) ``` ## Composing Layers: Merge vs Provide ### Merge (Parallel Composition) Combine independent layers: ```typescript import { Context, Layer } from "effect" declare class Config extends Context.Tag("Config") {} declare class Logger extends Context.Tag("Logger") {} declare const ConfigLive: Layer.Layer declare const LoggerLive: Layer.Layer // Layer // ▲ ▲ ▲ // │ │ └─ LoggerLive needs Config // │ └─ No errors // └─ Produces both Config and Logger const AppConfigLive = Layer.merge(ConfigLive, LoggerLive) ``` Result combines: - **Requirements**: Union (`never | Config = Config`) - **Outputs**: Union (`Config | Logger`) ### Provide (Sequential Composition) Chain dependent layers: ```typescript import { Context, Layer } from "effect" declare class Config extends Context.Tag("Config") {} declare class Logger extends Context.Tag("Logger") {} declare const ConfigLive: Layer.Layer declare const LoggerLive: Layer.Layer // Layer // ▲ ▲ ▲ // │ │ └─ ConfigLive satisfies LoggerLive's requirement // │ └─ No errors // └─ Only Logger in output const FullLoggerLive = Layer.provide(LoggerLive, ConfigLive) ``` Result: - **Requirements**: Outer layer's requirements (`never`) - **Output**: Inner layer's output (`Logger`) ## Pattern: Layered Architecture Build applications in layers: ```typescript import { Context, Layer } from "effect" declare class Config extends Context.Tag("Config") {} declare class Database extends Context.Tag("Database") {} declare class Cache extends Context.Tag("Cache") {} declare class PaymentDomain extends Context.Tag("PaymentDomain") {} declare class OrderDomain extends Context.Tag("OrderDomain") {} declare class PaymentGateway extends Context.Tag("PaymentGateway") {} declare class NotificationService extends Context.Tag("NotificationService") {} declare const ConfigLive: Layer.Layer declare const DatabaseLive: Layer.Layer declare const CacheLive: Layer.Layer declare const PaymentDomainLive: Layer.Layer declare const OrderDomainLive: Layer.Layer declare const PaymentGatewayLive: Layer.Layer declare const NotificationServiceLive: Layer.Layer // Infrastructure: No dependencies const InfrastructureLive = Layer.mergeAll( ConfigLive, // Layer DatabaseLive, // Layer CacheLive // Layer ).pipe( Layer.provide(ConfigLive) // Satisfy Config requirement ) // Domain: Depends on infrastructure const DomainLive = Layer.mergeAll( PaymentDomainLive, // Layer OrderDomainLive, // Layer ).pipe( Layer.provide(InfrastructureLive) ) // Application: Depends on domain const ApplicationLive = Layer.mergeAll( PaymentGatewayLive, NotificationServiceLive ).pipe( Layer.provide(DomainLive) ) ``` ## Pattern: Multiple Implementations Switch implementations for different environments: ```typescript import { Context, Effect, Layer } from "effect" interface Connection { readonly close: () => void } export class Database extends Context.Tag("Database")< Database, { readonly query: (sql: string) => Effect.Effect<{ rows: unknown[] }> } >() {} declare const connectToProduction: () => Effect.Effect declare const createDatabaseService: (connection: Connection) => { readonly query: (sql: string) => Effect.Effect<{ rows: unknown[] }> } declare const myProgram: Effect.Effect // Production export const DatabaseLive = Layer.scoped( 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 = myProgram.pipe( Effect.provide(process.env.NODE_ENV === "test" ? DatabaseTest : DatabaseLive) ) ``` ## Pattern: Layer Sharing Layers are memoized - same instance shared across program: ```typescript import { Context, Effect, Layer } from "effect" declare class Config extends Context.Tag("Config") {} declare const ConfigLive: Layer.Layer // 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)) ``` ## Error Handling in Layers Handle construction errors: ```typescript import { Context, Effect, Layer, Data } from "effect" interface Connection { readonly close: () => void } class ConnectionError extends Data.TaggedError("ConnectionError")<{ readonly message: string }> {} class DatabaseConstructionError extends Data.TaggedError("DatabaseConstructionError")<{ readonly cause: ConnectionError }> {} export class Database extends Context.Tag("Database")< Database, { readonly query: (sql: string) => Effect.Effect } >() {} declare const connectToDatabase: () => Effect.Effect declare const createDatabaseService: (connection: Connection) => { readonly query: (sql: string) => Effect.Effect } 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) }) ) ``` ## Naming Convention - `*Live` - Production implementation - `*Test` - Test implementation - `*Mock` - Mock for testing - Descriptive names for specialized implementations ## Quality Checklist - [ ] Layer type accurately reflects dependencies - [ ] Resource cleanup using `acquireRelease` if needed - [ ] Layer can be tested with mock dependencies - [ ] No dependency leakage into service interface - [ ] Appropriate use of merge vs provide - [ ] Error handling for construction failures - [ ] JSDoc with example usage Layers should make dependency management explicit while keeping service interfaces clean and focused.