Separation of Concerns with Finagle
The Separation of Concerns (SoC) pattern is one of those software
architectural choices that everyone is helpful. It increases clarity, shortens the amount of code in the working
context, and minimizes the chance of side effects. For example, two concerns that should not require entanglement:
updating data and cache invalidation. Both are related, but one is concerned about business logic and database access,
while the other deals with the cache servers. Finagle’s generated FutureIface
can be used to keep these two separate.
Some techniques, such as Type Class polymorphism (ie: .Net’s Extension
Methods), or Aspect-Oriented Programming (AOP) necessitate a
new way of thinking – and a lot of practice to be correctly applied. On the other hand, using files to separate code,
such as with a Scala trait
(or C# Partial Classes) is an approach intuitive to most developers. Our approach is
similar: create a separate class for each concern, and let Finagle wire them together for us.
We know that Scrooge generates an interface for every service, let’s extend the interface for an arbitrary service Foo
into two classes.
class FooService extends FooApi.FutureIface {
def getFoo(id: Int): Future[Foo] = { ... }
def updateFoo(foo: Foo): Future[Boolean] = { ... }
}
class PostFooService extends TestApi.FutureIface {
def getFoo(id: Int): Future[Foo] = Future.never
def updateFoo(foo: Foo): Future[Boolean] = Future.never
}
In the foo service above, we have one class FooService
containing all our data logic (ie: { ... }
), and a second
class PostFooService
handling an orthogonal concern. The PostFooService
will contain any code that should be
executed after (ie: post) a method call in FooService
. Because we are reusing the FutureIface
both classes will have
access to the exact same input parameters. For simplicity, let’s assume that our post action is cache invalidation.
Right now all “post” methods return Future.never
, which is ok since we will never try resolve it. The only point of
the post class is to execute code, the original service implementation handles any return values. The Future.never
satisfies Scala’s type checking, returning a Future[Nothing]
, this matches any thrift return type. We could have also
used null, but within Scala I think this is a better choice.
The only implementation necessary in the post class is for updateFoo
, since we don’t expect a get
method to trigger
cache invalidation. Without loss of generality, we’ll call a method in an arbitrary external object
called OurCacheManager
to handle the underlying details – we are only concerned about demonstrating use patterns at
the moment.
def updateFoo(foo: Foo): Future[Boolean] = {
OurCacheManagerObject.invalidateFooById(foo.id)
Future.never
}
All the work coordinating calls and executing methods will be handled by Finagle. A Finagle Filter is perfect for chaining together service calls, the code could not be any more succinct.
class PostConcernFilter(postConcernService: Service[Array[Byte], Array[Byte]])
extends SimpleFilter[Array[Byte], Array[Byte]] {
def apply(request: Array[Byte], service: Service[Array[Byte], Array[Byte]]): Future[Array[Byte]] = {
service(request).onSuccess(_ => postConcernService(request))
}
}
The key idea to recognize is that we are instantiating a second Service – don’t worry, a Scrooge generated Service has
nothing to do with the network or external resources, so the overhead is minuscule. The final filter code should be easy
to follow, we have two services, the original business logic service that we always have, and a second service that we
will relay the original request input to if and only if the first service call was successful. A Future’s onSuccess
method allows us to register this callback, and since callbacks never directly return a value we are safe returning
whatever we want (ie: a Future.never
).
The builders for our service are:
val filter = new PostConcernFilter(
new FooApi$FinagleService(new PostFooService, new TBinaryProtocol.Factory))
val server = ServerBuilder()
.codec(ThriftServerFramedCodec())
.name("Foo")
.bindTo(socket)
.build(filter andThen new FooApi$FinagleService(new FooService, new TBinaryProtocol.Factory))
It’s important to note that this can be used for any functionality, whether it is to run post method call, pre method call, or both. The most common use case for a post action is cache invalidation, but other uses include audit trails, notifications, or triggering external resources to synchronize.
We attached a callback to the onSuccess
event, but there are also onFailure
and ensure
events that are useful.
Some uses of these two might be notifications, resource cleanup, or integrity checks.
Full Sources: Separation-of-Concerns-with-Finagle.scala