F# Dependency Injection - how to compose dependencies with partial application and don't fail with tests

Dec 11, 2020 min read

One question you might ask yourself before starting a bigger project in F# How to inject dependencies? Let me show you how we used partial application to achieve loosely coupled testable components that can be tested in isolation or together in a broader perspective (acceptance tests). I will use Giraffe as the host, but the technique is free from any framework dependencies.

This post is part of the F# Advent Calendar 2020. Special thanks to Sergey Tihon for organizing this! Check out all the other great posts there!.

Introduction

First, let me make sure that you know what composition root is. I will take the definition from Dependency Injection Principles, Practices, and Patterns book [1]:

DEFINITION: A Composition Root is a single, logical location in an application where modules are composed together [1].

It also answers the important question “Where should we compose object graphs?” - “As close as possible to the application’s entry point.

Let’s create such a place in the Giraffe F# based project (a functional ASP.NET Core micro web framework [2]) together with .NET 5.0. We will be using the partial application to achieve our goal. Note that in functional programming we can also try different techniques: “Reader Monad” and the “Free Monad” but these techniques are far harder. I will tackle them in the future on my blog.

Why this post?

The post is heavily inspired by Scott Wlaschin’s post about Dependency Injection in F# [2]. It is great! It will give you strong fundaments on the topic. Mine post focuses heavily on implementation, Scott’s more on the partial application as IoC technique in general so make sure you read it and please come back - I have some answers for you once you will try to implement the described solution in a real project. There is a comment in the post by Konstantin at the bottom of the page. He asks about unit, integration, Acceptance Tests, and how it may look like in F# - I have the answer here. Also when I started to implement IoC at my work we quickly run into issues with testing that were caused by one piece composition root. I came across StackOverflow issue [3] which addressed my problem. It took me on the right track which I wanted to describe in the post. To fully understand the issue we have to face the problem first.

The sample solution

Everything is based on real code which is available on GitHub here: https://github.com/marcingolenia/FsharpComposition. You might want to clone the repository, to see how it is done. 100% real stuff, no bullshit:

  • .NET 5.0 with Giraffe based API.
  • Docker with PostgreSql + script that creates database and table (you have to run it by yourself).
  • Thoth.json for serializing objects. I like NoSQL in SQL :)
  • Composing dependencies using composition root and partial application that uses settings read from JSON file
  • Unit tests, integration tests, acceptance tests with FsUnit
  • Nice fake for HttpContext which allows e2e testing with Giraffe.
  • Id generation like Twitter Snowflake
  • Sample usage of FsToolkit.ErrorHandling for Async<Result<,>> code.
  • Demonstrates simple design of functional core, imperative shell. See point 7 for more.

I made this for you :) Don’t hesitate to use it or submit a PR with improvements!

Domain, application layer and httpHandler

Let’s imagine the following use case:

  1. Create a stock item with the name and amount of available items.
  2. Read the created item.

Besides, we will have some settings read from the json file (there are always some settings in the file right?) which composition root will use when composing up the dependencies to provide database connectivity or id generation.

Easy? Of course!

Let’s start with an extra easy domain StockItem.fs (don’t bother with the remove function - it is homework for you, I will mention it later):

 1namespace Stock
 2
 3module StockItem =
 4    
 5    type StockItemErrors = | OutOfStock | CannotReadStockItem
 6    type StockItemId = StockItemId of int64
 7    
 8    type StockItem = {
 9        Id: StockItemId
10        Name: string
11        AvailableAmount: int
12    }
13        
14    let create id name amount =
15       { Id = id; Name = name; AvailableAmount = amount }
16       
17    let (|GreaterThan|_|) k value = if value <= k then Some() else None
18        
19    let remove (stockItem: StockItem) amount: Result<StockItem, StockItemErrors> =
20        match amount with
21        | GreaterThan stockItem.AvailableAmount ->
22            { stockItem with AvailableAmount = stockItem.AvailableAmount - amount } |> Ok
23        | _ -> OutOfStock |> Error

Now let me present you the code that has the use cases StockItemWorkflows.fs (application layer):

 1namespace Stock.Application
 2
 3open Stock.StockItem
 4open FsToolkit.ErrorHandling
 5
 6module StockItemWorkflows =
 7    type IO = {
 8        ReadBy: StockItemId -> Async<Result<StockItem, string>>
 9        Update: StockItem -> Async<unit>
10        Insert: StockItem -> Async<unit>
11    }
12       
13    let remove io id amount: Async<Result<unit, StockItemErrors>> =
14        asyncResult {
15            let! stockItem = io.ReadBy (id |> StockItemId) |> AsyncResult.mapError(fun _ -> CannotReadStockItem)
16            let! stockItem = remove stockItem amount
17            do! io.Update stockItem
18        }
19    
20    let create io id name capacity: Async<unit> =
21        create (id |> StockItemId) name capacity
22        |> io.Insert

and one query in the same layer (let’s separate queries from commands ok?) StockItemById.fs

1module Queries.StockItemById
2
3type Query = bigint
4
5type Result = {
6    Id: int64
7    Name: string
8    AvailableAmount: int
9}

Let me skip the data access functions - their internal implementation is not important. If you want you can take a look at the code yourself. Having that code let’s see httpHandler.fs;

 1namespace Api
 2
 3open Api.Dtos
 4open FlexibleCompositionRoot
 5open Stock.StockItem
 6open Giraffe
 7open Microsoft.AspNetCore.Http
 8open FSharp.Control.Tasks.V2.ContextInsensitive
 9
10module HttpHandler =
11    let queryStockItemHandler queryStockItemBy (id: int64): HttpHandler =
12        fun (next: HttpFunc) (ctx: HttpContext) ->
13            task {
14                let! stockItem = queryStockItemBy (id |> Queries.StockItemById.Query)
15                return! match stockItem with
16                        | Some stockItem -> json stockItem next ctx
17                        | None -> RequestErrors.notFound (text "Not Found") next ctx
18            }
19            
20    //let removeFromStockItem ... (not important at the moment)
21
22    let createStockItemHandler createStockItem (createId: unit -> int64): HttpHandler =
23        fun (next: HttpFunc) (ctx: HttpContext) ->
24            let id = createId()
25            task {
26                let! stockItemDto = ctx.BindJsonAsync<CreateStockItemDto>()
27                do! createStockItem id stockItemDto.Name stockItemDto.Amount
28                ctx.SetHttpHeader "Location" (sprintf "/stockitem/%d" id)
29                return! Successful.created (text "Created") next ctx
30            }
31
32    let router (compositionRoot: ???): HttpFunc -> HttpContext -> HttpFuncResult =
33        choose [ GET >=> route "/" >=> htmlView Views.index
34                 GET >=> routef "/stockitem/%d" (queryStockItemHandler compositionRoot.QueryStockItemBy)
35                 PATCH >=> route "/stockitem/" >=> (removeFromStockItem compositionRoot.RemoveFromStock) 
36                 POST >=> route "/stockitem/" >=> (createStockItemHandler compositionRoot.CreateStockItem compositionRoot.GenerateId)
37                 setStatusCode 404 >=> text "Not Found" ]

I have placed three question marks - that is what we gonna do now.

Inflexible Composition root

Let’s start with the >=> GET >=> routef "/stockitem/%d" route. If you have read the Scott’s post[2] you should end up with something like that (at least the type should match); InflexibleCompositionRoot.fs

1  type InflexibleCompositionRoot =
2    { QueryStockItemBy: Queries.StockItemById.Query -> Async<Queries.StockItemById.Result option> }
3
4  let compose settings =
5    let createSqlConnection = DapperFSharp.createSqlConnection settings.SqlConnectionString
6    { QueryStockItemBy = StockItemQueryDao.readBy createSqlConnection }

This is basic stuff. Let me just show you the settings;

1module Settings
2
3[<CLIMutable>]
4type Settings = {
5    SqlConnectionString: string
6    IdGeneratorSettings: IdGenerator.Settings // we will use it for creating stock item
7}

If you will add asppsettings.json to the solution (make sure to have it copied on build - set that in fsproj) you can create the composition root in Program.fs read the settings and pass it to the router. This is how I do it; Program.fs

 1module Api.App
 2//... Some code that is not important now
 3//...
 4[<EntryPoint>]
 5let main args =
 6    let contentRoot = Directory.GetCurrentDirectory()
 7    let webRoot     = Path.Combine(contentRoot, "WebRoot")
 8    let confBuilder = ConfigurationBuilder() |> configureSettings
 9    let root        = InflexibleCompositionRoot.compose (confBuilder.Build().Get<Settings>())
10    Host.CreateDefaultBuilder(args)
11        .ConfigureWebHostDefaults(
12            fun webHostBuilder ->
13                webHostBuilder
14                    .UseContentRoot(contentRoot)
15                    .UseWebRoot(webRoot)
16                    .Configure(Action<IApplicationBuilder> (configureApp root))
17                    .ConfigureServices(configureServices)
18                    .ConfigureLogging(configureLogging)
19                    |> ignore)
20        .Build()
21        .Run()
22    0

And you are good to go! Before we identify the problem let me add dependencies to the POST >=> route "/stockitem/" >=> route. InflexibleCompositionRoot.fs

 1  type InflexibleCompositionRoot =
 2    { QueryStockItemBy: Queries.StockItemById.Query -> Async<Queries.StockItemById.Result option>
 3      CreateStockItem: int64 -> string -> int -> Async<unit>
 4      GenerateId: unit -> int64
 5    }
 6
 7  let compose settings =
 8    let createSqlConnection = DapperFSharp.createSqlConnection settings.SqlConnectionString
 9    let idGenerator = IdGenerator.create settings.IdGeneratorSettings
10    let stockItemWorkflowsIo: StockItemWorkflows.IO = {
11      Insert = StockItemDao.insert createSqlConnection
12    }
13    {
14      QueryStockItemBy = StockItemQueryDao.readBy createSqlConnection
15      CreateStockItem = StockItemWorkflows.create stockItemWorkflowsIo
16      GenerateId = idGenerator
17    }

Again, if you are interested in IdGenerator please see the code by yourself. Having this we can already:

  1. Start the app and fire POST to create the stock item.
  2. Receive a nice Location header in the response.
  3. Get the id from the header and query for the stock item that we’ve created.

So what’s the fuss?

Problems with the inflexible implementation

Testing is the problem. Note the signature of CreateStockItem function from Composition root: CreateStockItem: int64 -> string -> int -> Async<unit>. It just takes int64 (id), string (name) and int (amount) and returns asynchronously nothing. We are forced to use the database in our acceptance tests. You are still able to write unit tests for the domain or integration tests for data access objects (DAOs) including database interaction. There are such tests in the repo.

Maybe sometimes you want to write acceptance tests with all the dependencies included but sometimes you won’t (external services calls - do you want to have your tests failed because not-your service in the test environment is down?). Let me give you the clarification in code…

Acceptance tests

I will skip the integration test and unit test - they are too easy for us. Let’s write the acceptance test for the use case. Reminder:

Let’s imagine the following use case: Create a stock item with the name and amount of available items and Read the created item.

Tests1.fs

 1module Tests1
 2
 3open System
 4open Api
 5open Dtos
 6open Microsoft.AspNetCore.Http
 7open Xunit
 8open HttpContext
 9open FSharp.Control.Tasks.V2
10open FsUnit.Xunit
11open TestInflexibleCompositionRoot
12
13[<Fact>]
14let ``GIVEN stock item was passed into request WHEN CreateStockItem THEN new stockitem is created and location is returned which can be used to fetch created stockitem`` () =
15    // Arrange
16    let (name, amount) = (Guid.NewGuid().ToString(), Random().Next(1, 15))
17    let httpContext = buildMockHttpContext ()
18                      |> writeObjectToBody {Name = name; Amount = amount}
19    // Act
20    let http =
21        task {
22            let! ctxAfterHandler = HttpHandler.createStockItemHandler testRoot.CreateStockItem testRoot.GenerateId next httpContext 
23            return ctxAfterHandler
24        } |> Async.AwaitTask |> Async.RunSynchronously |> Option.get
25    // Assert
26    http.Response.StatusCode |> should equal StatusCodes.Status201Created
27    let createdId = http.Response.Headers.["Location"].ToString().[11..] |> Int64.Parse
28    let httpContext4Query = buildMockHttpContext ()
29    let httpAfterQuery =
30        task {
31            let! ctxAfterQuery = HttpHandler.queryStockItemHandler testRoot.QueryStockItemBy createdId next httpContext4Query
32            return ctxAfterQuery
33        } |> Async.AwaitTask |> Async.RunSynchronously |> Option.get
34    let createdStockItem = httpAfterQuery.Response |> deserializeResponse<Queries.StockItemById.Result>
35    createdStockItem.Id |> should equal createdId
36    createdStockItem.Name |> should equal name
37    createdStockItem.AvailableAmount |> should equal amount

The buildMockHttpContext () it’s not that important at the moment - but it creates a fake HttpContext that allows playing with the message body, headers, query string. It is handy - grab the implementation from the repo and take it to the broad world. Note I am passing a function (a use case from the application layer) from the composition root to our HttpHandler. I have created the testRoot before. It’s easy: TestInflexibleCompositionRoot.fs

 1module TestInflexibleCompositionRoot
 2
 3open System
 4open InflexibleCompositionRoot
 5open Settings
 6let testSettings: Settings =
 7    // We are forced to test against database
 8    { SqlConnectionString = "Host=localhost;User Id=postgres;Password=Secret!Passw0rd;Database=stock;Port=5432"
 9      IdGeneratorSettings =
10          { GeneratorId = 555
11            Epoch = DateTimeOffset.Parse "2020-10-01 12:30:00"
12            TimestampBits = byte 41
13            GeneratorIdBits = byte 10
14            SequenceBits = byte 12 } }
15
16let testRoot = compose testSettings

There is no place we can fake the StockItemDao.insert createSqlConnection. Do you see it? Database communication is a must! In this case, this is just the database that we control but this can be simply anything! All kinds of IO here. The IO operations are hidden by the composition root - that’s the real problem.

The alternative is to compose the function without the composition root and pass it to the httpHandler. Ok that is not that so bad but it has two significant drawbacks:

  • You don’t test your dependency tree because you are composing dependencies in your tests, not in the code that runs on production.
  • Maintenance cost of such a solution is high - when you change the dependencies you have to go to tests and adjust the implementation. Each time.

Let me show you now implementation of a more flexible composition root.

Flexible Composition root implementation

I didn’t get the proposed solution on SO question [3] at first. I came up with a metaphor that helped me to explain it to others once I did. Let me know explain it to you.

So… CompositionRoot… what have roots? Right! A tree. Imagine that we have to build the tree now and let’s start from the top. Bear with me, once we go through you will get the idea!

Leaves

Leaves are the IO operations associated with different kinds of dependencies side effects. Databases, Http, WCF… you name the next one. We should have them a lot of, to have a decent tree! For us, it will be the QueryStockItemBy and GenerateId and the 3 functions from the StockItemWorkflows.IO type.

Trunk

The trunk will take all of those dependencies into one place. The trunk will be a support for our leaves. Settings will be passed right into the trunk and the trunk will distribute the proper pieces of settings into the leaves.

Root

The Root will hide all the complexity. Having the root we will be able to call the right “workflow” and then it will go through the tunk to the leaves. This root will be almost identical to the composition root that we already implemented.

  • Leaves = workflows IO dependencies
  • Trunk = host of all the Leaves + common IO dependencies needed in different places (like id generation).
  • Root = proper composition root.

Now let me show you the implementation. After that, I will show you the benefits of it in the tests. I used to structure the flexible composition root like this;

One of the leaves may look just like that; StockItemWorkflowsDependencies.fs

 1namespace Api.FlexibleCompositionRoot.Leaves
 2
 3open System.Data
 4open Stock.Application
 5open Stock.PostgresDao
 6
 7module StockItemWorkflowDependencies =
 8    let compose (createDbConnection: unit -> Async<IDbConnection>) : StockItemWorkflows.IO =
 9        {
10            ReadBy = StockItemDao.readBy createDbConnection
11            Update = StockItemDao.update createDbConnection
12            Insert = StockItemDao.insert createDbConnection
13        }

You will have a lot of leaves in a more complex solution. There will be one trunk, which may look like this: Trunk.fs

 1namespace Api.FlexibleCompositionRoot
 2
 3open Settings
 4open Stock.Application
 5open Stock.PostgresDao 
 6
 7module Trunk = 
 8    type Trunk =
 9        {
10            GenerateId: unit -> int64
11            StockItemWorkflowDependencies: StockItemWorkflows.IO
12            QueryStockItemBy: Queries.StockItemById.Query -> Async<Queries.StockItemById.Result option>
13        }
14        
15    let compose (settings: Settings) =
16        let createDbConnection = DapperFSharp.createSqlConnection settings.SqlConnectionString
17        {
18            GenerateId = IdGenerator.create settings.IdGeneratorSettings
19            StockItemWorkflowDependencies = Leaves.StockItemWorkflowDependencies.compose createDbConnection
20            QueryStockItemBy = StockItemQueryDao.readBy createDbConnection
21            // Your next application layer workflow dependencies ...
22        }

And finally the composition root; FlexibleCompositionRoot.fs

 1module FlexibleCompositionRoot
 2  open Api.FlexibleCompositionRoot
 3  open Stock.Application
 4  open Stock.StockItem
 5
 6  type FlexibleCompositionRoot =
 7    { QueryStockItemBy: Queries.StockItemById.Query -> Async<Queries.StockItemById.Result option>
 8      RemoveFromStock: int64 -> int -> Async<Result<unit, StockItemErrors>>
 9      CreateStockItem: int64 -> string -> int -> Async<unit>
10      GenerateId: unit -> int64
11    }
12    
13  let compose (trunk: Trunk.Trunk) =
14    {
15      QueryStockItemBy = trunk.QueryStockItemBy
16      RemoveFromStock = StockItemWorkflows.remove trunk.StockItemWorkflowDependencies
17      CreateStockItem = StockItemWorkflows.create trunk.StockItemWorkflowDependencies
18      GenerateId = trunk.GenerateId
19    }

Building such dependency tree is still strightforward, let me show you the Program.fs part that is responsible for this:

 1<EntryPoint>]
 2let main args =
 3    let contentRoot = Directory.GetCurrentDirectory()
 4    let webRoot     = Path.Combine(contentRoot, "WebRoot")
 5    let confBuilder = ConfigurationBuilder() |> configureSettings
 6    // old way -> let root        = InflexibleCompositionRoot.compose (confBuilder.Build().Get<Settings>())
 7    let trunk       = Trunk.compose (confBuilder.Build().Get<Settings>())
 8    let root        = FlexibleCompositionRoot.compose trunk
 9    Host.CreateDefaultBuilder(args)
10        .ConfigureWebHostDefaults(
11            fun webHostBuilder ->
12                webHostBuilder
13                    .UseContentRoot(contentRoot)
14                    .UseWebRoot(webRoot)
15                    .Configure(Action<IApplicationBuilder> (configureApp root))
16                    .ConfigureServices(configureServices)
17                    .ConfigureLogging(configureLogging)
18                    |> ignore)
19        .Build()
20        .Run()
21    0

Testing with flexible composition root

It’s time to see the benefits of such a structured composition root. We still write unit and integration tests in the same way. You can still write the acceptance tests in the same way or you can pass your custom-defined functions to form the dependency tree. We only have to be sure that the custom function has the same signature. That’s a cool feature of functional programming that the function automatically behaves like an interface (first-class functions). Let’s write a small helper that will support passing the custom functions as dependencies. TestFlexibleCompositionRoot.fs

 1module TestFlexibleCompositionRoot
 2
 3open System
 4open Api.FlexibleCompositionRoot
 5open FlexibleCompositionRoot
 6open Settings
 7let testSettings: Settings =
 8    // We can test with database but we don't have to.
 9    { SqlConnectionString = "Host=localhost;User Id=postgres;Password=Secret!Passw0rd;Database=stock;Port=5432"
10      IdGeneratorSettings =
11          { GeneratorId = 555
12            Epoch = DateTimeOffset.Parse "2020-10-01 12:30:00"
13            TimestampBits = byte 41
14            GeneratorIdBits = byte 10
15            SequenceBits = byte 12 } }
16
17let composeRoot tree = compose tree
18let testTrunk = Trunk.compose testSettings
19
20let ``with StockItem -> ReadBy`` substitute (trunk: Trunk.Trunk) =
21  { trunk with StockItemWorkflowDependencies = { trunk.StockItemWorkflowDependencies with ReadBy = substitute } }
22  
23let ``with StockItem -> Update`` substitute (trunk: Trunk.Trunk) =
24  { trunk with StockItemWorkflowDependencies = { trunk.StockItemWorkflowDependencies with Update = substitute } }
25  
26let ``with StockItem -> Insert`` substitute (trunk: Trunk.Trunk) =
27  { trunk with StockItemWorkflowDependencies = { trunk.StockItemWorkflowDependencies with Insert = substitute } }
28
29let ``with Query -> StockItemById`` substitute (trunk: Trunk.Trunk) =
30  { trunk with QueryStockItemBy = substitute }

This gives us very sexy intellisense:

Time to rewrite our acceptance tests which checks the creation of the stock item. I already have tests with SQL queries in the dedicated project, so let’s cut off the database dependencies: Tests2.fs

 1module Tests2
 2
 3open System
 4open Api
 5open Dtos
 6open Microsoft.AspNetCore.Http
 7open Stock.StockItem
 8open Xunit
 9open HttpContext
10open FSharp.Control.Tasks.V2
11open FsUnit.Xunit
12open TestFlexibleCompositionRoot
13
14[<Fact>]
15let ``GIVEN stock item was passed into request WHEN CreateStockItem THEN new stock item is created and location is returned which can be used to fetch created stock item`` () =
16    // Arrange
17    let (name, amount) = (Guid.NewGuid().ToString(), Random().Next(1, 15))
18    let httpContext = buildMockHttpContext ()
19                      |> writeObjectToBody {Name = name; Amount = amount}
20    // Data mutation needed instead database operations:
21    let mutable createdStockItem: StockItem option = None
22    let root = testTrunk
23               // first faked function:
24               |> ``with StockItem -> Insert`` (fun stockItem -> async { createdStockItem <- Some stockItem; return () })
25               // second faked function:
26               |> ``with Query -> StockItemById`` (fun _ ->
27                   async {
28                       let stockItem = createdStockItem.Value
29                       let (StockItemId id) = stockItem.Id
30                       return ( Some {
31                           Id = id
32                           AvailableAmount = stockItem.AvailableAmount
33                           Name = stockItem.Name
34                       }
35                               : Queries.StockItemById.Result option)
36                   })
37               |> composeRoot
38    // Act
39    let http =
40        task {
41            let! ctxAfterHandler = HttpHandler.createStockItemHandler root.CreateStockItem root.GenerateId next httpContext 
42            return ctxAfterHandler
43        } |> Async.AwaitTask |> Async.RunSynchronously |> Option.get
44    // Assert
45    http.Response.StatusCode |> should equal StatusCodes.Status201Created
46    let createdId = http.Response.Headers.["Location"].ToString().[11..] |> Int64.Parse
47    let httpContext4Query = buildMockHttpContext ()
48    let httpAfterQuery =
49        task {
50            let! ctxAfterQuery = HttpHandler.queryStockItemHandler root.QueryStockItemBy createdId next httpContext4Query
51            return ctxAfterQuery
52        } |> Async.AwaitTask |> Async.RunSynchronously |> Option.get
53    let createdStockItem = httpAfterQuery.Response |> deserializeResponse<Queries.StockItemById.Result>
54    createdStockItem.Id |> should equal createdId
55    createdStockItem.Name |> should equal name
56    createdStockItem.AvailableAmount |> should equal amount

That was a long way, wasn’t it? Once you will try this approach you will stick to it believe me - the flexibility you have while writing your tests is worth it.

Wait! Isn’t that a service locator that you do in HttpHandlers?

Let’s look again into the DI book [1] to bring the definition

A Service Locator supplies application components outside the Composition Root with access to an unbounded set of Volatile Dependencies.

And learn the service locator pattern negative effects:

  1. The class drags along the Service Locator as a redundant dependency.
  2. The class makes it non-obvious what its Dependencies are.

In contradiction our composition root:

  1. Guarantees that application components have no access to an unbounded set of Volatile Dependencies.
  2. CompositionRoot is not dragged around as redundant dependency. All of the CompositionRoot members should be used - so we are far from “redundant”.
  3. All dependencies of our top-level component (HttpHandler) limit to the CompositionRoot and it is obvious that we want to associate specific “workflows” with given handlers. The “workflows” dependencies are obvious and explicit.

Impure/pure sandwich aka imperative shell

Before I write some conclusions, let me emphasize one thing here. This approach works extremely well with the imperative shell and functional core. You can read more on Mark Seeman’s blog [4] and Gary Bernhardt’s presentation [5]. In short: It is about moving the side effects functions to boundaries of the workflows (no Async in the domain, no synchronous operations which cause state changes elsewhere - for example in the database). This approach makes testing far easier, you get easy multithreading for IO stuff and makes reasoning about the program’s state over much easier. 3 times easy! Do it!

Conclusions

I use this approach in my current project, the team is happy with both - testing strategy and the way the dependencies are being composed. By treating the composition root as a tree with leaves, trunk, and roots we can segregate our concerns - functions with side effects from pure functions. Note that I have used Giraffe as the host, but the composition root is free from any framework references. You should be able to use this way in any F# project.

Two small pieces of advice that can help you in the future

  1. When your composition root will keep growing, consider making more roots. This will help you reason about dependencies. Remember that Composition Root is a single place, not a single type.
  2. I’ve created a record type in StockItemWorkflows for IO Dependencies. Feel free to skip it - if you have one or two dependencies you may simply pass the functions with the right signature (like Scott does in his post [2]).

EXTRA: Homework!

Try to write an acceptance test for removing items from the stock in both approaches. I wrote the “production code” for you already. This will fully help you understand how to do it. This approach works extremely well with TDD as well - try to extend the functionality with one more use-case; adding items to the stock, but write the tests first.


References:
Websites:
[1] Dependency Injection Principles, Practices, and Patterns by Mark Seeman
[2] Functional approaches to dependency injection by Scott Wlaschin
[3] Stack overflow question: F# analog of dependency injection for a real project
[4] Impureim sandwich by Mark Seemann
[5] Functional Core Imperative Shell by Gary Bernhardt