Layers

Provisioning explicit environments with Layer and Flow.provide.

A Layer<'input, 'error, 'output> builds an environment or service bundle from an input value. It runs inside a Scope, so resources acquired during provisioning can be finalized when the provided flow finishes.

let appFlow : Flow<AppEnv, AppError, unit> =
    placeOrder order

let runnable : Flow<IServiceProvider, AppError, unit> =
    appFlow |> Flow.provide appLayer

Primary Shape

Use layer { } for application environment construction:

let appLayer =
    layer {
        let! runtime = BaseRuntime.fromServiceProvider
        and! orders = ordersLayer

        return { Runtime = runtime; Orders = orders }
    }

Plain let! is sequential and dependent. Sibling and! bindings are independent and use Layer.merge, which provisions branches in parallel through child scopes.

Layer Surface

The core layer surface is:

Layer.succeed value
Layer.read projection
Layer.fromValueTask provision
Layer.map mapper layer
Layer.mapError mapper layer
Layer.bind binder layer
Layer.zip left right
Layer.zipPar left right
Layer.merge left right
Layer.map2 mapper left right
Layer.map3 mapper left middle right

Use Layer.succeed for already-built values, Layer.fromValueTask when construction can fail or register cleanup, and Layer.bind / layer { let! } when the next provisioning step depends on an earlier value.

Example

open System.Threading.Tasks

type AppEnv =
    { Runtime: BaseRuntime
      Orders: IOrderRepository }

    interface IHas<IClock> with member this.Service = this.Runtime.Clock
    interface IHas<ILog> with member this.Service = this.Runtime.Log
    interface IHas<IOrderRepository> with member this.Service = this.Orders

let ordersLayer : Layer<IServiceProvider, BaseRuntimeError, IOrderRepository> =
    Layer.fromValueTask (fun (provider, _) _ ->
        match provider.GetService(typeof<IOrderRepository>) with
        | null ->
            ValueTask(Exit.Failure (Cause.Fail (BaseRuntimeError.MissingService "IOrderRepository")))
        | service ->
            ValueTask(Exit.Success (service :?> IOrderRepository)))

let appLayer : Layer<IServiceProvider, BaseRuntimeError, AppEnv> =
    layer {
        let! runtime = BaseRuntime.fromServiceProvider
        and! orders = ordersLayer

        return
        { Runtime = runtime
          Orders = orders }
    }

Layer error types must match the flow error type. When different provisioning steps use different errors, map them into one startup error type before calling Flow.provide.

let! And and!

Use let! when provisioning is dependent:

let ordersLayerFromConfig config : Layer<IServiceProvider, BaseRuntimeError, IOrderRepository> =
    Layer.fromValueTask (fun (provider, scope) cancellationToken ->
        // Build or resolve the repository from config, provider, and scope.
        provisionOrders config provider scope cancellationToken)

let appLayer =
    layer {
        let! config = configLayer
        let! orders = ordersLayerFromConfig config

        return { Orders = orders }
    }

Use sibling and! when provisioning is independent:

let appLayer =
    layer {
        let! runtime = BaseRuntime.fromServiceProvider
        and! orders = ordersLayer

        return { Runtime = runtime; Orders = orders }
    }

An and! sibling cannot depend on a value introduced by another sibling. That remains an ordinary F# compile-time scope error, which is the desired signal: if a service needs another value, separate it into a prior let!.

zip, zipPar, And merge

Layer.zip provisions left then right, sequentially. Use it when ordering is intentional.

Layer.zipPar provisions both sides independently in parallel and returns a tuple.

Layer.merge is the layer-domain name for zipPar. Prefer it when combining service bundles or environment fragments:

let combined =
    Layer.merge runtimeLayer ordersLayer
    |> Layer.map (fun (runtime, orders) -> { Runtime = runtime; Orders = orders })

Layer.merge does not automatically merge IHas<'service> contracts or synthesize a new environment type. It only provisions both sides and returns their outputs. Keep the final environment explicit:

type AppEnv =
    { Runtime: BaseRuntime
      Orders: IOrderRepository }

    interface IHas<IClock> with member this.Service = this.Runtime.Clock
    interface IHas<IOrderRepository> with member this.Service = this.Orders

This keeps service requirements visible to people, the compiler, and LLMs. It also avoids ambiguous cases such as two services with the same implementation type. If an application needs multiple instances of the same service shape, give them named record fields or distinct nominal contracts rather than relying on tags.

Layer.map2 and Layer.map3 are sequential mapping helpers that avoid nested tuple reshaping. In a computation expression, sibling and! bindings use merge instead.

Cleanup

Flow.provide creates a root scope, builds the layer, runs the downstream flow, and closes the scope. Cleanup runs when the layer fails, the downstream flow fails, or the downstream flow succeeds.

Use Layer.acquireRelease when a layer provisions a service implementation or resource that must live for the whole provided flow:

let connectionLayer =
    Layer.acquireRelease
        (Layer.fromValueTask (fun (connectionString, _) _ ->
            openConnection connectionString
            |> Execution.ofValue))
        (fun connection _ ->
            connection.Dispose()
            Task.CompletedTask)

Parallel layer composition uses parent-owned child scopes. If one branch fails after another branch acquired resources, the acquired branch is finalized when the root scope closes. If both parallel branches fail, FsFlow preserves both failures as Cause.Both (leftCause, rightCause) rather than discarding one side.