How do I organise my namespaces/modules for good DDD practices?

I have some code which I have simplified below.
(I’m trying to follow the Domain-Driven Design model in Scott Wlaschin’s book “Domain Modelling Make Functional”.)
I’ve removed all of the Result processing of the original code, and a lot of the actual code, to focus on the organisational stuff.

I have the system domain in the Domain namespace.
I have the data transfer stuff (edge of domain) in the DataTransfer namespace.
I have the data access stuff (outside of domain) in the DataAccess namespace.

namespace Domain

open System

type Thing =
    { Id : Guid 
      Name : string }

module Thing =
   
   let validate item = item
   let moreChecks item = item

   let create item =
    item
    |> validate
    |> moreChecks
    |> DataTransfer.serialise
    |> DataAccess.saveToDisk

namespace DataTransfer

open Domain

type ThingDto = 
    { Id : string 
      Name : string }

module DataTransfer =

    let serialise (item : Thing) =
        { ThingDto.Id = item.Id.ToString() 
          Name = item.Name }

namespace DataAccess

open DataTransfer

module DataAccess =

    let saveToDisk (item : ThingDto) = printfn $"Item {item.Id} Saved"

However, I cannot access the serialise or saveToDisk functions as they are defined after they are used.

I could define the DataTransfer and DataAccess namespaces before the Domain namespace but that would mean that the Thing type would be defined after the functions in the DataTransfer namespace which need to know about it.

I can’t figure out how I can define things in the right order so they are usable by the things which need them but are also not all ‘bunched together’ in a single (messy) module.

Can anyone help?

I’ll take a stab at this, but I’ll have to caveat this by saying that I don’t know that much about DDD, so the things I say should be received skeptically.

From a functional programming perspective, it’s a big red flag that you can’t create a “validated” Thing without storing it to disk. From the little bit I understand about DDD, that’s a little suspicious there too, since DDD (I think) embraces a layered architecture, where the persistence layer is completely separated from the system domain layer.

There’s a few different solutions to this, and picking the right one probably depends on the domain. Probably my favorite and the most standard FP approach would be to separate the creation of Thing and the persistence of Thing, so just

   let create item =
    item
    |> validate
    |> moreChecks

and then any functions in the domain have to return any Things that get created so that the caller has to deal with persisting them. I think this post (and specifically Approach #2 on that page) covers this idea well. Also, I haven’t read his book, but there’s a nice talk by Scott on the subject of DDD in F# available on YouTube. You can see around 16:25 in his talk that he’s advocating for this style solution by having Deal return the modified deck and PickupCard return the modified hand that then the persistence layer would have to deal with.

If the Things are only ever created from outside the domain, then you could just define

module DataAccess =
  let saveToDisk (item : ThingDto) = ...
  let createThing item = Thing.create item |> DataTransfer.serialise |> saveToDisk

and exclusively use createThing (and possibly even play around with making Thing.create inaccessible outside the domain scope).

Another option is to try to do a DI-style solution, and inject a createThing factory function into anything in the domain that needs it. So I can have

namespace Domain 

type Thing = { Id : Guid; Name : string }
module Thing = 
  let validate item = ...

module DooDad = 
  let fn createThing = 
    blah, blah
    let thing = createThing { Id = id; Name = name }
    blah, blah

namespace DataTransfer

open Domain

type ThingDto = 
    { Id : string 
      Name : string }

module DataTransfer =

    let serialise (item : Thing) =
        { ThingDto.Id = item.Id.ToString() 
          Name = item.Name }

namespace DataAccess

open DataTransfer

module DataAccess =

    let saveToDisk (item : ThingDto) = printfn $"Item {item.Id} Saved"
    let createThing item = item |> Thing.validate |> DataTransfer.serialize |> saveToDisk

    
DooDad.fn DataAccess.createThing

Approach #3 from that same post I linked above gives a more in-depth overview of this approach. Just be careful with this because you can end up destroying a lot of the benefits of functional programming (if you’re using a functional programming approach). With this approach, the I/O of your program can happen anywhere in the guts of your code, instead of keeping I/O separate from the pure code, and that can make debugging problems harder.

Hope that helps! Sorry if I completely missed the mark, since I don’t know DDD that well.

1 Like

Thanks for yet another detailed and very useful answer.

It took me a while to understand what you were saying (my fault completely) but I think I understand it now and it makes much more sense than what I was trying to do.

I think the book probably explains it but the book can be confusing at times, especially at the end where persistence is only given one short chapter and this sort of ‘static data maintenance’ isn’t really addressed.

I’ve included the latest version of my cut-down code just in case it’s useful to anyone else.

(I’ve also changed the code to show how dependencies can be injected into the process, which was something else which wasn’t going right for me.)

namespace Public 

type UnvalidatedThing = { Name : string }

namespace Domain

open System
open Public

type Thing =
    { Id : Guid 
      Name : string }

module Thing =
   
   let validate (item : UnvalidatedThing) =
        { Thing.Id = Guid.NewGuid()
          Name = item.Name }

   let create checker item =
    item
    |> validate
    |> checker

namespace DataTransfer

open Public
open Domain

type ThingDto = 
    { Id : string 
      Name : string }

module DataTransfer =

    let toUnvalidated name = { UnvalidatedThing.Name = name }

    let serialise (item : Thing) =
        { ThingDto.Id = item.Id.ToString() 
          Name = item.Name }

namespace DataAccess

open DataTransfer

module DataAccess =

    let checkForDuplicateName item = item

    let saveToDisk (item : ThingDto) =
        printfn $"Item {item.Id} Saved"
        item

namespace Api

open Domain
open DataTransfer
open DataAccess

module ThingMaintenance =

    let createThing name =
        name
        |> DataTransfer.toUnvalidated
        |> Thing.create DataAccess.checkForDuplicateName
        |> DataTransfer.serialise
        |> DataAccess.saveToDisk

namespace Program

open Api

module OutsideWorld =

    let myNewThing = "Alex" |> ThingMaintenance.createThing