Domain Driven Design (DDD) is a software design approach, focusing on modeling software to match a domain according to input from that domain's experts.
In simple terms, it focuses on narrowing the gap between developers and domain experts, and encoding business terms and rules directly within code using names and models that are understandable to the business.
Another aspect of DDD is to focus on "bounded contexts" - the idea that are many different contexts in which a term has meaning, and instead of creating a single domain model to "rule them all", we can create multiple domain models to represent specific contexts. For example, within a given business the sales team may have a very different understanding to the delivery team for the concept of a "Project", both in terms of data but also in terms of lifetime.
FP and DDD?
Most code-focused examples of DDD take an object-oriented (OO) approach - involving classes, inheritance hierarchies, objects with mutable state, void methods etc. FP (and F#) eschews most of these practices, as it focuses on immutable data structures, expression-oriented code and separation of state and behaviour. However, this doesn't mean that you can't "do" DDD in and functional programming language - quite the opposite.
Rich domains
One of the main goals that developers attempt when working with a DDD mindset is to encode invariants and business rules directly within their domain models, rather than using "dumb" types which use primitives for all their values. Consider the following data structure:
type Customer = {
Id : System.Guid
FirstName : string
MiddleName : string // optional
LastName : string
EmailAddress : string
Rating : int
}
This sample above (already in F#) already implements some interesting behaviours out of the box that are relevant to this discussion:
- Immutable: Records in F# are immutable by default.
- Copy-and-Update: Supports simple copy-and-update syntax.
- Value Object: Automatically implements deep value equality - two distinct objects in memory with the same values are considered equal.
- Guaranteed Creation: The compiler enforces construction of all fields on the type. You cannot create "half" an object.
- Non-null: F# types cannot be null. Instead, you can use the
Option
type to encode optionality into your types, with full compiler support.
In such a scenario, you might have some business logic which needs to enforce invariants. You would typically do this in functions:
let sendEmail (content:string, emailAddress:string) =
// first check that this is a valid email address before sending
...
In such a case, we might even elect to return a Result
to encode the possibility of failure within the function due to an invalid email address.
However, we can do much better using wrapped types which we can use to both improve documentation and readability but also to provide a place in which to enforce invariants at the type level. Here's an alternative to the above "domain model":
type Name = {
FirstName : string
MiddleName : string option
LastName : string
}
type CustomerRating = HighValue | Standard | SendToCollections of dueDate:System.DateTime
type CustomerId = CustomerId of Guid
type Customer = {
Id : CustomerId
Name : Name
EmailAddress : EmailAddress
Rating : CustomerRating
}
In this model, we've encoded more rules within our types. CustomerRating is easier to understand at a glance as to what it stores and its use is; Id is no longer a Guid but a CustomerId, which wraps a GUID. In this type, we can enforce any specific invariants that we want. For a GUID this might not be so useful, but for the email address and our sendEmail
function, this might be very useful:
let sendEmail (content:EmailContent, emailAddress:ValidatedEmailAddress) =
// No need to check whether the email address is valid - that's already guaranteed by the type!
...
The EmailAddress
type in this example could have two possible cases: either an UnvalidatedEmailAddress
or a ValidatedEmailAddress
e.g.
type EmailAddress =
| Unvalidated of string
| Validated of ValidatedEmailAddress
By doing this, you enforce that you can only call sendEmail
if you have a ValidatedEmailAddress
, which would have any invariant checks applied in advance in order to construct the type.
In-memory business logic
When using DDD, it's also common to implement logic using your domain types. This does not exclude F# simply because classes are rarely used: There are many ways for doing this by simply using types, modules and functions. Typically, you might read data from the database to hydrate your "rich" domain model, perform business logic operations in memory and then dispatch commands to the database to persist any updates that took place.
This could be something as simple as a function that takes in a Customer and returns a modified version:
let upgradeCustomer (c:Customer) : Customer =
{ c with Rating = HighValue } // some business logic here...
It could also be used to safely create e.g. Commands that are executed to e.g. modify the database:
// validation logic goes here...
{ CustomerId = c.Id; ValidationResult = Ok () }
This command would be handled by the database; it contains just the required data to modify the data access layer so that the customer's email address is marked as valid (or could record the validation failure reason).
Summary
I've seen people who are just using F# - or looking from a distance - think that DDD is impossible because F# doesn't favour mutable data or behaviour on classes. I believe that those are simply implementation details that, due to the prevelance of OO techniques, have led to people thinking that they are core elements of DDD. They aren't! Similarly, I've seen people fall into the trap of creating anti-corruption layers between the boundary of their applications and the internal pure domain, but fail to actually create a rich internal domain - instead, they map from one set of DTOs to another.
If you're interested in finding out more about domain modelling in functional programming, I talk about this in more detail in my book F# In Action. Additionallt, you can check out the excellent Domain Modelling Made Functional by Scott Wlaschin.