Skip to Content
Course content

84: sbt Build Definition In Depth

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Wait, is build.sbt actually a Scala file or just a config file?

This is the first thing that trips everyone up. It looks like a properties file, but it's actually a Scala DSL. When sbt starts up, it evaluates the build.sbt file as Scala code to determine the build graph. This is why you can use variables, loops, and even conditional logic inside your build definition.

I've seen a lot of developers hard-code the same version number in five different places. Since this is just Scala, don't do that. Define a variable at the top. It makes upgrading your dependencies a one-line change instead of a search-and-replace nightmare.

val akkaVersion = "2.6.20"

libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion,
  "com.typesafe.akka" %% "akka-stream"      % akkaVersion
)


How do I split my project into separate modules without creating five different repos?

As your project grows—say you're building a payment processing system—you don't want your API logic mixed in with your database schemas and your core domain models. You want a multi-project build. This allows you to keep everything in one repository but maintain strict boundaries between components.

You define these as project objects. The key here is the .dependsOn() method, which tells sbt exactly which modules need to be compiled first. If payment-api depends on payment-core, sbt is smart enough to handle the ordering.

lazy val core = (project in file("core"))
  .settings(
    name := "payment-core",
    libraryDependencies += "org.typelevel" %% "cats-core" % "2.9.0"
  )

lazy val api = (project in file("api"))
  .dependsOn(core)
  .settings(
    name := "payment-api",
    libraryDependencies += "com.typesafe.play" %% "play-json" % "2.9.0"
  )

lazy val root = (project in file("."))
  .aggregate(core, api)

Note that aggregate is different from dependsOn. Aggregation just means "when I run compile on the root, also compile these other projects," but it doesn't create a classpath dependency.

I keep seeing "Settings" and "Tasks" mentioned—what's the actual difference?

I like to think of it this way: a Setting is a value; a Task is an action. Settings are generally static (or calculated once), whereas Tasks are things you actually "run" from the sbt shell.

  • scalaVersion := "2.13.12" is a Setting. You're telling sbt what version to use.
  • compile is a Task. You're telling sbt to do the act of compiling.

The confusing part is that they both use the := and += operators. If you're ever unsure, ask yourself: "Am I describing a property of my project, or am I triggering a process?" If it's a process, it's a Task.

Can I write my own sbt commands to automate boring stuff?

Absolutely. This is where sbt becomes powerful. You can define custom tasks to handle things like generating documentation, cleaning up specific temp folders, or validating a config file before deployment. To do this, you define a TaskKey and then assign a block of code to it.

Here is a quick example of a task that checks if a required secrets.conf file exists in your project root. It's a simple check, but it saves you from a runtime crash in CI/CD.

val validateConfig = taskKey[Unit]("Checks if secrets.conf exists")

validateConfig := {
  val file = file("secrets.conf")
  if (file.exists()) {
    println("Config found. Ready to go!")
  } else {
    sys.error("Missing secrets.conf! Please create it from secrets.conf.example")
  }
}

Now, you can just type validateConfig in your sbt shell, and it'll run that logic. It's a huge time-saver when onboarding new developers to a team.




📋 Practical Task

Exercise: Implementing a Multi-Module Build for a Payment Gateway

You are tasked with restructuring a monolithic payment application into a multi-project build to improve build times and separation of concerns. Create a build.sbt file that satisfies the following requirements:

  • Define a root project that aggregates two sub-projects: domain and infrastructure.
  • The domain project should be located in the domain/ directory and have a dependency on org.typelevel %% "cats-core" % "2.9.0".
  • The infrastructure project should be located in the infrastructure/ directory, depend on the domain project, and have a dependency on com.typesafe.akka %% "akka-actor-typed" % "2.6.20".
  • Define a custom task called checkBuildHealth that simply prints "Build structure is valid" to the console.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.