Contents


Model-first microservices with Scala and Cats

Use Scala’s powerful type-system and functional programming capabilities to manage microservices

Comments

Microservices-based architectures have been growing in popularity in recent years. Based on a recent survey, the ability to decompose even large systems into independently deployable units is one of the biggest benefits cited by developers. However, this comes at a cost: The frequent traversal of network boundaries calls for abstractions that allow for convenient ways to deal with a high degree of concurrency on the one hand, and fault tolerance and error handling on the other.

Scala's composable Future type can be used to stitch together microservice calls, and ... some of its shortcomings can be addressed with Cats, a Scala library that provides abstractions supporting a typeful, functional programming style.

This tutorial provides an overview of how Scala's powerful type-system and its capabilities for functional programming can provide an excellent canvas for dealing with microservice composition and the problems arising from it. We will also focus on a model-driven development style. Microservices often implement a Bounded Context, a concept from domain-driven design that maps a specialized subset of the domain model. It's important to ensure that domain concepts continue to stand out clearly and don't get compromised in the presence of concurrency and error-handling routines.

To keep things practical, we will start by introducing a simplified problem domain—an article search service—and show you how to break it down using familiar Scala types. Then, you'll see how Scala's composable Future type can be used to stitch together microservice calls, and how some of its shortcomings can be addressed with Cats, a Scala library that provides abstractions that support a typeful, functional programming style. We will then turn to error handling, and attempt to disentangle domain errors from operational errors using Scala's Either[A, B] type and the extensions Cats provides for it. Seeing how some convenience is lost in the process, we will conclude by showing you Cats' monad transformers, a powerful way to combine nested effect types.

Setting the scene: An article search service

Before we get into the thick of it, you need a service to build—in this case, a service that can search an article library such as that of developerWorks. To keep things manageable, we will focus on a small, simplified subset of this problem domain. Here is what we want the service to accomplish:

Given a search query, return all publications for the first author found matching the query.

Looking closer at this requirement, there are four Bounded Contexts that you can map to services (see Figure 1):

  • AuthorPublications: The aggregate service that will be outlined here; given a search term, it returns matching author properties along with all publications and their respective properties
  • AuthorSearch: Author discovery service that maps search terms to author IDs
  • AuthorMetadata: Author lookup service that maps author IDs to a set of properties
  • PublicationMetadata: Publication lookup service that maps author IDs to a set of publication properties
Figure 1. The article search domain
The article search domain
The article search domain

Let's examine what could be an appropriate set of types to model this. In terms of domain-driven design, you might find that there are two key entities, Author and Publication:

case class Author(id: Long, name: String)

case class Publication(id: Long, authorId: Long, title:String)

You will also need an aggregate that binds the authors and their publications together:

case class AuthorPublications(author: Author, publications: List[Publication])

This is fairly straightforward. Now take a look at the domain logic and see how you might describe the steps that you would need to carry out in the AuthorPublications context. The requirement was that given a name as a search query, you would need to resolve the query to the first matching author entity. In a real service, this call would most likely query a search index in the AuthorSearch sub-domain, returning an entity ID to you:

def findAuthor(query: String): Future[Long]

The function signature indicates that this call might run concurrently by returning Future[Long] instead of Long. You don't need to concern yourself with the actual implementation here, but you can assume that this (and the following functions) will involve network I/O and hence should run concurrently to the application's main thread of execution.

Given an author ID, you now need to resolve it to an Author entity. This functionality is abstracted away in the AuthorMetadata sub-domain and might expose an interface like the following:

def getAuthor(id: Long): Future[Author]

In a real system, this function would likely be part of a repository, which queries either a data store or a metadata service asynchronously to locate the entity.

Finally, given an author ID, you need to find all publications by the given author in the PublicationMetadata sub-domain:

def getPublications(authorId: Long): Future[List[Publication]]

Let's recap: You defined your main domain objects—Article, Author, and AuthorPublications. You also provided functions (findAuthor, getAuthor, and getPublications) that allow you to return instances of these types from upstream service calls using Scala's Future type. We will now show you how to stitch these calls together to build meaningful functionality.

Composing services with futures

Equipped with these functions, you can combine them in a for-comprehension to produce an instance of AuthorPublications, the aggregate object:

  def findPublications(query: String): Future[AuthorPublications] =
    for {
      authorId <- findAuthor(query)
      author <- getAuthor(authorId)
      pubs <- getPublications(authorId)
    } yield AuthorPublications(author, pubs)

Given a search query, which might be provided to you in a request parameter, you can then run your search as follows:

  val query = "matthias k"
  val search: Future[Unit] = findPublications(query) map { authorPubs =>
    renderResponse(200, s"Found $authorPubs")
  }
  
  Await.result(search, Duration.Inf)

This is all standard Scala, however there are two things worth highlighting here:

  1. Even though each service call might execute on a different thread, none of this complexity will leak into your code and your logic will stand out clearly. The compositional aspect of the Future type is key to this: Scala desugars for-comprehensions into calls to flatMap and map, which act as continuations that perform the necessary function composition for you. What you get is a new function, findPublications, which returns a new Future with the result of that composition chain. The only evidence you have that you're dealing with an effect (concurrency) is encoded in the return type.
  2. With the above in mind, it's clear that up until the point where you actually await the result, all you have done is compose functions; you did not stop to await an intermediate result, but instead relied on Future composition all the way through. This is a prime example of how functional programming works: Effects such as the delayed arrival of values due to concurrency are pushed to the system boundaries. This is also where error handling usually happens, as you will see shortly.

Benefits aside, there are problems with this example. In findPublications, subsequent service calls won't start to execute before the previous ones return, so you lose something important: the option to parallelize. This is because each line in the for-comprehension can be thought of as a callback that acts on the previous result, as shown in Figure 2.

Figure 2. findPublications
findPublications
findPublications

As you may have noticed, both getAuthor and getPublications work off of the authorId that was fetched in the first step. Therefore, getPublications does not need to wait for getAuthor to finish since these two operations can execute in parallel, as depicted in Figure 3.

Figure 3. Executing in parallel
Executing in parallel
Executing in parallel

Unfortunately, as of Scala 2.12 there is no way for you to explicitly express this intent, and you have to fall back on somewhat unintuitive practices to make these calls actually run concurrently.

So does that mean we have reached the limits of Scala's built-in functionality when it comes to expressing concurrency? It turns out that Cats, a Scala library for functional programming, has an answer to this question; it lets you join two Futures, effectively running them in parallel.

Multiplying Futures: Cats and the Semigroupal type

Cats isn't a reference to the furry animal; rather, it's short for "categories," a nod to the branch of mathematics that studies structure in mathematics itself. Category theory is concerned with abstractions that are extremely useful when working with strongly typed languages. Specifically, category theory provides blueprints or recipes for the traversal, transformation, and composition of types in ways that are lawful, meaning provably correct.

One such type is Semigroupal (formerly called "Cartesian"), which allows you to join Futures using its product function. If you've ever written code that joins two tables in SQL, then you're already familiar with cartesian products. A SQL join takes sets of tuples (the rows of the tables you join) and yields a new set with every element being a tuple that consists of the values from all source tables. This effectively multiplies the source tables. You can translate this idea to type systems: Every type represents a set of possible values for that type (or in terms of category theory, a category with types as objects), so if you multiply two types, A and B, you get a new type, (A, B), whose values are pairs of values from A and B.

Applying this to our example, you can now say: Given Future[Author] and Future[List[Publication]], multiply them to produce a tupled Future[(Author, List[Publication])] (see Figure 4).

Figure 4. A tupled future
A tupled future
A tupled future

Unlike in the previous section, where two Future-returning functions f and g were chained together even if g didn't rely on f's output at all, the tuple type instead reveals your intention a lot better: A tuple of values can only exist if all of its values have been produced, but this is allowed to happen in any order (you might know this law by its name: commutativity). In other words, it doesn't matter whether the author or the publications arrive first, so you can now run them in parallel:

  def findPublications(query: String): Future[AuthorPublications] =
    for {
      authorId <- findAuthor(query)
      (author, pubs) <- getAuthor(authorId) product getPublications(authorId)
    } yield AuthorPublications(author, pubs)

Composing getAuthor and getPublications with the product function will trigger the two Futures simultaneously and produce either a tuple that contains both values as soon the two Futures succeed, or an error if any of them fail. This is not only an elegant solution to your problem, but it also expresses the domain logic more clearly in that getAuthor and getPublications do not depend on each other.

Before we take a look at what other aces Cats has up its sleeves, let's go back to another problem that I mentioned earlier but we haven't quite solved: how to effectively deal with errors that arise in article searches.

Dealing with service failures

Futures come with error handling baked right in. They capture the outcome of their computation using Scala's Try type, which can either succeed (carrying a value of A) or fail (carrying an exception). Exceptions are well suited to dealing with operational issues such as I/O failures, runtime platform errors, and unmet system expectations, so Try seems like an appropriate choice for capturing a Future's internal state.

You might be tempted to rely solely on this mechanism, but when it comes to representing failures in a problem domain, exceptions should be avoided. This is because logical failures in an application are best represented by proper values that you can pass around in return types, and which should not be treated exceptionally at all. For example, not being able to find an author in your article search engine is an unexceptional failure, but a failure nonetheless. Think about it: If you navigate to a URL that points to a missing resource, would you expect your browser to explode? No, you would expect it to return a 404 page, which is really just an alternative value for the page you originally wanted. Let's take a look at how we can accomplish this in Scala.

Sealed type hierarchies

Sealed type hierarchies are a convenient way to encode failure values in Scala. Since sealed hierarchies cannot be extended outside of their lexical scope, they allow the compiler to check whether a pattern match against its cases is exhaustive. This prevents you from crashing at runtime if you forget to handle a particular failure, which can force you to deal with every possible case. Here's how you can use this feature to represent failures in your article search service:

  sealed trait ServiceError
  case object InvalidQuery extends ServiceError
  case object NotFound extends ServiceError

Note how these are all value objects, and since they're types on their own, you can return them from functions instead of having to resort to something nasty like throwing an exception. You might now ask: How can I return either a ServiceError or the actual result of this call from a service function call? You said it yourself: Using Either.

The Either type

Either[L, R] is the canonical way of representing a mutually exclusive choice of two values in Scala: It carries either an L ("left"), or an R ("right"), but never both. As of Scala 2.12 (with Cats on the classpath for earlier versions as well), the Either type is a right-biased monad, which means you can transform it using map and flatMap to follow the happy path. This makes it an excellent choice for representing outcomes from any kinds of computations, including service calls, where the left branch is substituted for an error type and the right branch for a success type. Right-bias, therefore, becomes success bias, as you saw with Future. Let's implement the findAuthor function to account for the fact that it might produce a ServiceError instead of an author ID:

  def findAuthor(query: String): Future[Either[ServiceError, Long]] =
    Future.successful(
      if (query == "matthias k") 42L.asRight
      else if (query.isEmpty) InvalidQuery.asLeft
      else NotFound.asLeft
    )

Granted, this is not a very useful search implementation, but it does illustrate the use of return types to represent both failure and success cases. While this implementation is explicit (the method contract is clearly visible in the signature), it is a little verbose. Nesting effects this way means that you now have two levels of machinery, Future and Either, which have to be dealt with separately. This can make it difficult to get your hands on the value you're after, since you need to first map the Future to extract the Either, then map the Either to obtain the right value, or in code:

findAuthor("matthias k") map { idOrError => idOrError map { id => ... }  }

Figure 5 illustrates this effect stack using a containment metaphor:

Figure 5. A containment metaphor
A containment metaphor
A containment metaphor

Using stacked effects also undermines the convenience of for-comprehensions, since they cannot traverse both effects at once. Ideally, you want an abstraction that folds both effects into one: If the Future succeeds and the Either contains a right outcome, then map the result. It turns out that Cats provides just such an abstraction: monad transformers.

Merging effect stacks with monad transformers

Monad transformers allow you to stack effects like "runs asynchronously" (Future) and "multiple outcomes" (Either) and treat them as one. In other words, you get to have your cake and eat it too! Cats provides monad transformers for some of the existing effect types like Option and Either, and for Either it's called EitherT. Let's use it to define a custom Result type that represents the outcome of a search result in your service:

  // bind Future and SearchError together while leaving the inner result type unbound
  type Result[A] = EitherT[Future, SearchError, A]

  // this allows you to invoke the companion object as "Result"
  val Result = EitherT

With this handy type definition, you can redefine your service functions in a compact and readable way, without losing any of the utility you got from using Either:

  def findAuthor(query: String): Result[Long] =
    if (query == "matthias k") Result.rightT(42L)
    else if (query.isEmpty) Result.leftT(InvalidQuery)
    else Result.leftT(NotFound)

Here, leftT and rightT are helper functions that lift the given value into the effect type that you used to instantiate EitherT (in this case Future), and then turn them into left (error) and right (success) cases for Either, respectively. Rewriting this function in terms of Result[Long] solves the nested map problem mentioned above, as illustrated in Figure 6.

Figure 6. Solving nested maps
Solving nested maps
Solving nested maps

You can do the same with getAuthor and getPublications:

  def getAuthor(id: Long): Result[Author] = 
    Result.rightT(Author(id, "Matthias Käppler"))

   def getPublications(authorId: Long): Result[List[Publication]] =
    Result.rightT(List(
      Publication(1L, authorId, "Model-First Microservices with Scala & Cats")
    ))

Since EitherT is itself a monad that simply stacks Future and Either, the article search program that composes these functions remains essentially unchanged:

  def findPublications(query: String): Result[AuthorPublications] =
    for {
      authorId <- findAuthor(query)
      result <- getAuthor(authorId) product getPublications(authorId)
      (author, pubs) = result
    } yield AuthorPublications(author, pubs)

The only change is that due to an idiosyncrasy in the Scala compiler, you cannot produce the (author, pubs) tuple directly anymore, but have to capture the product in a temporary result first, then perform the extraction in a separate step. A small price to pay.

You're now prepared to run an article search with all the bells and whistles:

  val query = "matthias k"
  val search: Result[Unit] = findPublications(query) map { authorPubs =>
    renderResponse(200, s"Found $authorPubs")
  } recover {
    case InvalidQuery => renderResponse(400, s"Not a valid query: '$query'")
    case NotFound => renderResponse(404, s"No results found for '$query'")
  }
  
  Await.result(search.value, Duration.Inf)

On top of the success path, which you defined earlier, you're also calling recover with a custom handler that maps the errors you might encounter to an appropriate service response. Since search is not a Future anymore, but a Result, you need to call its value method to obtain the underlying Future before you can await its result.

Conclusion

This tutorial started by acknowledging that microservice composition is prone to errors and must be parallelized for efficient execution, and revealed how composable Futures are a powerful tool for modeling concurrent service calls in a functional way. You also saw their limited capabilities for parallel composition, which can be solved by enriching Futures with Cats; this enables you to produce tuples of results. We then showed you how to represent domain errors with sealed hierarchies, and how to use Either to write service functions that can return both errors and values. However, working with this stack of different effects can be cumbersome, so we wrapped things up by explaining how you can put monad transformers to work to bring clarity, readability, and convenience to your code. When put together, all of this illustrates how a typeful functional programming style can lead to expressive microservices that put the model first without compromising the often sophisticated and complex machinery behind it.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Cloud computing, Java development
ArticleID=1055432
ArticleTitle=Model-first microservices with Scala and Cats
publish-date=12182017