At bol.com, we have a microservice-based software
ecosystem. Every functionally grown enough monolithic software is split into
its own microservice, which exposes a simple REST interface to the outside
world. Fine-grained functional scope of the services helps to keep the code
base clean and lets programmers just to stick to the point. Which implicitly
also results in a coding style such that the clean code has a higher
priority over performance. Optimizations are introduced whenever they are
necessary and after careful profiling. For the rest, KISS
principle is applied.

The Problem

For a particular microservice, we are given the task of returning a snapshot
of the current state of the data model. In this post, I will use the following
simple model to demonstrate the problem at hand and the solution I came up
with.

In a nutshell, service basically encapsulates User and Group entities in
the database. While doing so, database records are first interfaced via a
DAO layer requiring a
database connection to operate.

Next, DAO layers are interfaced by services
such that they acquire a database connection from the JDBC pool and make
relevant DAO calls.

traitUserService{defcreate(name:String,groupId:Int):UnitdeffindAll():Seq[User]}@ComponentclassUserServiceImpl@Autowired()(userDao:UserDao,groupDao:GroupDao,connectionPool:ConnectionPool)extendsUserService{defcreate(name:String,groupId:Int):Unit=connectionPool.acquire{implicitconnection=>require(groupDao.findById(groupId).isDefined,"non-existing group id")userDao.create(user,groupId)}deffindAll():Seq[User]=connectionPool.acquire{implicitconnection=>userDao.findAll()}}// GroupService and GroupServiceImpl are implemented likewise...

This also applies to SnapshotService, which accesses UserDao and
GroupDao using a single database connection.

While this approach totally makes sense, as the user and group collections
start to grow in size, calls to SnapshotService.take() started to take more
and more time.

The Solution

Given the fact that constructing a snapshot is a read-only operation, I
anticipated querying UserDao.findAll() and GroupDao.findAll() in parallel
will buy us quite some SnapshotService.take() time. That being said, it is
crucial to note that querying DAOs via separate database connections in
parallel is going to violate the transactional safety, but in our case it has
a negligible margin of error for fetching collections in parallel.

I updated the existing SnapshotService to replace its UserDao and
GroupDao calls to UserService and GroupService instead (to provide each
DAO a dedicated connection) and perform these fetch operations in parallel
through an ExecutorService as follows:

Conclusion

Scala
Futures
provide a powerful abstraction to run certain tasks in parallel using a thread
pool in the back of the scenes. At the beginning I was afraid that it might
necessitate too much boilerplate just to get a simple thing done, but luckily
it turned out to be a quite easy task.

In this example, I needed to trade off the transactional consistency of
fetched collections for the sake of increasing performance using multiple
threads to query the database. While this implied a negligible error margin
for the particular problem at hand here, it is not something desirable at
every microservice. But that is the discussion of another blog post.