Lgd. Viktor Klang

Systems all the way down

Viktor Klang bio photo


Best practices for ExecutionContext management with Futures

A common situation as one gets started writing code using Future is how to depend / inject ExecutionContext into one’s own logic. So, I thought I’d take the time to share some of my experiences in this field, and I hope you’ll find it useful.

Common question: Where do I get the ExecutionContext from?

Let’s start with having some sort of component, this one deals with the universal concept of customers.

import java.util.UUID, scala.concurrent._
final case class Customer(uuid: UUID)
abstract class CustomerComponent {
  final def byId(customerId: UUID): Future[Customer] =
    selectCustomer(customerId)
  protected def selectCustomer(uuid: UUID): Future[Customer] 
}

Now, selectCustomer will perhaps need to execute some logic to obtain Customers asynchronously, and that means that an ExecutionContext is in order.

So where should this ExecutionContext be provided?

What is the recommendation(s)?

As with everything in life: «There is no optimal, general, solution.»™

If the component wants to control where its logic is executed:

import java.util.UUID, scala.concurrent._
final case class Customer(uuid: UUID)
abstract class CustomerComponent {
  protected implicit def ec: ExecutionContext
  final def byId(customerId: UUID): Future[Customer] =
    selectCustomer(customerId)
  protected def selectCustomer(uuid: UUID): Future[Customer] 
}

The solution for this use-case is to make the ExecutionContext a protected implicit def member so that implementations of this module will be in control over how the ExecutionContext is supplied.

If the instantiator of the component should control where the components’ logic is executed:

import java.util.UUID, scala.concurrent._
final case class Customer(uuid: UUID)
abstract class CustomerComponent(protected implicit val ec: ExecutionContext) {
  final def byId(customerId: UUID): Future[Customer] =
    selectCustomer(customerId)
  protected def selectCustomer(uuid: UUID): Future[Customer] 
}

The solution in this case is to have the ExecutionContext as a protected implicit val field to have the instantiator of the module supply the ExecutionContext instance upon creation.

If the user of the component should control where the components’ logic is executed:

import java.util.UUID, scala.concurrent._
final case class Customer(uuid: UUID)
abstract class CustomerComponent {
  final def byId(customerId: UUID)(implicit ec: ExecutionContext): Future[Customer] =
    selectCustomer(customerId)
  protected def selectCustomer(uuid: UUID)(implicit ec: ExecutionContext): Future[Customer] 
}

The solution in this case is to have the caller supply the ExecutionContext to be used for the methods which require it.

How can it be improved?

import java.util.UUID, scala.concurrent._
final case class Customer(uuid: UUID)
abstract class CustomerComponent {
  type EC[_] = ExecutionContext
  final def byId[_ : EC](customerId: UUID): Future[Customer] =
    selectCustomer(customerId)
  protected def selectCustomer[_ : EC](uuid: UUID): Future[Customer] 
}

It is possible to, in order to cut down on boilerplate, to define a type alias for ExecutionContext such that it takes an unused type parameter, and then it can qualify as a context bound. You can then, if you don’t have a suitable target type parameter, introduce an “anonymous” one, in this case named _.

Click here for the next part in this blog series.

Cheers,