“If it compiles, it works”. With Valiktor and Konad In Domain Driven Design there is the concept of . Being trivial, this is usually related to the names you give to the entities in the domain model. ubiquitous language It is possible to take it a step further. We can turn the code into an unequivocal expression of the domain. For example, here it follows how an American domain expert may describe a “contact” and some business rules. A contact has a name, a surname, and an email. The name can have a middle initial. The email must be verified. You can send password recovery only to verified emails. A typical, persistence oriented, modeling could look like the following: ( firstname: String, middleInitial: String, lastname: String, email: String, emailIsVerified: ) data class ContactInfo val val val val val Boolean Does this code express any of the requirements? Yes, it enumerates all the data. Anyway, is there any constraint on strings? Are all the data mandatory, or some may be missing? We can do better, and declare these informations in the model. Declarative Domain model First, recall the basics. Cohesion Some of the data are related, then it is a good idea to group them. ( firstname: String, middleInitial: String, lastname: String) ( value: String, isVerified: ) ( name: PersonalName, email: Email) data class PersonalName val val val data class Email val val Boolean data class ContactInfo val val This model expresses which information is related. Maintaining high cohesion also favors the reusability. Declare the constraints The string attributes of should not be empty. This is not clear in the code. Let’s clarify it. PersonalName data ( : , : ?, // : ) class PersonalName val firstname NotEmptyString val middleInitial NotEmptyString Here it is clear that it is optional val lastname NotEmptyString Notice that is nullable. This expresses at compile-time that the middle initial is optional. middleInitial I wrapped strings in that ensures compliance with the constraints. NotEmptyStrings ( value: String){ { : NotEmptyString = validate(NotEmptyString(value)){ validate(NotEmptyString::value).isNotBlank() } } } inline class NotEmptyString @Deprecated(level = DeprecationLevel.ERROR, message = ) "use companion method 'of'" constructor val @Suppress( ) "DEPRECATION_ERROR" companion object fun of (value: ) String Here I used an class, and I declared a compile-time error to prevent the use of the constructor. This class can be instantiated only using the factory method , hence there is no chance to skip the validation. I used to implement it. inline of Valiktor Inline classes exist in Kotlin with the purpose to enhance the expressiveness of primitive types. Using primitive types in the domain model is a code smell, known as primitive obsession Make it compile-time safe The signature of states that if you input a string, you will always get back a NotEmptyString.of NotEmptyString fun (value: ): NotEmptyString of String This is a lie. If the validation fails, then Valiktor will throw a . The possibility that it throws is not declared in the signature then, you can get an unexpected run-time error. Let’s fix this issue with ConstraintViolationException Konad io.konad.Result ( value: String){ { : Result<NotEmptyString> = valikate { validate(NotEmptyString(value)){ validate(NotEmptyString::value).isNotBlank() } } } } -> T): Result<T> = { valikateFn().ok() } (ex: ConstraintViolationException){ ex.constraintViolations .mapToMessage() .joinToString( ) { } .error() } import inline class NotEmptyString @Deprecated(level = DeprecationLevel.ERROR, message = ) "use companion method 'of'" constructor val companion object @Suppress( ) "DEPRECATION_ERROR" fun of (value: ) String internal inline fun < T> reified valikate (valikateFn: () try catch "\n" "\t\" \" of . : " ${it.value} ${T::class.simpleName} ${it.property} ${it.message} Here I used the helper function that catches the exception, format its messages with Valiktor API, and wrap everything in . valikate Result The builders and are extension methods from , that build a and a , respectively. ok() error() Konad Result.Ok Result.Errors Look at the new signature: : Result<NotEmptyString> fun of (value: ) String The possibility to fail is now clear. In Konad, Result is a class that can be or . Using it, the function is free from side-effects. sealed Result.Ok Result.Errors Compose a PersonalName Now we can create some that may result in an error or the desired value. From those, we need to obtain a . If you are used to “monad like” structures like or , then you could expect that I’m going to write an infinite list of … or …. Result<NotEmptyString> PersonalName Java Optional Kotlin nullables .flatMap.flatMap.map let.let.let Well, I’m not. This is where shines. We are going to create a with the composition API of Konad: Konad PersonalName ( firstname: NotEmptyString, middleInitial: NotEmptyString?, lastname: NotEmptyString){ { : Result<PersonalName> = ::PersonalName.curry() .on(NotEmptyString.of(firstname)) .on(middleInitial?.run { NotEmptyString.of(middleInitial) } ?: .ok()) .on(NotEmptyString.of(lastname)) .result } } ( nameResult = PersonalName.of( , , )){ Result.Ok -> nameResult.value.toString() Result.Errors -> nameResult.description( ) }.run(::println) data class PersonalName val val val companion object fun of (firstname: , lastname: , middleInitial: ? = ) String String String null null // usage example when val "Foo" "" "" is is "\n" Konad accumulates all the errors. The in the example, is going to print the list of all the errors, separated by a new line with . println name.description("\n") Remove flags An email can be verified or not. In the initial model, there is the boolean flag that keeps this information. isVerified ( value: String, isVerified: ) data class Email val val Boolean The flag is not type-safe. It requires to be checked every time that we need a verified Email. For example: { (email.verified) sendTo(email.value) } fun sendPasswordRecovery (email: ) Email if This can’t be checked at compile-time. My preferred approach is to have a type for the status and one for the . Unverified Verified ( value: String){ ( email: Unverified): Email(email.value) ( value: String): Email(value) } sealed class Email open val data class Verified private val data class Unverified override val The function can change as follows: sendPasswordRecovery { sendTo(email.value) } fun sendPasswordRecovery (email: . ) Email Verified The has gone, and there is no chance that anyone forgets to check the status. Also, you don’t need to read the implementation to understand that only verified emails can achieve a password recovery. if isVerified Validate Email We need to constrain the construction of an email, as already did for . NotEmptyString To build a Email, you need an one. It follows that it suffices to implement the validation only for emails. Verified Unverified Unverified ( value: String){ ( email: Unverified): Email(email.value) (value: String): Email(value){ { : Result<Email.Unverified> = valikate { validate(Email.Unverified(value)){ validate(Email.Unverified::value).isEmail() } } } } } sealed class Email val data class Verified private val class Unverified private constructor companion object fun of (value: ) String Notice that Unverified became a normal class, instead of being a data class. This is because there is no way to make private the copy method of a data class. Finally, the declarative model In the end, this is how the model looks: ( name: PersonalName, email: Email) ( firstname: NotEmptyString, middleInitial: NotEmptyString?, lastname: NotEmptyString){ ... } ( value: String){ ( email: Unverified): Email(email.value) (value: String): Email(value){ ... } } data class ContactInfo val val data class PersonalName val val val // construction code here sealed class Email val data class Verified private val class Unverified private constructor // construction code here You can understand all the requirements just by reading it. A contact is composed of a name and an email. A name is composed of three strings that must not be empty. One of those strings may be missing. An email can be unverified or verified. let’s have a look also to some example services: (){ : = TODO( ) } (){ : Email.Verified? = (incredibleConditions())Email.Verified(unverifiedEmail) = } class PasswordResetService fun send (email: . ) Email Verified Unit "send reset" class EmailVerificationService fun verify (unverifiedEmail: . ) Email Unverified if else null private fun incredibleConditions () true Finally, how to build a . Again, with the composition API of : ContactInfo Konad contact: Result<ContactInfo> = ::ContactInfo.curry() .on(PersonalName.of( , , )) .on(Email.Unverified.of( )) .result val "Foo" "Bar" "J." "foo.bar@gmail.com" Conclusion With the proposed approach, you don’t need to look at any method implementation to understand: the structure of the data; all the constraints; the flow of the processes that involve those data. Everything is in the signatures. It is not trivial to design classes that ensure compile-time safety, without side-effects. I proposed some implementation examples using: to implement readable data checks, with few lines of code. Valiktor to improve compile-time safety. Konad The use of monads, highers the code complexity. With Konad you can benefit from their compile-time safety, with a minimum impact on the code complexity. Konad composition API is easy to use and doesn’t require any knowledge of functional concepts. Code and material Here it follows the complete code example. You can find it at the following repo: . https://github.com/lucapiccinelli/typesafe-domain-model { contactResult: Result<ContactInfo> = ::ContactInfo.curry() .on(PersonalName.of( , , )) .on(Email.Unverified.of( )) .result contactResult .map { contact -> (contact.email){ Email.Unverified -> EmailVerificationService.verify(contact.email) Email.Verified -> contact.email } } .map { verifiedMail: Email.Verified? -> verifiedMail ?.run { PasswordResetService.send(verifiedMail) } ?: println( ) } .ifError { errors -> println(errors.description( )) } } PasswordResetService{ : = println( ) } EmailVerificationService{ : Email.Verified? = (incredibleConditions())Email.Verified(unverifiedEmail) = } ( name: PersonalName, email: Email) ( firstname: NotEmptyString, middleInitial: NotEmptyString?, lastname: NotEmptyString){ { : Result<PersonalName> = ::PersonalName.curry() .on(NotEmptyString.of(firstname)) .on(middleInitial?.run { NotEmptyString.of(middleInitial) } ?: .ok()) .on(NotEmptyString.of(lastname)) .result } } ( value: String){ ( email: Unverified): Email(email.value) (value: String): Email(value){ { : Result<Email.Unverified> = valikate { validate(Email.Unverified(value)){ validate(Email.Unverified::value).isEmail() } } } } } ( value: String){ { : Result<NotEmptyString> = valikate { validate(NotEmptyString(value)){ validate(NotEmptyString::value).isNotBlank() } } } } -> T): Result<T> = { valikateFn().ok() } (ex: ConstraintViolationException){ ex.constraintViolations .mapToMessage() .joinToString( ) { } .error() } fun main (args: < >) Array String val "Foo" "Bar" "J." "foo.bar@gmail.com" when is is "Email was not verified" "\n" object fun send (email: . ) Email Verified Unit "send reset to " ${email.value} object fun verify (unverifiedEmail: . ) Email Unverified if else null private fun incredibleConditions () true data class ContactInfo val val data class PersonalName val val val companion object fun of (firstname: , lastname: , middleInitial: ? = ) String String String null null sealed class Email val data class Verified private val class Unverified private constructor companion object fun of (value: ) String inline class NotEmptyString @Deprecated(level = DeprecationLevel.ERROR, message = ) "use companion method 'of'" constructor val companion object @Suppress( ) "DEPRECATION_ERROR" fun of (value: ) String internal inline fun < T> reified valikate (valikateFn: () try catch "\n" "\t\" \" of . : " ${it.value} ${T::class.simpleName} ${it.property} ${it.message} Acknowledgments This article is based on concepts brought to the scene by with its great talk (and book) “ ”. Scott Wlaschin Domain modeling made functional Thank you for reading!