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,
√