Date

Reactive programming offers a powerful paradigm for handling asynchronous and event-driven code. When working with reactive frameworks, it's important to understand the distinction between two key timelines: the Construction Timeline and the Subscription Timeline. In this article, we'll explore these timelines, their significance, and the potential bugs that can occur if they are confused.

Developing a Mental Model

When writing reactive code for the first time it's important to realize you are dealing with two timelines in your code. Consider this simple code snippet. Unless you're familiar with reactor, it's not clear that there are two timelines at play.

fun isBirthday(userId: Int): Mono<Boolean> {
    val today = Date.now()
    return getUser(userId).map {
        today == it.birthdate
    }
}

fun getUser(userId: Int): Mono<User>

Construction

The first is what I call the Construction timeline. This is the code that constructs your chain of Monos. It defines the initial Mono and uses reactive operations to chain business logic together. Think top-level usages of map, flatMap, filter, etc.

Subscription

The second is what I call the Subscription timeline. This is the code that operates on the values within the reactive chain. This logic is found within the blocks associated with the reactive operations. That is, the predicates passed to map, flatMap, filter, etc.

Example Revisited

Let's take a look at how the timelines manifest in our previous snippet.

fun isBirthday(userId: Int): Mono<Boolean> {
    // Construction Timeline
    val today = Date.now()
    // Construction Timeline
    return getUser(userId).map {
        // Subscription Timeline
        today == it.birthdate
    }
}
  1. The isBirthday function is called in the construction timeline.
  2. The variable today is bound to Date.now() in the construction timeline.
  3. The getUser method is called in the construction timeline.
  4. The comparison of the birthday to today happens in the subscription timeline

The key point is that the construction of the Mono happens first, and subscription happens only once values are emitted.

Fixing a bug

There's a bug in the above code-snippet. Can you find it?

The bug is the following. Since the variable today is evaluated in the construction timeline, but used in the subscription timeline, the value of today is determine much earlier than it is used. It is possible that today becomes out of date. That is, we might set up a server on one day, but subscribe a day later. The value of today won't change in that case, since it was bound earlier. (This is only possible thanks to closures, a non-reactive feature found in many programming languages)

Here's a fix:

fun isBirthday(userId: Int): Mono<Boolean> {
    // Construction Timeline
    return getUser(userId).map {
        // Subscription Timeline
        val today = Date.now()
        today == it.birthdate
    }
}

In this fix we move the binding of the today variable from the Construction timeline into the Subscription timeline. In this way the value of today is more likely to correspond to the actual day.

This is a pattern of bug that can appear in many ways throughout a reactive codebase. It's important to keep timelines in mind when pre-computing values or defining variables.

Print Debugging Reactive Code

A common issue that comes up for first time reactive programmers is: "How do I print out this value?"

In the below snippet the programmer has added a println around the result of the getUser call. When she go to run the code she finds that the User object is not printed. Instead, the program prints something opaque like MonoLiftFuseable or MonoJust.

fun isBirthday(userId: Int): Mono<Boolean> {
    val user = getUser(userId)
    println(user)
    return user.map {
        val today = Date.now()
        today == it.birthdate
    }
}

The problem comes about from the mixing up timelines. The programmer wants to print a value in the construction timeline, but that value is only found in the subscription timeline. By moving the print statement into the subscription timeline the programmer can now access the value to print it.

fun isBirthday(userId: Int): Mono<Boolean> {
    // Construction Timeline
    val user = getUser(userId)
    return user.map {
        // Subscription Timeline
        println(it)
        val today = Date.now()
        today == it.birthdate
    }
}

A common question when running into this error is: "How can I extract values from a Mono?" This is often leads to wrong results. You seldom want to extract results from a mono. Extracting means subscribing or blocking, both of those operations defeat the purpose of using a reactive library.

Handling Nested Monos

Another common task is to call two services sequentially. Imagine a scenario where after we call the user service we to schedule it on the calendar service. An initial implementation might look like this, where we call the second service in a map operator following the first service. This is almost right, but it produces an unexpected type.

fun createBirthdayEvent(userId: Int): Mono<Event> {
    return getUser(userId).map { user ->
        createEvent(user)
    }
}

fun createEvent(birthDate: Date): Mono<Event>

The expected return type is Mono<Event> but the actual return type becomes Mono<Mono<Event>>. Now the programmer asks "How can I flatten a Mono of a Mono?"

To do this we can use the flatMap operator.

As a general rule, use flatMap when the operation's return type is another Mono or Flux. Use map when the operation in other cases.

At the heart of it, this too is an error caused by mixing up timelines. To understand this scenario we have to update our understanding of timelines. There isn't a single construction and subscription timeline, rather each of these timelines exists for each Mono in your system. So in our example the construction timeline of the createEvent timeline happens in the subscription timeline of the getUser Mono. In other words, it's nested. The flatMap operator lets us flatten the createEvent subscription timeline to be at the same level as getUser's subscription timeline.

fun createBirthdayEvent(userId: Int): Mono<Event> {
    return getUser(userId).flatMap { user ->
        createEvent(user)
    }
}

Conclusion

This article covers some early issues you'll discover on your journey with reactive programming. Hopefully it gives you tools you need to address future issues that come up in your work.

If you found this article helpful, or wish I had covered something else, email me, or toot me on Mastodon. https://fosstodon.org/@hpincket