Here is a very very basic example of what I do. I actually use CQRS and Event Sourcing, but this matches the majority of DDD examples out there which focus on aggregates.
The domain is the truth and is not abstracted. It is just defined as modules. I encourage you to go as deep as you want in defining your domain to be solid using type-driven design. But for simplicity, I will just use things like string:
// Domain Layer - User.res
type user = {
email: string
nickname: string
}
let changeUserEmail = (user, ~newEmail) => {
// return possible domain errors
let modifiedUser = {
email: newEmail,
nickname: user.nickname
}
Ok(modifiedUser)
}
module type UserRepository = {
type t
let findByEmail: (t, ~email: string) => promise<option<user>>
}
Now you will define the application layer, which uses the domain layer. Since the application layer is built to protect the domain layer, I believe that means it accesses the domain directly. So it is not passed through a functor. The infrastructure layer is of course separate and passed through the functor to separate the concerns.
// Application Layer - UserService.res
module Make = (R: User.UserRepository) => {
type t = {
userRepository: R.t
}
type error = | UserNotFound
let make = (~userRepository) => { userRepository: userRepository }
let changeUserEmail = async (t, ~currentEmail, ~newEmail) => {
switch await t.userRepository->R.findByEmail(~email=currentEmail) {
| None => Error(UserNotFound)
| Some(user) =>
switch user->User.changeUserEmail(~newEmail) {
| Error(domainError) => Error(...) // change to application error
| Ok(_) as newUser => newUser
}
}
}
}
Then you would build something like a MysqlUserRepository module which, when passed into the functor, is limited to the interface defined by the UserRepository module type.
If you repeat this pattern again, one layer up, you can pass the Application layer into the UI layer. But at some point, you will have to initialize things.
module US = UserService.Make(MysqlUserRepository)
let userRepository = MysqlUserRepository.make(mysqlConfig)
let userService = US.make(~userRepository)
userService->US.changeUserEmail(~currentEmail, ~newEmail)