| 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
Layer<RequirementsOut, Error, RequirementsIn>
▲ ▲ ▲
│ │ └─ What this layer needs
│ └─ Errors during construction
└─ What this layer produces
Pattern: Simple Layer (No Dependencies)
export class Config extends Context.Tag("Config")<
Config,
{
readonly getConfig: Effect.Effect<ConfigData>
}
>() {}
export const ConfigLive = Layer.succeed(
Config,
Config.of({
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://localhost/db"
})
})
)
Pattern: Layer with Dependencies
export class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (message: string) => Effect.Effect<void> }
>() {}
export const LoggerLive = Layer.effect(
Logger,
Effect.gen(function* () {
const config = yield* Config
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.
export const DatabaseLive = Layer.scoped(
Database,
Effect.gen(function* () {
const config = yield* Config
const connection = yield* Effect.acquireRelease(
connectToDatabase(config),
(conn) => Effect.sync(() => conn.close())
)
return Database.of({
query: (sql) => executeQuery(connection, sql)
})
})
)
Composing Layers: Merge vs Provide
Merge (Parallel Composition)
Combine independent layers:
const AppConfigLive = Layer.merge(ConfigLive, LoggerLive)
Result combines:
- Requirements: Union (
never | Config = Config)
- Outputs: Union (
Config | Logger)
Provide (Sequential Composition)
Chain dependent layers:
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:
const InfrastructureLive = Layer.mergeAll(
ConfigLive,
DatabaseLive,
CacheLive
).pipe(
Layer.provide(ConfigLive)
)
const DomainLive = Layer.mergeAll(
PaymentDomainLive,
OrderDomainLive,
).pipe(
Layer.provide(InfrastructureLive)
)
const ApplicationLive = Layer.mergeAll(
PaymentGatewayLive,
NotificationServiceLive
).pipe(
Layer.provide(DomainLive)
)
Pattern: Multiple Implementations
Switch implementations for different environments:
export const DatabaseLive = Layer.scoped(
Database,
Effect.gen(function* () {
const connection = yield* connectToProduction()
return createDatabaseService(connection)
})
)
export const DatabaseTest = Layer.succeed(
Database,
Database.of({
query: () => Effect.succeed({ rows: [] })
})
)
const program = myProgram.pipe(
Effect.provide(process.env.NODE_ENV === "test" ? DatabaseTest : DatabaseLive)
)
Pattern: Layer Sharing
Layers are memoized - same instance shared across program:
const program = Effect.all([
Effect.gen(function* () {
const config = yield* Config
}),
Effect.gen(function* () {
const config = yield* Config
})
]).pipe(Effect.provide(ConfigLive))
Error Handling in Layers
Handle construction errors:
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
Layers should make dependency management explicit while keeping service interfaces clean and focused.