Getting Started with Lagom Framework

Reading Time: 5 minutes

Introduction

Lagom is a highly opinionated framework for building flexible, resilient, and responsive systems in Java and Scala.

It’s an open-source framework maintained by Lightbend. It offers libraries and development environments to build systems based on reactive microservices with best practices.

It supports multiple aspects from development to deployment by leveraging other reactive frameworks from Lightbend like Play and Akka:

Lagom model
Lagom

We design microservices to be isolated and autonomous, with loosely coupled communication between them. This facilitates synchronous or asynchronous communication through HTTP or WebSocket. Further, it also offers message-based communication through a broker like Kafka, providing at-least-once delivery semantics.

We also design microservices to own their data exclusively and have direct control over it; this draws from the principles of Bounded Context. It facilitates data persistence through well-known design patterns like Event Sourcing and CQRS. Lagom persists the event stream in the database through asynchronous APIs. The default database in Lagom is Cassandra.

Furthermore, other critical parts in developing loosely coupled microservices are service discovery and service gateway. We require them to provide location transparency in the way services communicate with each other and external clients.

A service locator is embedded in the Lagom’s development environment, which allows services to discover and communicate with each other. There’s a Service Gateway embedded as well, to allow external clients to connect to the Lagom services.

Working Example in Lagom

To explore the options to integrate Lagom with either Play of Akka, we first need a working example. In this section, we’ll build a simple microservice-based application leveraging the Lagom Framework.

Further, we’ll use this example to understand ways to integrate Lagom with Play or Akka. Please note that this example is based on the one provided in the standard documentation of Lagom but sufficient to cover the basics that we need.

Set-up

Typically, Lagom is useful when we have a full-blown microservice architecture that can benefit from the features that it has to offer. However, since the objective of this tutorial is to show how to integrate Play and Akka APIs in Lagom, we’ll keep it simple. We’ll just define a single microservice without any persistence and later expand it with Akka and Play integrations.

Lagom provides APIs in Java and Scala; however, we’ll use Scala for our example in this tutorial. Further, Lagom has an option of using Maven or sbt with Java, but sbt is the only choice with Scala.

The easiest way to bootstrap a Lagom project is to use the starter tool provided by Lagom. Alternatively, we can define the project structure and let sbt generate the bootstrap for us:

organization in ThisBuild := "com.baeldung"
version in ThisBuild := "1.0-SNAPSHOT"
scalaVersion in ThisBuild := "2.13.0"

val macwire = "com.softwaremill.macwire" %% "macros" % "2.3.3" % "provided"
val scalaTest = "org.scalatest" %% "scalatest" % "3.1.1" % Test

lazy val `hello` = (project in file(".")).aggregate(`hello-api`, `hello-impl`)

lazy val `hello-api` = (project in file("hello-api"))
  .settings(
    libraryDependencies ++= Seq(
      lagomScaladslApi
    )
  )

lazy val `hello-impl` = (project in file("hello-impl"))
  .enablePlugins(LagomScala)
  .settings(
    libraryDependencies ++= Seq(
      lagomScaladslTestKit,
      macwire,
      scalaTest
    )
  )
  .settings(lagomForkedTestSettings)
  .dependsOn(`hello-api`)

This is a simple project structure that we can define in an SBT build file.

Please note that Lagom suggests defining separate projects for the service interface and its implementation for every microservice. Hence, as we can see, we have a hello-world microservice defined in projects “hello-api” and “hello-impl”. Further, the project “hello-impl” depends on the project “hello-api”.

Other than this, there are regular dependencies for Lagom to work like dependencies for Cassandra and Kafka. Note that these are optional, and we may not need them in every microservice.

Defining Messages

The first thing we have to do is to define messages that our service will consume and produce. We also need to ensure that we provide either implicit or custom message serializer that Lagom can use to serialize and deserialize request and response messages.

Let’s quickly define the messages:

case class Job(jobId: String, task: String, payload: String)
object Job {
    implicit val format: Format[Job] = Json.format
}
case class JobAccepted(jobId: String)
object JobAccepted {
    implicit val format: Format[JobAccepted] = Json.format
}

Now let’s understand these messages little better:

  • We’ve defined two case classes Job and JobStatus and their companion objects
  • We’ve added the implicit JSON serialization; by default, Lagom uses Play JSON for this purpose
  • These messages represent the request and the corresponding response for a service

Defining the Service

As we’ve seen, Lagom favors splitting a microservice into a service interface and it’s implementation. Hence, our next step would be to define the service interface:

trait HelloService extends Service {
    def submit(): ServiceCall[Job, JobAccepted]
    override final def descriptor: Descriptor = {
        import Service._
        named("hello")
            .withCalls(
                pathCall("/api/submit", submit _)
            ).withAutoAcl(true)
    }
}

This is a simple Scala trait which we know as a service descriptor in Lagom. The service descriptor defines how we can implement and invoke a service.

Let’s understand a few important things here:

  • We’ve defined a single call “/api/submit” that maps to the submit function
  • The function returns handle to a ServiceCall which takes parameters Job and JobAccepted
  • These parameters are message types which can be strict or streamed
  • We can use this handle to invoke the call, which executes the work
  • We’re using a path-based identifier to use path and query string to route the calls
  • Other possible identifiers include name-based identifiers and REST identifiers

Implementing the Service

Next, we have to provide the implementation for the service interface we’ve just defined. This must include an implementation for each call specified by the descriptor:

class HelloServiceImpl()(implicit ec: ExecutionContext)
  extends HelloService {
    override def submit(): ServiceCall[Job, JobAccepted] = ServiceCall {
      job =>
        Future[String] {JobAccepted(job.jobId)} 
    } 
}

This is a fundamental implementation for the function we defined service descriptor. However, there are a few things worth noting:

  • The method does not execute the call but returns the call to be executed as a lambda
  • This provides a convenient way to compose this call in a function-based composition
  • The call itself does not return a value but a Future which is a promise
  • This gives us a powerful way to create asynchronous and non-blocking, reactive applications

Creating Lagom Application

Now, we have to bring the services and their implementations together in a Lagom application. Lagom uses compile-time dependency injection to wire together this Lagom application. Lagom prefers Macwire, which provides lightweight macros that locate dependencies for the components.

Let’s see a quick and easy way to create our LagomApplication:

abstract class HelloApplication(context: LagomApplicationContext)
    extends LagomApplication(context)
    with AhcWSComponents {
  override lazy val lagomServer: LagomServer = 
    serverFor[HelloService](wire[HelloServiceImpl])
}

Here, some interesting things are happening behind these simple lines:

  • HelloApplication gets mixed with AhcWSComponents through Macwire
  • Moreover, we can mix many other components from Scala or third parties
  • We implement the method lagomServer which Lagom uses to discover the service bindings
  • We can use Macwire’s wire macro to inject other dependencies into HelloServiceImpl
  • This class is still abstract as it requires the implementation of the method serviceLocator

Finally, we need to write an application loader such that the application can bootstrap itself. We can do this conveniently in Lagom by extending the LagomApplicationLoader:

class HelloLoader extends LagomApplicationLoader {
    override def load(context: LagomApplicationContext): LagomApplication =
        new HelloApplication(context) {
            override def serviceLocator: ServiceLocator = NoServiceLocator
        }

    override def loadDevMode(context: LagomApplicationContext): LagomApplication =
        new HelloApplication(context) with LagomDevModeComponents

    override def describeService = Some(readDescriptor[HelloService])
}

Let’s go over the important things to note in this piece of the code:

  • We’re implementing the two methods required, load and loadDevMode
  • This is where we mix the appropriate serviceLocator with our HelloApplication
  • The method describeService is optional but can help configure components like service gateways

Configurations

It’s possible to configure many different parts of Lagom services through values provided in the configuration file application.conf.

However, for our simple example, the only thing that we need to configure is our application loader:

play.application.loader = com.baeldung.hello.impl.HelloLoader

Running the Example

Now, we have done all that is required to create a simple working example in Lagom. So, we are finally ready to run the application.

Using a command prompt and sbt tool, it’s just a single command to run the entire Lagom application:

sbt runAll

Once the server bootstraps successfully, we should be able to post our jobs to this service using a tool like postman with POST method:

http://localhost:9000/api/submit

{
    "jobId":"jobId",
    "task":"task",
    "payload":"payload"
}

Thanks for reading this blog Stay tune for more blogs. Happy learning!!

knoldus

Written by 

Meenakshi Goyal is a Software Consultant and started her career in an environment and organization where her skills are challenged each day, resulting in ample learning and growth opportunities. Proficient in Scala, Akka, Akka HTTP , JAVA. Passionate about implementing and launching new projects. Ability to translate business requirements into technical solutions. Her hobbies are traveling and dancing.