Building microservices with Finatra and Slick

I've been away for a while and I've been playing with Finatra, so, I decided to write this post because I really enjoyed using it.
Lately I've been using Scala a lot in my personal projects and even for testing my Java applications (it is much better than using JUnit, believe me), so, I also tried a few scala web frameworks out and I found out that Finatra is the easiest one (at least for me).

First of all, what is Finatra? Well, it is a web framework created by Twitter and built on TwitterServer and Finagle. You can check their home page over here. With Finatra, you can build http and thrift services that are fast and testable.
We also are using Slick, which is, according to their web site, a modern database query and access library for Scala. With Slick you can easily access your database using the Scala functional paradigm in a reactive way.
For testing porpuses, we are using H2 as our DB, but, you can use any database that Slick supports. You can check if your database is supported over here.

I also am using Gradle in this post (because it is simpler and much less verbose than SBT), so, if you want to use SBT, please, check this github repository.

We should start with our build.gradle:

apply plugin: 'scala'
apply plugin: 'gradle-one-jar'

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.github.rholder:gradle-one-jar:1.0.4'
    }
}

repositories {
    jcenter()


    maven {
        url "https://maven.twttr.com"
    }
}

jar.archiveName = "finatra-slick-microservice-nolibs.jar"

task uberJar(type: OneJar) {
    mainClass = 'org.thedevpiece.finatra.slick.microservices.ApplicationHttpServerMain'
    archiveName = 'finatra-slick-microservice.jar'
}

final scalaVersion = "_2.11"

final finatra = "2.6.0"
final slick = "3.1.1"
final scalaAsync = "0.9.5"
final twitterBijection = "0.9.2"
final logback = "0.9.28"

dependencies {
    compile 'org.scala-lang:scala-library:2.11.8'

    compile "com.twitter:finatra-http$scalaVersion:$finatra"
    compile "com.twitter:finatra-slf4j$scalaVersion:$finatra"
    compile "com.twitter:finatra-jackson$scalaVersion:$finatra"

    compile "com.twitter:inject-server$scalaVersion:$finatra"
    compile "com.twitter:inject-app$scalaVersion:$finatra"
    compile "com.twitter:inject-modules$scalaVersion:$finatra"

    compile "com.typesafe.slick:slick$scalaVersion:$slick"

    compile "com.h2database:h2:1.4.181"

    compile "org.scala-lang.modules:scala-async$scalaVersion:$scalaAsync"
    compile "org.scala-lang.modules:scala-async$scalaVersion:$scalaAsync"

    compile "com.twitter:bijection-util$scalaVersion:$twitterBijection"
    compile "com.twitter:bijection-core$scalaVersion:$twitterBijection"

    compile "ch.qos.logback:logback-classic:$logback"

    testCompile "com.twitter:finatra-http$scalaVersion:$finatra:tests"
    testCompile "com.twitter:finatra-slf4j$scalaVersion:$finatra:tests"
    testCompile "com.twitter:finatra-jackson$scalaVersion:$finatra:tests"
    testCompile "com.twitter:inject-server$scalaVersion:$finatra:tests"
    testCompile "com.twitter:inject-app$scalaVersion:$finatra:tests"
    testCompile "com.twitter:inject-core$scalaVersion:$finatra:tests"
    testCompile "com.twitter:inject-modules$scalaVersion:$finatra:tests"

    testCompile 'junit:junit:4.12'
    testCompile "org.scalatest:scalatest$scalaVersion:3.0.0"
    testCompile "org.specs2:specs2$scalaVersion:2.3.12"
    testCompile "com.google.inject.extensions:guice-testlib:4.1.0"
}

Our first class is a repository using Slick to access the database and Guice as our dependency injector:

package org.thedevpiece.finatra.slick.microservices.domain

import com.google.inject.{Inject, Singleton}
import slick.driver.JdbcProfile

import scala.concurrent.ExecutionContext.Implicits.global

import scala.concurrent.Future

@Singleton
class UserRepository @Inject()(val driver: JdbcProfile) {
  import driver.api._

  /*
    We are assuming we have a table named 'USERS' that has the following columns:
    - IDT_USER (number, auto inc, PK)
    - DES_USERNAME (varchar)
    - NUM_AGE (number)
    - DES_OCCUPATION (varchar)
   */
  class Users(tag: Tag) extends Table[(Option[Long], String, Int, String)](tag, "USERS") {
    def id = column[Long]("IDT_USER", O.AutoInc, O.PrimaryKey)
    def username = column[String]("DES_USERNAME")
    def age = column[Int]("NUM_AGE")
    def occupation = column[String]("DES_OCCUPATION")

    override def * = (id.?, username, age, occupation)
  }

  val users: TableQuery[Users] = TableQuery[Users]
  val db = Database.forConfig("database")

  def findById(id: Long): Future[Seq[(Option[Long], String, Int, String)]] = {
    val query = users.filter(_.id === id).result
    db.run(query).map({
      case Nil => None
      case x +: Nil => Some(x)
    })
  }

  def create(user: (Option[Long], String, Int, String)): Future[Option[Long]] = {
    val action: DBIO[Seq[Long]] = (users returning users.map(_.id)) ++= List(user)
    db.run(action).map({
      case Nil => None
      case x +: Nil => Some(x)
    })
  }
}

We need to provide a JdbcProfile to Guice inject into our UserRepository class, so:

package org.thedevpiece.finatra.slick.microservices.modules

import com.google.inject.{ Provides, Singleton }
import com.twitter.inject.TwitterModule
import slick.driver.{ H2Driver, JdbcProfile }

object DefaultModule extends TwitterModule {
  @Singleton
  @Provides
  def jdbcDriver(): JdbcProfile = H2Driver
}

Finatra uses Jackson to serialize Scala objects into JSON, so, we need to create our models to be serialized:

package org.thedevpiece.finatra.slick.microservices.api

object Resources {
  case class Message(message: String)
  case class User(id: Option[Long], username: String, age: Int, occupation: String)
}

We have two services in our UserEndpoint: create and findById. Since Finatra uses the Twitter util classes, we must convert our scala.concurrent.Future to their com.twitter.util.Future, knowing that Finatra is reactive, so, you should not block threads in your app:

package org.thedevpiece.finatra.slick.microservices.utils

import com.twitter.bijection.Conversion._
import com.twitter.bijection.twitter_util.UtilBijections.twitter2ScalaFuture
import com.twitter.util.{ Future => TwitterFuture }

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{ Future => ScalaFuture }

object FutureUtils {
  implicit class FutureImplicit[A](val x: ScalaFuture[A]) {
    def asTwitterFuture: TwitterFuture[A] = x.as[TwitterFuture[A]]
  }
}

And then we can create our Endpoint:

package org.thedevpiece.finatra.slick.microservices.api

import com.google.inject.{Inject, Singleton}
import com.twitter.finagle.http.Request
import com.twitter.finatra.http.Controller
import org.thedevpiece.finatra.slick.microservices.utils.FutureUtils._
import org.thedevpiece.finatra.slick.microservices.api.Resources.{Message, User}
import org.thedevpiece.finatra.slick.microservices.domain.UserRepository

import scala.concurrent.ExecutionContext.Implicits.global
import scala.language.postfixOps

@Singleton
class UserEndpoint @Inject()(repository: UserRepository) extends Controller {
  get("/users/:id") { request: Request =>
    repository
      .findById(request.getLongParam("id"))
      .map({
        case None => response.notFound(Message(s"Resource with id: ${request.getLongParam("id")} not found"))
        case Some(user) => response.ok((User.apply _).tupled(user))
      }) asTwitterFuture //converting scala future to twitter future
  }

  post("/users") { user: User =>
    repository
      .create(User.unapply(user).get)
      .map({
        case None => response.internalServerError(Message("Error while creating the user"))
        case Some(id) => response.created.location("/users/" + id)
      }) asTwitterFuture //converting scala future to twitter future
  }
}

Our last class will be ApplicationHttpServer:

package org.thedevpiece.finatra.slick.microservices

import com.twitter.finatra.http.HttpServer
import com.twitter.finatra.http.routing.HttpRouter
import org.thedevpiece.finatra.slick.microservices.api.UserEndpoint
import org.thedevpiece.finatra.slick.microservices.modules.DefaultModule

class ApplicationHttpServer extends HttpServer {
  override val defaultFinatraHttpPort: String = ":8080"
  override val modules = Seq(DefaultModule)

  override protected def configureHttp(router: HttpRouter): Unit = {
    router
      .add[UserEndpoint]
  }
}

object ApplicationHttpServerMain extends ApplicationHttpServer {
}

In this class we configure our endpoint, but, you can also configure several other stuff, such as exception mappers, filters, etc. If you want to run your app, just run the ApplicationHttpServerMain and your app will be up.

Finally, add a file named application.conf to src/main/resources with the following lines to configure your database access:

database = {
  url = "jdbc:h2:mem:test1"
  driver = org.h2.Driver
  connectionPool = disabled
  keepAliveConnection = true
}

Just to show how it is easy to create tests using Finatra, we are going to create a feature test that assures that our services are working properly:

package org.thedevpiece.finatra.slick.microservices.api

import com.twitter.finagle.http.Status
import com.twitter.finatra.http.EmbeddedHttpServer
import com.twitter.inject.server.FeatureTest
import org.thedevpiece.finatra.slick.microservices.ApplicationHttpServer
import org.thedevpiece.finatra.slick.microservices.domain.UserRepository
import slick.driver.JdbcProfile

class UserEndpointTest extends FeatureTest {
  override val server = new EmbeddedHttpServer(new ApplicationHttpServer)

  override protected def beforeAll(): Unit = {
    val driver = injector.instance[JdbcProfile]
    val repository: UserRepository = injector.instance[UserRepository]

    import driver.api._

    //creating the schema of the table users
    val insertActions = DBIO.seq(
      repository.users.schema.create
    )

    repository.db.run(insertActions)
  }

  "Clients" should {
    "be able to create user resources" in {
      val response = server.httpPost(
        path = "/users",
        postBody =
          """{
            	"username": "Gabriel",
            	"age": 23,
            	"occupation": "Software Engineer"
            }"""
      )

      response.status shouldBe Status.Created
      response.location should not be empty
    }
  }

  "Clients" should {
    "be able to fetch a created user" in {
      val response = server.httpPost(
        path = "/users",
        postBody =
          """{
            	"username": "Gabriel",
            	"age": 23,
            	"occupation": "Software Engineer"
            }""",
        andExpect = Status.Created
      )

      response.location should not be empty

      val location = response.location.get
      val id = location.split("/users/")(1)

      server.httpGet(
        path = location,
        andExpect = Status.Ok,
        withJsonBody = s"""{
              "id": $id,
            	"username": "Gabriel",
            	"age": 23,
            	"occupation": "Software Engineer"
            }"""
      )
    }
  }
}

And that's all. I hope this post can be helpful to you. Probably soon I'll post more about other Scala frameworks.

Other posts about microservices

Building microservices with Akka HTTP and MongoDB
Building microservices using Undertow, CDI and JAX-RS
Building microservices with Kotlin and Spring Boot

Thank you, and, any questions, please, leave a comment!

Gabriel Francisco

Software Engineer at GFG, 25 years, under graduated in Computer Science and graduated in Service-oriented Software Engineering. Like playing guitar once in a while. Oh, and I'm kind of boring.

São Paulo

comments powered by Disqus