Enable javascript in your browser for better experience. Need to know to enable it? Go here.
Domain modeling by Algebraic Data Types (part 2)

Domain modeling by Algebraic Data Types (part 2)

The previous article introduced ADTs (Algebraic Data Types). This article will start with an example to introduce how to use ADTs for domain modeling.

 

 

Mapping domain knowledge to code

type CreditCard = {
  cardNo: string
  firstName: string
  middleName: string
  lastName: string
  contactEmail: Email
  contactPhone: Phone
}

 

Based on the type definition above, we can easily model the domain of CreditCard. Note that we are not using a class.

 

But is this a reliable domain model? If not, what’s the issue?

 

The biggest problem with this code is incomplete domain knowledge. Let me explain with a question:

 

Can the middle name be empty?

 

  • Answer 1: Unsure - I need to check the document.

  • Answer 2: Maybe - the middle name can be null.

     

     

Modeling nullable types

Imagine a domain expert telling you the middle name can exist or be empty. Pay attention to the word "or," as it indicates that we can model the nullable type through the union type. In functional programming languages, the nullable type is defined as Option<T>.

type Option<T> =  T | null

A simple Option type is actually a union type. (You can use a more complex Option implementation, though that isn’t within the scope of our article.) 

 

The improved domain model for our Credit Card now looks like this:

type CreditCard = {
  cardNo: string
  firstName: string
  middleName: Option<string>
  lastName: string
  contactEmail: Email
  contactPhone: Phone
}

 

 

Avoid Primitive Obsession

 

  • Can cardNo be represented by a string? If so, can it be any string? 

  • Can firstName be a string of any length?

     

You cannot answer the questions above because this model doesn’t include such domain knowledge.

 

In programming languages, cardNo can be expressed as a string, but in our CreditCard domain model, strings don’t fully capture the domain knowledge behind cardNo.

 

Experts in our domain know cardNo is a 19-digit string starting with 200, and names are strings not exceeding 50 digits. This domain knowledge can be achieved through the following type aliases:

 

type CardNo = string
type Name50 = string

 

With the above two types, you have the opportunity to include the cardNo business rules in the domain model by defining functions.

type GetCardNo = (cardNo: string) => CardNo

 

The improved domain model now looks like this:

type CreditCard = {
  cardNo: CardNo
  firstName: Name50
  middleName: Option<string>
  lastName: Name50
  contactEmail: Email
  contactPhone: Phone
}

 

This model has more domain knowledge and the rich types also act as unit tests. For example, you will never assign an Email type to contactPhone. They aren’t strings, they represent different domain knowledge.

 

 

Error handling

If the user enters a 20-digit string, what does the function GetCardNo return? Throw an exception? 

 

Functional programming languages ​​have more elegant error handling approaches than exceptions, such as Either Monad or Railway oriented programming. For now we can use the Option type to update the function signature:

type GetCardNo = (cardNo: string) => Option<CardNo>

 

This function type clearly expresses the entire verification process. The user enters a string and returns a CardNo type or null. 

 

 

Atomicity and aggregation of domain models

Can the three names in the CreditCard domain model be modified separately? For example, could you only modify the middle name? If not, how do we include this atomic modification knowledge in the domain model?

 

We can easily separate the two types of Name and Contact and combine them:

type Name = {
  firstName: Name50
  middleName: Option<string>
  lastName: Name50
}

 

type Contact = {
  contactEmail: Email
  contactPhone: Phone
}

 

type CreditCard = {
  cardNo: CardNo
  name: Name
  contact: Contact
}

 

 

Make illegal states unrepresentable

This is a very important principle of domain modeling. In fact, the entire domain model follows this principle, such as the above Email type and Phone type. Why not use string to represent email? Because the domain knowledge given by string isn’t enough, it lets developers make mistakes.

 

Let’s look at a simple example to understand how to apply this principle in domain modeling... 

 

A Contact type was defined which contained Email and Phone. After a credit card payment is successful, the system sends a notification to the user through one of these two properties, following the rule that the user must fill in at least Email or Phone to accept the payment notification.

 

Currently our domain model doesn’t support this business rule, because the Email and Phone types are both required.

 

Can we change both of them to Option type?

type Contact = {
  contactEmail: Option<Email>
  contactPhone: Option<Phone>
}

 

Unfortunately, this violates the "Make illegal states unrepresentable" principle. Your domain model expresses an illegal state that both Email and Phone can be null. Can this rule be expressed in the domain model? Yes, we can simply express this relationship through the union type:

type OnlyContactEmail = Email
type OnlyContactPhone = Phone
type BothContactEmailAndPhone = Email & Phone
type Contact =
  | OnlyContactEmail
  | OnlyContactPhone
  | BothContactEmailAndPhone

 

 

Conclusion

 

  1. Does the domain model contain as much domain knowledge as possible, and can it reflect the business model for domain experts?

  2. Can the domain model become a document, and then become a way for everyone to communicate and share knowledge?

     

The type system of a functional programming language not only helps developers build a great domain model, it also provides a simple and composable type system as a basis for code as a document.

Disclaimer: The statements and opinions expressed in this article are those of the author(s) and do not necessarily reflect the positions of Thoughtworks.

Keep up with our latest insights