| 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
import { Layer } from "effect"
// Layer<RequirementsOut, Error, RequirementsIn>
// ▲ ▲ ▲
// │ │ └─ What this layer needs
// │ └─ Errors during construction
// └─ What this layer produces
Pattern: Simple Layer (No Dependencies)
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<ConfigData>
}
>() {}
// 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"
})
})
)
Pattern: Layer with Dependencies
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<ConfigData>
}
>() {}
export class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (message: string) => Effect.Effect<void> }
>() {}
// 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}`)
})
})
})
)
Pattern: Layer with Resource Management
Use Layer.scoped when resources need cleanup:
- Database connections
- File handles, network connections
- Any resource requiring
Effect.acquireReleaseoraddFinalizerfor cleanup
Use Layer.effect for stateless services without cleanup needs.
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<ConfigData>
}
>() {}
export class Database extends Context.Tag("Database")<
Database,
{
readonly query: (sql: string) => Effect.Effect<unknown, DatabaseError>
}
>() {}
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.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:
import { Context, Layer } from "effect"
declare class Config extends Context.Tag("Config")<Config, {}> {}
declare class Logger extends Context.Tag("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:
- Requirements: Union (
never | Config = Config) - Outputs: Union (
Config | Logger)
Provide (Sequential Composition)
Chain dependent layers:
import { Context, Layer } from "effect"
declare class Config extends Context.Tag("Config")<Config, {}> {}
declare class Logger extends Context.Tag("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:
- Requirements: Outer layer's requirements (
never) - Output: Inner layer's output (
Logger)
Pattern: Layered Architecture
Build applications in layers:
import { Context, Layer } from "effect"
declare class Config extends Context.Tag("Config")<Config, {}> {}
declare class Database extends Context.Tag("Database")<Database, {}> {}
declare class Cache extends Context.Tag("Cache")<Cache, {}> {}
declare class PaymentDomain extends Context.Tag("PaymentDomain")<PaymentDomain, {}> {}
declare class OrderDomain extends Context.Tag("OrderDomain")<OrderDomain, {}> {}
declare class PaymentGateway extends Context.Tag("PaymentGateway")<PaymentGateway, {}> {}
declare class NotificationService extends Context.Tag("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)
)
Pattern: Multiple Implementations
Switch implementations for different environments:
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<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.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:
import { Context, Effect, Layer } from "effect"
declare class Config extends Context.Tag("Config")<Config, { readonly value: string }> {}
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))
Error Handling in Layers
Handle construction errors:
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<unknown>
}
>() {}
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)
})
)
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
acquireReleaseif 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.