In Domain Driven Design (DDD) it is recommended to introduce a translation layer (aka anticorruption layer) between 2 bounded contexts. The role of the anticorruption layer is to avoid any concepts to leak from one domain into the other.
This is a sound idea as it keeps the domains isolated from each other ensuring they can evolve independently. After having implemented several anticorruption layers I realised that, although useful, they also introduced a lot of boilerplate code that doesn’t add much value to the business.
To this extent, let me introduce Fluent, a library that aims at getting rid of this boilerplate code by leveraging all the power of Shapeless and its generic programming.
If you follow this blog you’d probably noticed it’s been a while since the last post. Part of the reasons was that I was working on the first implementation of Fluent, struggling to understand how Shapeless and implicit resolution works.
First things first, let’s start by a brief overview of Fluent, how to get started and use it. Then in another post I’ll show you some of the internals and the lessons I learned along the way.
Setup
First of all you need to add the following lines to your build.sbt
resolvers += Resolver.bintrayRepo("beyondthelines", "maven") libraryDependencies += "beyondthelines" %% "fluent" % "0.0.1"
Context
As we don’t want to have any leaks into/from our internal domain model we need a translation layer to convert domain objects between their external and internal representation. Quite often both domains share some similarities and writing code to translate a case class into another similar (but not the same) case class isn’t very exciting.
Fluent exploits the fact that both domain models share some similarities (e.g. some fields are named the same but have different types) in order to convert a case class into another one.
Just a side note on protobuf: Protobuf is a handy tool to define the external domain model, plus there is a Scala plugin for protobuf: ScalaPB. ScalaPB generates case classes corresponding to the protobuf definitions. So in the end it doesn’t really matter how you get your domain case classes because Fluent is just a tool to translate one case class into another case class.
Converting a simple case class
So let’s walk through an example and define our domain models. (I am not going to use protobuf here in order to keep things simple but the only thing that matter is that you end up with a set of case classes):
object External { case class Circle(x: Double, y: Double, radius: Double, colour: Option[String]) }
object Internal { case class Point(x: Double, y: Double) sealed trait Colour object Colour { case object Blue extends Colour case object Red extends Colour case object Yellow extends Colour } case class Circle(origin: Point, radius: Double, colour: Option[Colour]) }
As you can see the External.Circle
and Internal.Circle
look similar. However the Internal
model is reacher as it defines types for Point
and Colour
whereas the External
model uses only primitive types (which makes sense if the external domain has to be serialised and set over the network or stored on disk).
Let’s create an instance from the external domain:
val externalCircle = External.Circle( x = 1.0, y = 2.0, radius = 3.0, colour = Some("Red") )
Writing code to translate from one domain to the other isn’t very exciting, so let’s use Fluent to do just that.
import fluent._ import cats.instances.option._ val internalCircle = external.translateTo[Internal.Circle]
Yes, that’s it only 2 imports and a call to translateTo
and we’re done! Isn’t that great?
All the rules used to translate from one domain to the other are provided by a set of implicit definitions provided by Fluent.
In this case it’s easy to figure out what Fluent does:
External.Circle
contains a x
and a y
of type Double
so Fluent can create a Point
from an External.Circle
. The radius
can be taken as it is and colour
needs to be turned into the correct type (Note that if the colour doesn’t exist in the expected values it would fail at runtime).
As you can see by using the same field names in both domains (although with different types) Fluent is able to generate the right instance using Shapeless Generic under the hood.
Using user-defined functions
Great, but what if we need more rules than what’s already available? Well, in that case one can always define its own rules using implicit functions. Let’s consider the following case to illustrate this use case:
object External { case class Post(author: String, body: String, timestamp: Long) }
import java.time.Instant object Internal { case class Author(name: String) case class Post(author: Author, body: String, tags: List[String], timestamp: Instant) }
Let’s create a simple external message:
val externalPost = External.Post( author = "Misty", body = "#Fluent is a cool library to implement your #DDD #translationLayer seamlessly", timestamp = 1491823712002L )
Now if we try to convert it straight away with
val internalPost = externalPost.transformTo[Internal.Post]
we get a compilation error
could not find implicit value for parameter transformer: fluent.shapeless.Transformer[External.Post,Internal.Post]
This is just the compiler telling us that it doesn’t know how to transform an External.Post
into an Internal.Post
.
Let’s have a closer look at the message fields: author
is fine – Fluent knows how to transform a String
into an Author
case class and the author
field name matches in both representation. body
is even easier as both name and type match.
There is no obvious way (for the compiler) to extract the tags
from the External.Post
. Let’s define a simple function that do just that:
implicit def extractTags(post: External.Post): List[String] = post.body.split("\\s").toList.filter(_.startsWith("#"))
Note that we can’t use a function that takes only a string to extract the tags
// doesn't work as Fluent can't figure out which field to use to extract the tags implicit def extractTags(body: String): List[String] = ???
That’s because Fluent can’t figure out that it needs to apply this function on the body
field.
There is a similar problem for timestamp
. This time the field name matches but there is no implicit definition to turn a Long
into an Instant
. Let’s fix it:
implicit def toInstant(timestamp: Long): Instant = Instant.ofEpochMilli(timestamp)
Now we these 2 implicit functions in scope we can compile again and run it.
Conclusion
Really this is all there is to know if you want to use this library. It is still in a very experimental stage however you can have a look at the source code on github.
The error messages you get at compile time are not always useful but reading through this blog post should have give you a clue on the way Fluent works and how to overcome such errors by defining your own implicit functions.
In case this sounds like magic to you (or you’re just curious about how things work) stay tuned for the next post covering the internals of Fluent where we’ll explore some Shapeless features and implicit resolution rules.