Tutorial: Layers
Layers are for construction time, not business logic time.
Use a layer when you need to:
- build an environment from other services or config
- fail during provisioning before the workflow starts
- own resources that must be cleaned up exactly once
- compose several independent startup steps in parallel
1. The Workflow Still Targets An Environment
open System
open System.Threading
open System.Threading.Tasks
open FsFlow
type IOrders =
abstract Save : string -> Task<unit>
type IClock =
abstract UtcNow : unit -> DateTimeOffset
type AppEnv =
{ Orders: IOrders
Clock: IClock }
let saveOrder (orderId: string) : Flow<AppEnv, string, unit> =
flow {
let! env = Flow.env
do! env.Orders.Save orderId
let now = env.Clock.UtcNow()
printfn "[%O] saved %s" now orderId
}
The workflow still depends on AppEnv. Layers only change how AppEnv gets built.
2. Build Small Layers
let ordersLayer : Layer<unit, string, IOrders> =
Layer.succeed
{ new IOrders with
member _.Save orderId =
task {
// Imagine the real dependency here: open connection, transaction, etc.
printfn "persisting %s" orderId
} }
let clockLayer : Layer<unit, string, IClock> =
Layer.succeed
{ new IClock with
member _.UtcNow() = DateTimeOffset.UtcNow }
3. Merge Them Into An App Layer
let appLayer : Layer<unit, string, AppEnv> =
layer {
let! orders = ordersLayer
and! clock = clockLayer
return
{ Orders = orders
Clock = clock }
}
Use plain let! when one provisioning step depends on another. Use sibling and! when the steps are independent.
4. Provision Failure Happens Before Business Logic
let failingOrdersLayer : Layer<unit, string, IOrders> =
Layer.fromTask (fun _ _ ->
task {
return Exit.Failure (Cause.Fail "database connection string missing")
})
If provisioning fails, Flow.provide never runs the downstream business workflow. That separation is one of the main reasons to use layers.
5. Resource Ownership
type FakeConnection() =
interface IAsyncDisposable with
member _.DisposeAsync() =
ValueTask(Task.CompletedTask)
let connectionLayer : Layer<unit, string, FakeConnection> =
Layer.acquireRelease
(Layer.succeed (new FakeConnection()))
(fun connection _ct -> connection.DisposeAsync().AsTask())
This is another main reason to use layers: acquired resources belong to the provisioning scope and are released when the provided workflow completes, fails, or is interrupted.
6. Run Through Flow.provide
let run () = task {
let! exit =
saveOrder "A-100"
|> Flow.provide appLayer
|> fun flow -> flow.ToTask(())
match exit with
| Exit.Success () -> printfn "done"
| Exit.Failure cause -> printfn "failed: %A" cause
}
The call site stays small:
- construct or choose the layer
- provide it once
- run the workflow
That is much cleaner than manually opening and closing startup resources around every feature entry point.