F# - How to keep domain pure when logic depends on current date

In functional programming we strive to write side-effect free applications. In other words, all the functions of the application should be pure. However, completely side-effect free applications are mostly useless, so the next best thing is to minimize the amount of side-effects, make them explicit and push them as close to the boundaries of the application as possible. Although the whole application cannot be completely side-effect free, it’s usually possible to write all the business and domain logic in a pure functional way.

In this post, I will present one way to implement some domain logic that depends on the concept of current date without introducing impureness into the domain logic itself.

DISCLAIMER: I’m a novice when it comes to functional programming. Please advice me if there are any errors or if the example code could be cleaned up somehow.

The problem of current date

One property of a pure function is that it always evaluates to the same output value given the same input value. If we consider System.DateTime.Today, we can immediately see that it is not a pure function, because the output depends on something else than the input. It depends on when the function is called. This means that if we use such a function in our domain it pollutes the code base with impureness.

Let’s see an example in invoicing domain. When changing a due date of an invoice we want to check that the new due date is in the future. We could implement it like this:

let changeDueDate (newDueDate: DateTime) invoice =
  if newDueDate > System.DateTime.Today then
    success { invoice with DueDate = newDueDate }
  else
    failure "Due date must be in future."

Never mind the success and failure now. They come from something called either monad or railway-oriented programming. What we are interested in, is the validation logic newDueDate > System.DateTime.Today in the if-expression. This is the problematic part. We definately want to make this check and it is part of the domain logic, but it’s impure.

Now the question is, can we do this check in a pure way?

Solving the problem with type system

To begin with, let’s remove the direct dependency to DateTime and wrap it into our own type Date and let’s add isInFuture function to help us with validation.

type Date = Date of DateTime

let isInFuture (Date date) =
  date > DateTime.Today

Now we can rewrite our domain code to look like this:

let changeDueDate (newDueDate: Date) invoice =
  if isInFuture newDueDate then
    success { invoice with DueDate = newDueDate }
  else
    failure "Due date must be in future."

Yay, no more DateTime.Today in sight! The bad news is, we still call it in isInFuture function which makes the changeDueDate function impure too. We could provide the isInFuture function as a parameter to changeDueDate function. The downside with this solution is that we would need to pass the validation function along all the way from the outer boundary of the application. That doesn’t sound pratctical!

What if we could know if the date is in the future without knowing the current date in domain logic? Let’s see how we can achive this. First, I’ll modify the Date type and add some new fuctions to work with it.

type Date =
  | PastDate of DateTime
  | CurrentDate of DateTime
  | FutureDate of DateTime

let create (dt: DateTime) =
  match dt with
  | dt when dt.Date = DateTime.Today -> CurrentDate dt
  | dt when dt.Date < DateTime.Today -> PastDate dt
  | _ -> FutureDate dt

let isToday = function
  | CurrentDate _ -> true
  | _ -> false

let isInFuture = function
  | FutureDate _ -> true
  | _ -> false

let isInPast = function
  | PastDate _ -> true
  | _ -> false  

The key idea here is that we type Date in more detail. We have a union case for past, current and future dates. This means that when ever we have a date instance in our code we already know which one of these it is.

We still have one impure function here too. It’s the create function that works as a factory to create Date instances. However, this function is used only at the boundaries of the application when we map for example REST calls to typed values. The domain always operates on Date instances and therefore it knows for each date if they are current date, in the past or in the future without needing to know what is the actual current date. I implemented also 3 helpers functions to make those checks easier in the domain. You can see that those functions are completely pure.

With these changes in place we can finally implement a pure changeDueDate function. Let’s see how it looks now.

let changeDueDate (newDueDate: Date) invoice =
  if isInFuture newDueDate then
    success { invoice with DueDate = newDueDate }
  else
    failure "Due date must be in future."

Hmm, it’s exactly the same as before! We have successfully pushed the impure part of the code away from our domain logic. We were able to do this by typing the date in more detail.

Going even further with typing

I’m quite happy with the solution above, but we can indeed go even further with typing.

Let’s redefine the Date type as follows.

type PastDate = PastDate of DateTime
type CurrentDate = CurrentDate of DateTime
type FutureDate = FutureDate of DateTime

type Date =
  | Past of PastDate
  | Current of CurrentDate
  | Future of FutureDate

// update the functions accordingly...

Now we can change the changeDueDate function so that the input date is type of FutureDate. This removes the need for validation completely! Now we have a compile time check that the function is called only with a future date. The changeDueDate function is now trivial:

let changeDueDate (newDueDate: FutureDate) invoice =
  { invoice with DueDate = Date newDueDate }

Again, the FutureDate instance is created at the boundary of the application. For example in the service layer when we map REST call to typed values.

Problems with this solution?

I haven’t encountered any problems with this solution so far. There is a theortetical issue of checking the current date (or time) at the application boundary, because it could be different than what it would be if checked later in domain code. This however, is not a problem in real life. It might be even safer that the date cannot change during one business operation.

Conclusion

We were able to implement some domain logic depending on current date without introducing impureness to that logic. We could have achived this with a monad or some other fancier approaches, but in this case it was enough to introduce more types to describe date. There is nothing F# specific in this solution. Indeed, you could use the same approach in C# application.