Richard Searle

home

Choreography using Akka Futures

16 Jan 2012

A previous series of post covered a choreography implementation using recursive PartialFunctions. That implementation is quite complex and required the workflow designer to have a good understanding of the underlying implementation. In other words, an interesting engineering solution but not really practical.

Both Akka and Finagle have new Future based toolkits that permit a much simpler implementation.

The following discussion uses the Akka future, with the complete code available here.

The choreography problem domain involves the implementation of a service using lower level services that expose a request-response message exchange protocol(R-R MEP). The service generally includes non trivial state and the combination of responses from several invocations. The latency seen by the client must be minimized, requiring the exploitation of any available concurrency in the invocation of the lower level services.

This combination of characteristics does not match a straightforward message passing implementation, that might be perhaps be trivially implemented using Camel. It can be implemented using a full blown workflow engine, such as Tibco BusinessWorks or Aqualogic Service Bus.

The R-R MEP can be directly mapped to a send-And-Receive-Future actor invocation. The returned Future directly supports the desired concurrency. Directly referencing the Akka actor is an unnecessary lock-in and complicates unit testing. The Lookup trait provides an abstraction of the lower level service, exposing a function that takes a single argument and returns a Future that will ultimately return the result.

trait Lookup[A, R] extends Function1[A, Future[R]] {
  def apply(arg: A): Future[R]
}

A simple service that returns the balance of an account might then have the form

object Balance {
  def apply(acct: Acct)(implicit balLook: Lookup[Acct, Bal]): Future[Bal] = 
     balLook(acct)
}

The following service applies a discount to the balance of certain accounts. Two concurrent calls are made to determine the account balance and whether the account qualifies for the discount.

object Discount {
  def apply(acct: Acct)(implicit balLook: Lookup[Acct, Bal],
                                 specialLook: Lookup[Acct, Boolean]): Future[Bal] = {
    val balance = balLook(acct)
    val special = specialLook(acct)
    for {
      val bal <- balance
      val spec <- special
    } yield if (spec) bal * 0.9F else bal
  }
}

Determine the discounted balance, given the phone number of the account. Note how easily the two services are composed,

object DiscountByPhone {
  def apply(pn: Num)(implicit acctLook: Lookup[Num, Acct],
                              balLook: Lookup[Acct, Bal], 
                              specialLook: Lookup[Acct, Boolean]): Future[Bal] =
    acctLook(pn) flatMap { Discount(_) }
}

Determine the sum of the discounted balances, given an Id (e.g. SSN) that maps to many accounts.

object DiscountById {
  def apply(id: Id)(implicit numLook: Lookup[Id, List[Num]], 
                             acctLook: Lookup[Num, Acct], 
                             balLook: Lookup[Acct, Bal], 
                             specialLook: Lookup[Acct, Boolean]): Future[Bal] =
    numLook(id) flatMap { Future.traverse(_)(DiscountByPhone(_)) } 
          map { _.reduce(_ + _) }
}

This might be clearer, split into several lists with typed temporary variables

object DiscountById {
  def apply(id: Id)(implicit numLook: Lookup[Id, List[Num]], 
                             acctLook: Lookup[Num, Acct],
                             balLook: Lookup[Acct, Bal],
                             specialLook: Lookup[Acct, Boolean]): Future[Bal] = {
    val numbers: Future[List[Num]] = numLook(id)
    val balances: Future[List[Bal]] = numbers flatMap
             { ns: List[Num] => Future.traverse(ns) { 
                 n: Num => DiscountByPhone(n) } }
    balances map { _.reduce(_ + _) }
  }
}

The above does require knowledge of Scala (and its functional idioms) and the akka Futures API. An internal DSL could permit a simpler representation, hiding some of the implementation details.

Nonetheless, this is still quite straightforward and very powerful.