Claude-skill-registry layer-design
Design and compose Effect layers for clean dependency management
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/layer-design" ~/.claude/skills/majiayu000-claude-skill-registry-layer-design && rm -rf "$T"
manifest:
skills/data/layer-design/SKILL.mdsource content
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> } >() {} // 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
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 console.log(`[${logLevel}] ${message}`) }) }) }) )
Pattern: Layer with Resource Management
Use
Layer.scoped for resources that need cleanup:
// Layer<Database, DatabaseError, Config> export const DatabaseLive = Layer.scoped( Database, Effect.gen(function* () { const config = yield* Config // Acquire resource with automatic release const connection = yield* Effect.acquireRelease( connectToDatabase(config), (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:
// 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:
// 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:
// 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:
// 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:
// 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:
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
- Production implementation*Live
- Test implementation*Test
- Mock for testing*Mock- Descriptive names for specialized implementations
Quality Checklist
- Layer type accurately reflects dependencies
- Resource cleanup using
if neededacquireRelease - 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.