layer-design

Design and compose Effect layers for clean dependency management

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "layer-design" with this command: npx skills add front-depiction/claude-setup/front-depiction-claude-setup-layer-design

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.acquireRelease or addFinalizer for 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 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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

command-executor

No summary provided by upstream source.

Repository SourceNeeds Review
General

react-composition

No summary provided by upstream source.

Repository SourceNeeds Review
General

effect-ai-streaming

No summary provided by upstream source.

Repository SourceNeeds Review