Skip to content

Blog


Using java.time to Increase Code Readability and Reduce Errors

March 9, 2021

|

Kevin Huang

DoorDash’s platform relies on accurate times affecting real-world events, such as if an order is ready when a Dasher, our term for a delivery person, comes to pick it up. Our assignment algorithm, which makes many of these events possible, considers hundreds of variables, including location, distance, available Dashers, and food preparation time. After considering every variable, we apply a score to each Dasher and delivery pair, and optimize the region to make the most optimal assignments

The dispatch team iterates on this algorithm weekly, as we’re constantly searching for quicker and more efficient ways of making the most optimal assignment. Because we iterate so quickly on our codebase, code readability is an important quality that helps us balance speed with accuracy. If the code is difficult to understand at a glance, it increases our cognitive load and slows down our development time. Code with poor readability also makes it easier for bugs to slip into our algorithm. 

Like many startups, when we first developed the algorithm, we chose to represent time with primitive types. This methodology worked for a while, but as we’ve grown in size and complexity, it became obvious that we needed a more reliable way to represent time due to bugs that stem from this practice. Some bug examples include unit conversion errors from milliseconds to seconds, or errors with adding five minutes to an estimation when we only meant to add five seconds. 

After considering the challenge, we refactored our codebase to use a time library instead of primitives to increase code readability. Refactoring our codebase in this manner was a tall order. Timestamps and durations are the basis for all of our estimations, so naturally they are used extensively throughout our codebase. Because our code directly impacts our customers’ livelihoods and meals, we had to make sure our refactorization would not introduce any unintended changes. Rather than refactoring our code all at once, we broke the problem into smaller chunks and opted to do the migration slowly over multiple changes. 

A quick note about time libraries

In JVM-based languages, the java.time library (also known as JSR-310) is practically the only choice. Another commonly used library, Joda-Time, was actually the precursor to java.time, and was developed by the same person who would go on to lead the java.time project. Given that Joda-Time is no longer under active development, and that the Joda-Time website recommends users switch to java.time, using java.time for our project was an obvious choice. 

A basic time coding example

One of the basic building blocks of code are functions and the parameters we pass into them. Take this function as an extremely simple example:

fun getEstimateInSeconds(timestampInSeconds: Long): Long {
    val now = System.currentTimeMillis()
    return if (timestampInSeconds < (now / 1000)) {
        timestampInSeconds + 60 * 5
    } else {
        timestampInSeconds - 60 * 60 * 3
    }
}

The above code merely takes a timestamp and compares it against the current time, modifying the time to get a result. Functionally, it gets the job done. However, it’s hard to read because of its use of primitive types and literals to represent time. 

Similar to how we iterate on our algorithm, we will iterate on the above function to improve its readability by using java.time. 

Setting function parameters

Notice that the function above takes Long as its parameter, and the parameter timestampInSeconds emphasizes the unit in the parameter name. However, there is no compile-time or runtime guarantee that what gets passed into this function is a timestamp with seconds as its unit. For example, there is nothing stopping an unsuspecting user from passing in the wrong value, like so:

val myTimestampInMinutes = 1000
val myIncorrectResult = getEstimateInSeconds(myTimestampInMinutes)

The above code will compile and run without issue, despite there being an obvious bug. One way the java.time library helps us is by innately capturing time units in its objects. We can improve this function by using java.time’s Instant object to represent timestamps.

fun getEstimate(timestamp: Instant): Long {
    val now = Instant.now()
    return if (timestamp.getEpochSecond() < now.getEpochSecond()) {
        timestamp.getEpochSecond() + 60 * 5
    } else {
        timestamp.getEpochSecond() - 60 * 60 * 3
    }
}

Now that we’re passing a java.time object into this function, the function doesn’t need to make any assumptions about the time unit used for the input. We can retrieve the time unit we need with getEpochSecond() or toEpochMilli(). Additionally, Instants provide a convenient function to get an object with the current time with Instant.now().

Adding or subtracting time

When working with timestamps, it’s common practice to add constant values of time. If the timestamp is in seconds, adding five minutes is frequently represented as 60 * 5. Not only is this practice error prone, but it also becomes unreadable when we start dealing with longer time spans like hours. We can further improve representation of time spans by introducing the Duration object. 

fun getEstimate(timestamp: Instant): Instant {
    val now = Instant.now()
    return if (timestamp.getEpochSecond() < now.getEpochSecond()) {
        timestamp.plus(Duration.ofMinutes(5))
    } else {
        timestamp.minus(Duration.ofHours(3))
    }
}

The Duration object represents a time-based amount of time, such as five minutes, written as ofMinutes(5) in the above code snippet. Now that we’re dealing with the Duration object, we don’t need to convert the timestamp back into the primitive as we can just use the built in plus and minus methods and then return the resulting Instant object. 

Comparing timestamps

There is one more improvement we can make because we are still converting into primitives with the getEpochSecond method and comparing the result with a comparison operator. The Instant object offers a convenient and human readable way to compare one Instant object with another: 

fun getEstimate(timestamp: Instant): Instant {
    val now = Instant.now()
    return if (timestamp.isBefore(now)) {
        timestamp.plus(Duration.ofMinutes(5))
    } else {
        timestamp.minus(Duration.ofHours(3))
    }
}

Comparing timestamps is the same thing as determining which timestamp comes before or after the other. We can use the isBefore() or isAfter() functions to make this comparison without having to convert back into primitive types. 

Conclusion

We began with this code:

fun getEstimateInSeconds(timestampInSeconds: Long): Long {
    val now = System.currentTimeMillis()
    return if (timestampInSeconds < (now / 1000)) {
        timestampInSeconds + 60 * 5
    } else {
        timestampInSeconds - 60 * 60 * 3
    }
}

And ended with this:

fun getEstimate(timestamp: Instant): Instant = {
    val now = Instant.now()
    return if (timestamp.isBefore(now)) {
        timestamp.plus(Duration.ofMinutes(5))
    } else {
        timestamp.minus(Duration.ofHours(3))
    }
}

While these functions do the exact same thing, the latter is more easily read. For a code maintainer, the purpose of this function is more easily understood, leading to less time spent trying to read the code. When quick iteration is vital to an engineering team’s efforts, code readability is a small but worthwhile investment. Migrating from primitive types to a time library for time coding is one way to improve code readability.

Header photo by Fabrizio Verrecchia on Unsplash.

About the Author

  • Kevin Huang

Related Jobs

Location
Toronto, ON
Department
Engineering
Location
San Francisco, CA; Sunnyvale, CA
Department
Engineering
Location
San Francisco, CA; Mountain View, CA; New York, NY; Seattle, WA
Department
Engineering
Location
San Francisco, CA; Sunnyvale, CA; Seattle, WA
Department
Engineering
Location
San Francisco, CA; Sunnyvale, CA; Seattle, WA
Department
Engineering