Mapping Database Values to Discriminated Unions for Domain-Driven Design

Let's explore two strengths of F# and their application in a typical enterprise app, using an example that is relevant to any subscription-based service - working with a user's subscription status in both the code and the database. The first strength is it's focus on the domain, and being able to succinctly define types in a way that communicates behavioral intention of the type in a format that effortlessly make sense to the non-technical domain experts. The second strength is its exceptional compiler which is capable of warning you if you leave out a case in a pattern matching expression - pattern matching being the primary means of control flow in F#.

My hope is that through this excercise it might help someone who wants to create a traditional business application in F# to know and understand the tools at their disposal. Also, through posts like this I hope to demonstrate that F# isn't just a "data science" or "math" language, but is entirely practical for the typical business applications we are all used to making with C#, Java, NodeJS, etc. Let's dig in.

Establishing the Domain

Say we have defined a User and the user looks something like this:

type User =
    { FirstName: string
      LastName: string
      Email: string }

Then we determine that a User also has a certain subscription status with our service. In collaboration with our domain experts, we come up with the following representation of a user's status:

type SubscriptionStatus =
    | Active
    | Pending
    | Trial 
    | Cancelled

This disciminated union communicates that a user can have one of 4 statuses. Pending if the user has created an account, but has not entered their payment information. They merely have access to the "billing" screen. Active if the user is subscribed and paid. Trial if the user is in a trial period. Cancelled if the user has terminated their subscription.


Here we should pause and talk about the two strengths of F# I mentioned in the introduction. The first strength is F#'s ability to focus on the domain through succinct, understandable types that communicate intention and are reasonably understandable by non-technical people. This can be seen in our SubscriptionStatus type. Notice the lack of excessive symbols and keywords - the domain expert consuming this information will notice that it maps nearly identically to their own words: "Subscription status can be active, pending, trial, or cancelled." Likewise, the a domain expert's assertion that a "subscription status can be active, pending, trial, or cancelled" is clearly and succinctly communicated in the codebase to the developer any time they approach this code.

Perhaps more impressively is F#'s type system which will enforce that you handle every one of these possibilities any time you deal with SubscriptionStatus. Say, you pull user Gus from the DB and write some code that deals with his subscription status. You will do so in F# with pattern matching:

let user = // SQL to get Gus

// I need to do Thing1 if he's subscribed and Thing2 if he's cancelled
match user.Subscription with
| Subscribed -> // do the subscribed thing
| Cancelled -> // do the cancelled thing

In this instance F#, knowing that there are two other paths in the SubscriptionStatus type, will give you a compilation warning that there are two other scenarios that are unhandled. Thus, by doing type-first, domain-driven development in F# has some nice perks. Certainly regarding both code quality and safety, but also regarding communication with domain experts via expressive and succinct types that communicate intention.


Having touched briefly on F#'s strengths I mentioned before, let's continue looking at our scenario. Now that our user is defined and we've established what a SubscriptionStatus looks like, our User looks like this:

type User =
    { FirstName: string
      LastName: string
      Email: string
      SubscriptionStatus: SubscriptionStatus }

Of course, this user needs to be persisted in the database. Before we defined subscription status, each column mapped neatly to a SQL data type because they were all strings. But now we have a discriminated union for SubscriptionStatus. If we were to pull the user directly from the DB into our F# code, it's looking so far like SubscriptionStatus would be a string of "pending", "active", "trial" or "cancelled".

Indeed, if that descriminated union just gets turned to a string in the DB, and we have to treat it as such when we retrieve it, then it defeats both the elegance of representing the type in that way, and in the type safety we get in pattern matching. However, that is not the case. We can represent our domain model in our elegant discriminated union down until we save it to the DB, and seamlessly retrieve it from the DB as well. That's what we'll do now.

Preserving the Domain when Reading from the Database

In our domain, we've established, in collaboration with our domain experts, that a user can be in one of exactly four statuses with regards to their subscription to the software. Therefore, whenever we encounter a user in our code, we want F# to naturally enforce the SubscriptionStatus type we've defined. The problem is the closest thing to our SubscriptionStatus SQL has is a string type or an enum. If we retrieve a string from our database, it is going to have a string type in F#, and we will lose the type safety and overall elegance of our domain-driven types.

No problem. We can define type handlers that take care of translating our discriminated union to a string on save to the DB, and also the string to our discriminated union on read. We can do this with Dapper. This type handler will use Dapper's SqlMapper to define the transformation that happens when we SetValue (write to the DB), and when we Parse (read from the DB). For our discriminated union, it will look like this:

type MySqlSubscriptionStatusTypeHandler() =
    inherit SqlMapper.TypeHandler<SubscriptionStatus>()

    override _.SetValue(param, value) =
        match value with
        | SubscriptionStatus.Active -> param.Value <- "active"
        | SubscriptionStatus.Pending -> param.Value <- "pending"
        | SubscriptionStatus.Trial -> param.Value <- "trial"
        | _ -> param.Value <- "cancelled"

    override _.Parse value =
        value |> string |> stringToSubscriptionStatus

A few things to note about the above. First, this is an F# class which we can name whatever we want, but it inherits (extends) the SqlMapper.TypeHandler class. This class needs to be instantiated in the application at a high level before our type is ever written or read, most likely on setup/bootstrap of the application. Second, SetValue will be called any time we write to the database and the data being written corresponds to the type parameter we've passed as parameter to the TypeHandler constructor. Therefore, if our F# type has the SubscriptionStatus discriminated union as the type for the SubscriptionStatus member on User, then Dapper will run this SetValue transformation on that column when it encounters the User being saved to the DB. Finally, when we pull from the DB with User specified as the type to materialize to, then the logic in Parse will be applied.

Now, once we've defined this type parameter and added MySqlSubscriptionStatusTypeHandler our bootstrapping process, then we can do this:

let user =
    connection.query("SELECT * FROM User")
    |> Seq.tryHead

match user.SubscriptionStatus with
| Pending -> // handle pending
| Active -> // handle active
| Trial -> // handle trial
| Cancelled -> // handle cancelled

As you can see here the data we retreived from the DB has the F# discriminated union type as originally defined, with all of it's type safety and just as importantly with it's original representation in our domain's design. But what would have happened if we hadn't done this type handler? We could could have just retrieved a string and pattern matched on that to be sure. However, by defining this type handler we ensure that from the time the data enters our F# application to the point to where it exits to the DB, and vice-versa, we have a type that represents the domain expert's mental model - no matter where we encounter this value. We also ensure that we're using the compiler as an ally in our type safety. If we encounter that type, we handle all scenarios because it won't let us skip over one.

Conclusion

To some, there may be a misconception that F# is a language that is relegated to data science, or to mathematics, or to functional programming purists. I hope that through some examples of its applicaton in enterprise app scenarios we can see that not only are those ideas false, but that is the opposite of the truth. F# is a practical language designed to highlight the domain and how the domain behaves within a program, this is an incredible asset, especially in a startup environment where the domain experts and programmers interact closely. Also, with F# you have a lot of practical utility, we can utilize libraries like Dapper to seamlessly transform the domain to a database and vice-versa.

Finally, for those who are using F#, or want to use it soon, but are used to the all the tools of an OOP framework or language, I hope this adds some perspective of how, with F#, one might accomplish the conveniences of the environments you are used to, yet also have access to it's excellent type system and practical conveniences.