Enthusiastic Java, Scala and Haskell programmer with a long history of large and successful systems. Known author, speaker, motivator and coach. Jan is a DZone MVB and is not an employee of DZone and has posted 26 posts at DZone. You can read more from them at their website. View Full User Profile

Maven, SBT and Modularisation

11.27.2012
| 3302 views |
  • submit to reddit

Let me revisit the Maven to SBT post, and expand on the modularisation of Akka applications. I will also outline the Akka AMQP addition, which allows you to use AMQP brokers like my favourite RabbitMQ in your Akka applications. Again, it is a long post, so gather some food & drink to make it all the way to the end.

Maven to SBT

The first thing to tackle is the source code modularisation. How do we build & deploy our application and how do our choices influence the packaging and building approaches. I shall start with typical Maven strcutrue. Imagine a typical Akka application that constructs the important actor structure in the core module, operating on the instances of classes in the domain. We then expose the functionality in the api and expose the API on HTTP in the web module. To support our efforts throughout the code, we may even have the test module, which helps us write code in the tests.

In a typical Maven beast, you’d create these modules (domain, core, …) as separate modules, with explicitly defined dependencies on the other modules. The reasoning is that this ensures that you don’t skip tiers; and that you can reuse portions of your application’s code. (Assuming you have some sort of company-wide artifact repository.)

On the face of it, this sounds good. But my experience with large project tells me that this almost never works as well as you think. The clear dependencies between the modules become tangled; and often the strict “separate project” structure prevents large refactorings. Finally, you almost always deploy the entire application, packaged up in, say a WAR. Our reasons for using multi-module Maven projects are no longer that relevant; and in most cases, they actually hurt us.

Out with Maven!

This is the line you’ve no doubt been waiting for. Out with Maven, in with SBT; and while we’re at it, let’s build single-module SBT project. We’ll build a small Akka application whose small portion connects RabbitMQ to deliver & route the messages. Naturally, we’ll include tests, including tests that exercise the AMQP componentry.

First of all, all our source code will sit in the src directory. Because we also need some SBT plugins, we’ll create the project directory. We will complete the picture with the build.sbt file; along with a few good housekeeping files. We need something like this:

$ ls -la
-rw-r--r--  1 janmachacek  staff  1155 17 Nov 16:42 README.md
-rw-r--r--  1 janmachacek  staff  2389 17 Nov 16:42 build.sbt
drwxr-xr-x  2 janmachacek  staff   102 11 Nov 17:00 project
drwxr-xr-x  4 janmachacek  staff   136 11 Nov 17:00 src
-rw-r--r--  1 janmachacek  staff    31 11 Nov 17:00 version.sbt

Turning to build.sbt, we have the dependencies and other bits and pieces. Here it is in its entirety:

import sbtrelease._

/** Project */
name := "Akka Patterns"

version := "1.0"

organization := "org.cakesolutions.akkapatterns"

scalaVersion := "2.10.0-RC2"

/** Shell */
shellPrompt := { state => System.getProperty("user.name") + "> " }

shellPrompt in ThisBuild := { state => Project.extract(state).currentRef.project + "> " }

/** Dependencies */
resolvers += "spray repo" at "http://repo.spray.io"

resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/"

resolvers += "Sonatype OSS Releases" at "http://oss.sonatype.org/content/repositories/releases/"

resolvers += "Sonatype OSS Snapshots" at "http://oss.sonatype.org/content/repositories/snapshots/"

libraryDependencies <<= scalaVersion { scala_version => 
  val sprayVersion = "1.1-M5"
  val akkaVersion  = "2.1.0-RC2"
  Seq(
    "com.typesafe.akka" % "akka-kernel"       % akkaVersion cross CrossVersion.full,
    "io.spray"          % "spray-can"         % sprayVersion,
    "io.spray"          % "spray-routing"     % sprayVersion,
    "io.spray"          % "spray-httpx"       % sprayVersion,
    "io.spray"          % "spray-util"        % sprayVersion,
    "io.spray"          % "spray-client"      % sprayVersion,
    "io.spray"          % "spray-json"        % "1.2.2" cross ...,
    "org.mongodb"       % "mongo-java-driver" % "2.9.3",
    "com.aphelia"      %% "amqp-client"       % "1.0",
    "io.spray"          % "spray-testkit"     % sprayVersion % "test",
    "com.typesafe.akka" % "akka-testkit"      % akkaVersion  % "test" cross ...,
    "org.specs2"        % "classycle"         % "1.4.1" % "test",
    "org.specs2"        % "specs2"            % "1.12.2"     % "test" cross ...
  )
}

/** Compilation */
javacOptions ++= Seq("-Xmx1812m", "-Xms512m", "-Xss6m")

javaOptions += "-Xmx2G"

scalacOptions ++= Seq("-deprecation", "-unchecked")

maxErrors := 20 

pollInterval := 1000

logBuffered := false

cancelable := true

testOptions := Seq(Tests.Filter(s =>
  Seq("Spec", "Suite", "Unit", "all").exists(s.endsWith(_)) &&
    !s.endsWith("FeaturesSpec") ||
    s.contains("UserGuide") || 
    s.contains("index") ||
    s.matches("org.specs2.guide.*")))

/** Console */
initialCommands in console := "import org.cakesolutions.akkapatterns._"

Nothing too funky. Let’s take a look at our src directory, which contains the entire source code, split into the main and the test codebase. We run the main code on the server and we run the test when we want to check that the main does the right thing. The packages we create in the bowels of the src directory mimicks our Maven modules:

$ ls -la
total 8
drwxr-xr-x  9 janmachacek  staff   340 17 Nov 16:42 .
drwxr-xr-x  3 janmachacek  staff   102 10 Nov 13:06 ..
drwxr-xr-x  2 janmachacek  staff   238 17 Nov 16:42 api
drwxr-xr-x  4 janmachacek  staff   272 17 Nov 16:42 core
drwxr-xr-x  2 janmachacek  staff   272 17 Nov 16:42 domain
drwxr-xr-x  2 janmachacek  staff   136 17 Nov 16:42 main
drwxr-xr-x  2 janmachacek  staff   102 17 Nov 16:42 test
drwxr-xr-x  2 janmachacek  staff   102 17 Nov 16:42 web

This structure requires more discipline; you’re free to tangle your packages. Instead of relying on the rigid structure of our Maven build (or the multi-project SBT build), we will include tests that verify the architectural soundness of our codebase. We shall be using Specs2, so the test that verifies our architecture should be no surprise. (Note that you’ll need to compile & run the code using JDK 1.7.)

class ArchitectureSpec extends Specification 
  with Analysis with ClassycleDependencyFinder {

  "The architecture" should {
    "Have properly defined layers" in {
      val ls = layers(
        "main",
        "web",
        "api",
        "core",
        "domain"
      ).withPrefix("org.cakesolutions.akkapatterns").
        inTargetDir("target/scala-2.10")

      ls must beRespected
    }
  }

}

So, instead of having separate Maven modules and having some of our architectual constraints expressed in the modules’ dependencies, we have moved our architectural tests to our testing code.

Deploying & running the modules

If our project includes just a single main method, all that you have to do to boot your project is to type sbt run and you’re up!

Suppose you now have two main components in your applicaiton; and you connect these two components over AMQP. So, we now have two main methods. Trying to execute sbt run will ask which main you want to execute? So, instead of sbt run, we’ll need to say sbt run-main org.cakesolutions.akkapatterns.main.Server to boot the core actors and sbt run-main org.cakesolutions.akkapatterns.Keystore to run the key store component.

Even though we can control which component to run, we always deploy the entire codebase.

I’ll give you the details of the AMQP-based actor communication in the next few days.

Published at DZone with permission of Jan Machacek, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)