Thomas Buß

05 Dec 2021

Are functional domain models always anemic?

Domain Driven Design advocates against the use of anemic data models and for the use of rich domain models. DDD is said to be technology-agnostic, and thus also programming-language-agnostic. But does this work in practice with functional programming, where all logic lives in functions and not in classes?

What is an anemic data model?

An anemic data model is a representation of domain objects that does not contain any business logic. In OOP terms, this means that the classes of this model contain only fields and their respective getters and setters. This puts the burden of correctness (i.e. which states are valid and which are not) to the clients of the model… to all clients to be precise. In the best case, this distribution of logic leads to duplicate efforts when the business logic has to change, but this can quickly grow into an unmanagable ball of mud.

Rich domain models, on the other hand, encapsulate the logic into meaningful objects and methods that do not allow clients to change the data as they see fit. Well, objects and methods are what OOP-style models use. But what if we wanted to take a more functional approach?

Functional Programming and logic

In functional languages like OCaml (which I use for the examples below), we compose functions and a handful of base data types to build our logic. The data structures themselves do not (cannot) contain logic. How do you protect your model from invalid changes that go against the business rules? Let me show you with an example what I mean by this.

Example: An anemic, functional model

Here is some OCaml code for a simple study group management domain in a file called study_group.ml. I’m not going into the details of the OCaml language and hope the example code is understandable. The add_student function contains a business rule: A study group can only contain 5 student. When the study group is full, the function returns a domain-specific response; a type called Study_Group_Is_Full.1

type student = {
  name : string;
  grade : int;
}

type add_result =
| Student_Added of student list
| Study_Group_Is_Full

let add_student (grp: student list) stud =
  if List.length grp < 5 then Student_Added (stud :: grp)
  else Study_Group_Is_Full

A client of the code can use the add_student function like this (in a different file called client.ml):

let full_class: student list = [
  { name = "Jeffrey Winger"; grade = 1 };
  { name = "Britta Perry"; grade = 3 };
  { name = "Pierce Hawthrone"; grade = 3 };
  { name = "Troy Barnes"; grade = 3 };
  { name = "Annie Edison"; grade = 3 };

]

let res = (add_student full_class { name ="Abed Nadir"; grade = 2 });;

This works as expected and the res variable contains Study_Group_Is_Full, but there is a problem: Any client can just bypass the domain logic by adding a student to the study group directly, using the list concatenation operation ::, like this:

let full_class = { name = "Abed Nadir"; grade = 1 } :: full_class;;

How can we make sure the business rule is always executed?

Slightly changed model

We have to hide the fact that a study group is just a list of students from the clients and expose only those functions that comply with our business rules to clients. To do this in OCaml, we need an additional file for our interface (with the same name, but different file extension: study_group.mli).

type student = {
  name : string;
  grade : int;
}

type study_group

type add_result =
| Student_Added of study_group
| Study_Group_Is_Full

val add_student: study_group -> student -> add_result
val print_study_group: study_group -> unit
val full_class: study_group

This file contains the student type as well as the study_group type. But we do not define the internal structure of the study group, merely “mention” it2. We also do not include the function bodies, only the signatures of the functions we want to expose to outside clients.

Now, we just need to make slight changes to our implementation: We introduce the study_group type and change the function signatures accordingly. We now also need a “constructor” function as we cannot create a study group on the client side otherwise.

type student = {
  name : string;
  grade : int;
}

type study_group = student list

type add_result =
| Student_Added of study_group
| Study_Group_Is_Full

let new_study_group (): study_group = []
let add_student (grp: study_group) student =
  if List.length grp < 2 then Student_Added (student :: grp)
  else Study_Group_Is_Full

When we now try to use the contatenation operator :: to append a student to a study group (full or not), we’ll get an error as the types study_group and student list are not compatible. The only way to add a student to a study group is through our add_student function and there is no way around our business rule.

Encapsulation

This example shows that a functional model does not always mean the model is anemic. We used the same principle that OOP languages use to hide information and enforce business rules: encapsulation. It’s just that this kind of encapsulation looks different. By making deliberate choices of which types and functions we expose to outside clients, we can get the same benefits as encapsulation does in OOP languages.


  1. You do not always need a method-specific return type like this; this just shows one way of doing it. [return]
  2. This is OCaml’s way to hide internal details, but of course, there might be different solutions to this problem in other languages. [return]
comments powered by Disqus