Skip to main content

The Golden Rule

Compose layers first, then provide once. Don’t chain multiple Effect.provide calls.
// ❌ Anti-pattern: multiple provides
program.pipe(
  Effect.provide(Signer.Live),
  Effect.provide(Provider),
  Effect.provide(HttpTransport(rpcUrl))
)

// ✅ Correct: compose layers once, then provide
const AppLayer = Layer.mergeAll(
  Signer.Live,
  Provider
).pipe(Layer.provide(HttpTransport(rpcUrl)))

program.pipe(Effect.provide(AppLayer))

Why Layer Composition Matters

  1. Efficiency - Layers built together share resources
  2. Correctness - Dependencies resolved properly
  3. Testability - Swap entire layer graphs for testing
  4. Reusability - Define layers once, use everywhere

Composition Patterns

Pattern 1: Layer.mergeAll for Independent Layers

When layers don’t depend on each other:
import { Layer } from 'effect'
import { Secp256k1Live, KeccakLive, AesGcmLive } from 'voltaire-effect/crypto'

const CryptoLayer = Layer.mergeAll(Secp256k1Live, KeccakLive, AesGcmLive)

Pattern 2: Layer.provide for Dependencies

When one layer depends on another:
import { Layer } from 'effect'
import { Provider, HttpTransport } from 'voltaire-effect'

// Provider needs HttpTransport
const ProviderLayer = Provider.pipe(
  Layer.provide(HttpTransport('https://eth.llamarpc.com'))
)

Pattern 3: Layer.provideMerge for Incremental Building

Add services while providing dependencies:
import { Layer } from 'effect'
import { Signer, Provider, HttpTransport, LocalAccount } from 'voltaire-effect'
import { Secp256k1Live, KeccakLive } from 'voltaire-effect/crypto'

const CryptoLayer = Layer.mergeAll(Secp256k1Live, KeccakLive)
const TransportLayer = HttpTransport('https://eth.llamarpc.com')
const ProviderLayer = Provider.pipe(Layer.provide(TransportLayer))

const SignerLayer = Layer.mergeAll(
  Signer.Live,
  CryptoLayer,
  ProviderLayer
).pipe(
  Layer.provideMerge(LocalAccount(privateKey))
)

Complete Examples

Read-Only Provider

import { Effect, Layer } from 'effect'
import { getBlockNumber, Provider, HttpTransport } from 'voltaire-effect'

const ProviderLayer = Provider.pipe(
  Layer.provide(HttpTransport('https://eth.llamarpc.com'))
)

const program = Effect.gen(function* () {
  return yield* getBlockNumber()
})

await Effect.runPromise(program.pipe(Effect.provide(ProviderLayer)))

Full Wallet with Signing

import { Effect, Layer } from 'effect'
import { Signer, Provider, HttpTransport, LocalAccount } from 'voltaire-effect'
import { Secp256k1Live, KeccakLive } from 'voltaire-effect/crypto'

const CryptoLayer = Layer.mergeAll(Secp256k1Live, KeccakLive)
const TransportLayer = HttpTransport('https://eth.llamarpc.com')
const ProviderLayer = Provider.pipe(Layer.provide(TransportLayer))

const WalletLayer = Layer.mergeAll(
  Signer.Live,
  CryptoLayer,
  ProviderLayer
).pipe(
  Layer.provideMerge(LocalAccount(privateKey))
)

const program = Effect.gen(function* () {
  const signer = yield* SignerService
  return yield* signer.sendTransaction({ to, value })
})

await Effect.runPromise(program.pipe(Effect.provide(WalletLayer)))

Test Layer Substitution

import { Effect, Layer } from 'effect'
import { ProviderService } from 'voltaire-effect'
import { CryptoTest } from 'voltaire-effect/crypto'

// Mock provider
const MockProvider = Layer.succeed(ProviderService, {
  request: (method: string) => {
    if (method === 'eth_blockNumber') return Effect.succeed('0x3039')
    return Effect.succeed(null)
  }
})

// Compose test layer
const TestLayer = Layer.mergeAll(MockProvider, CryptoTest)

// Same program, different layer
await Effect.runPromise(program.pipe(Effect.provide(TestLayer)))

Layer Reuse Across Files

Define layers in a shared module:
// layers.ts
import { Layer } from 'effect'
import { Provider, HttpTransport, Signer, LocalAccount } from 'voltaire-effect'
import { Secp256k1Live, KeccakLive } from 'voltaire-effect/crypto'

export const CryptoLayer = Layer.mergeAll(Secp256k1Live, KeccakLive)

export const makeProviderLayer = (rpcUrl: string) =>
  Provider.pipe(Layer.provide(HttpTransport(rpcUrl)))

export const makeSignerLayer = (privateKey: Uint8Array, rpcUrl: string) => {
  const transportLayer = HttpTransport(rpcUrl)
  const providerLayer = Provider.pipe(Layer.provide(transportLayer))
  
  return Layer.mergeAll(
    Signer.Live,
    CryptoLayer,
    providerLayer
  ).pipe(Layer.provideMerge(LocalAccount(privateKey)))
}
// usage.ts
import { makeSignerLayer } from './layers'

const layer = makeSignerLayer(myPrivateKey, 'https://eth.llamarpc.com')
await Effect.runPromise(program.pipe(Effect.provide(layer)))

Common Mistakes

Mistake 1: Providing Same Dependency Multiple Times

// ❌ KeccakLive provided twice
program.pipe(
  Effect.provide(Secp256k1Live),
  Effect.provide(KeccakLive),
  Effect.provide(MnemonicAccount(mnemonic)),
  Effect.provide(KeccakLive)  // Duplicate!
)

// ✅ Compose once
const CryptoLayer = Layer.mergeAll(Secp256k1Live, KeccakLive)
const AccountLayer = MnemonicAccount(mnemonic).pipe(Layer.provide(CryptoLayer))
program.pipe(Effect.provide(AccountLayer))

Mistake 2: Order-Dependent Provides

// ❌ Fragile - order matters
program.pipe(
  Effect.provide(A),
  Effect.provide(B),  // B depends on A?
  Effect.provide(C)   // C depends on A and B?
)

// ✅ Explicit dependencies
const BLayer = B.pipe(Layer.provide(A))
const CLayer = C.pipe(Layer.provide(Layer.mergeAll(A, BLayer)))
program.pipe(Effect.provide(CLayer))

See Also