DEV Community

Cover image for Domain modeling obsession
Angelo Sciarra
Angelo Sciarra

Posted on • Updated on

Domain modeling obsession

Hi there,
today's topic is "primitive obsession" and why it is important (to avoid it) in your domain modeling.

What is "primitive obsession"?

It is classified as a code smell and it signals the abuse of primitive data types to represent domain ideas.

Think of a class like this

data class CustomerInfo(
    val name: String, 
    val lastName: String, 
    val age: Int, 
    val email: String, 
    val address: String
)
Enter fullscreen mode Exit fullscreen mode

What is wrong with it?

You could think that it is perfectly fine but give it a second thought: does your domain expert talk about Strings or rather Email Addresses?

Types are important in your design because they act as a live documentation of your domain model and rules.

Consider for example a validation process. I think you all perform some sort of validation of the data coming from the outside world into your domain.

Let's stick with the email example.

Suppose you have a function like

fun `throwing validate email`(toValidate: String): String = 
// omitted
Enter fullscreen mode Exit fullscreen mode

or

fun validateEmail(toValidate: String): Validated<InvalidEmail, String> = 
// omitted 
Enter fullscreen mode Exit fullscreen mode

What is wrong with those two functions from a domain modeling point of view?

Well, what is wrong is that from a type point of view nothing is telling us that the validation process produced a valid Email Address.

Let's try to improve it.

inline class EmailAddress(val value: String)

val validateEmail: (String) -> Validated<InvalidEmail, EmailAddress> = 
// omitted
Enter fullscreen mode Exit fullscreen mode

Much better now: we have a function that is perfectly sound also from a type point of view. It clearly states that it will take as input a String and will give back either an InvalidEmail error or an actual EmailAddress. (If you are curious about the Validated data type have a look here)

Why not being obsessed?

I know what may be critics about this way of designing your domain model: too much of a hassle!

And that may be even true for Java, where adding a class just to have a wrapper has a cost in terms of lines of code you have to maintain (if you think about it, it is a bit weird to have some penalties adding new classes in an OOP language where classes should be the bread and butter of your everyday programming!)

NOTE: with Java 14 there is the feature preview of Records that, even if intended to give an easy way of representing Value Objects, addresses also the problem of wrapper types (even if the ergonomics of records can be improved)

Other critics may be linked to performance penalties related to creating too many classes.

To answer to this line of criticism: performance, unless it is an outmost prerequisite of your domain (say you are designing a realtime engine or whatever), and even in that case, should be addressed later in the design process, i.e. when you have a design to test for performance, and only with the most analytical mindset: measure performances and then think about performance improvements and of course measure again to test your improvements actually worked.

NOTE The inline class, shown in the last code example, is an experimental feature of Kotlin, starting from version 1.3. It is designed to reduce the overhead of actually creating real classes and objects at runtime by replacing any instance of the class with the wrapped value. So it is an optimization to let you use wrapper classes to better express your business domain while not having to pay any penalty at runtime.

Why should you be obsessed?

Should you stop being obsessed at avoiding primitive obsession? No! You should go ALL IN and start modeling to be the more specific you can be at the type level. Consider for example you are modeling an order taking system: your order has a lifecycle and passes through different states. How would you go and model it?

A first try could be

enum class OrderStatus {
    PLACED, CONFIRMED, CANCELLED, ... // etc
}

data class Order(
    val id: OrderId, 
    val items: List<OrderItem>, 
    ...
    val status: OrderStatus, 
    ....
)
Enter fullscreen mode Exit fullscreen mode

What do you think? Is it a sound design? Would it be easy to design functions that target a specific order status? Well, no. It would be much easier if you design the entire order as a choice type (or sum type in some circles)

sealed class Order(val id: OrderId, ...)
data class PlacedOrder(override val id: OrderId, ...): Order(id,...)
data class ConfirmedOrder(override val id: OrderId, ...): Order(id,...), 
...
Enter fullscreen mode Exit fullscreen mode

With this kind of design, it becomes much easier to define functions that accept orders in a certain state because the order status is encoded in the different choices.

Ex.:

fun sendConfirmationMail(order: ConfirmedOrder) = // omitted
Enter fullscreen mode Exit fullscreen mode

Conclusion

What do you gain from being obsessed with avoiding primitive obsession? Well, you gain a codebase that speaks your domain expert language (the so-called Ubiquitous language from DDD circles) and that is less prone to error because you always know what you are dealing with at type level.


DISCLAIMER: most of the concepts expressed in this article summarize, from my point of view, notions and ideas taken from two books you should read if you have not yet:
- "Domain Modeling Made Functional" by Scott Wlaschin
- "Refactoring: Improving the Design of Existing Code" by Martin Fowler

Discussion (4)

Collapse
tarodbofh profile image
Juan Ara

The thing about sum | choice types is that the state machine around the status (in your example) is hard to design to allow parallel processing, i.e. when it's purely a linear flow, it's easy to model but when the domain grows and there are collaborators that alter part of the order either (pun intended) you need to redesign (which is not bad, after all needs and infra changed) or be extremely careful about where and how coupling happens.

Collapse
tarodbofh profile image
Juan Ara

BTW, the Validated approach, though nice, hides for me a fear for null. Since you are using kotlin, a validateEmail(email: String) would just return a ValidatedEmail (inline) or null.
Embrace null! Null is a valid domain idea! Check github.com/Kotlin/KEEP/blob/master...
At that link there are a lot of references about idiomatic kotlin being direct style instead of railway style (though doesn't forbid the 2nd!):

However, core Kotlin language and its Standard Library are designed around a direct programming style in mind.
Collapse
eureka84 profile image
Angelo Sciarra Author

I really like Kotlin nullable types because they force you to think about nullability and provides compile time checks against nullability.
I would use them maybe instead of data types like Option, because, as you said they are more idiomatic. I wouldn't go and use them in place of Eithers or Validated because they are currying other kinds of information and not simply the presence/absence of a value.

Collapse
eureka84 profile image
Angelo Sciarra Author

Well I have seen how difficult it is to work with a model that is in a 1:1 relation with its DB representation and I would say I would take my chances with choice types...