Key Takeaways
- sbt builds Scala and Java projects, manages dependencies, and provides an interactive shell for everyday development.
build.sbtdefines your project;project/build.propertiespins the sbt version used to build it.- Keep application code under
src/mainand tests undersrc/test, with separate Scala, Java, and resource directories. - In sbt 2.x,
testis incremental and cached. UsetestFullwhen you need a complete, uncached test run.
sbt is a build tool for Scala and Java that resolves libraries, compiles source code, runs tests, and packages artifacts. Its conventional project layout and interactive shell give you a repeatable development workflow without configuring every build step yourself.
What Is sbt?
sbt is especially useful for Scala projects that need dependency management, multiple modules, or custom build logic. Its build definitions use a Scala-based domain-specific language: a small configuration language built from Scala expressions. You don't need to master Scala to understand a basic build.
If you're still learning the language, start with our introduction to Scala versus Java. The important distinction here is that sbt is the build tool, not the Scala compiler or the application runtime.
sbt isn't your only option. These are useful starting points when choosing a workflow:
| Tool | Build configuration | When to consider it |
|---|---|---|
| sbt | Scala-based settings, dependencies, and tasks in build.sbt | Scala projects needing reusable build logic and multi-project workflows. |
| Maven | An XML project model in pom.xml | A team already standardized on Maven's project model and lifecycle. See the Maven POM introduction. |
| Gradle | Kotlin or Groovy build scripts, with a Scala plugin | A team already using Gradle across JVM projects. The Gradle Scala plugin documentation explains its Scala compilation tasks. |
| Scala CLI | Command-line options and directives in source files | Scripts, experiments, and small applications that don't need sbt's extensible task model. See Scala CLI's scope and features. |
For an existing repository, learn its build tool before proposing a replacement. For a new Scala project with custom build requirements, sbt is a sensible choice—not a requirement imposed by the language.
Before You Start
Install a JDK and the sbt runner using the official sbt installation options. sbt 2.x requires JDK 17 or newer; JDK 21 is a compatible choice for this example. Check the Java installation your terminal sees:
java --versionThis tutorial pins sbt 2.0.9, Scala 3.9.0 LTS, and MUnit 1.3.6. These are explicit example versions, not instructions to upgrade an existing project blindly.
How sbt Works: Project Structure
Create a directory named sbt-intro. The conventional sbt project structure looks like this; the example source files are added below. You don't need to create unused Java or resource directories.
sbt-intro/
|-- build.sbt
|-- project/
| |-- build.properties
| `-- plugins.sbt (optional)
|-- src/
| |-- main/
| | |-- scala/example/Main.scala
| | |-- java/
| | `-- resources/
| `-- test/
| |-- scala/example/GreetingSuite.scala
| |-- java/
| `-- resources/
`-- target/ (generated)src/main/scalaandsrc/main/javacontain application or library sources.src/main/resourcescontains classpath resources, such as XML configuration files.src/test/scala,src/test/java, andsrc/test/resourcescontain test-only code and resources.target/holds generated build output. Its nested layout differs between sbt 1.x and 2.x, so don't hard-code an old artifact path into automation.
These source-directory conventions are familiar from Maven, but build.sbt and the build-support files under project/ are sbt-specific. Paths are relative to the selected project's base directory—not filesystem-root paths such as /main/scala.
Pin sbt in project/build.properties
Create project/build.properties with this content:
sbt.version=2.0.9The installed runner invokes the project's declared sbt version, downloading it if necessary. This separates the launcher installed on your machine from the sbt version used by a particular repository. Commit the file so teammates and continuous integration use the same pin.
Understand the project directory
The project/ directory supports the build itself. Scala files there can define shared dependency declarations, helper objects, or local plugins; they aren't application sources. External build plugins are commonly declared in project/plugins.sbt using addSbtPlugin(...), as explained in the plugin basics guide. This example needs no plugins.
Build Definition: A Minimal build.sbt
Create build.sbt in the project root. The following is sbt's Scala-based build syntax:
scalaVersion := "3.9.0"
lazy val root = rootProject
.settings(
name := "sbt-intro",
version := "0.1.0-SNAPSHOT",
libraryDependencies +=
"org.scalameta" %% "munit" % "1.3.6" % Test
)scalaVersion selects the Scala compiler for your project. name and version identify the project, while libraryDependencies declares external libraries. The := operator assigns a setting; += appends an entry. rootProject identifies the project in the build's root directory.
In sbt 2.x, settings written outside .settings(...), such as the scalaVersion line above, are shared with all subprojects. Project-specific settings belong inside the relevant project's .settings(...). The Scala version used to interpret the build definition is determined by sbt, independently of your application's scalaVersion.
Dependencies, configurations, and repositories
The dependency entry adds MUnit, a Scala testing library. On the JVM, %% adds the Scala binary-version suffix to the artifact name: this Scala 3 build requests munit_3. Use a single % between organization and artifact for Java libraries that don't use Scala-specific artifact suffixes. The final % Test restricts MUnit to the test configuration.
sbt uses Coursier to resolve dependencies, including transitive dependencies—the libraries your libraries require—and download and cache their artifacts. Compilation triggers resolution when needed, so you don't manually download JARs or run a separate install step.
Maven Central is available by default. Add another repository through resolvers only when a dependency actually requires it. For a resolution failure, check the dependency coordinates, published Scala suffix, repository access, and credentials before adding repositories at random.
Add Runnable Scala Code and a Test
Create src/main/scala/example/Main.scala with this Scala source. The @main annotation creates an entry point named example.hello.
package example
object Greeting:
def message(name: String): String = s"Hello, $name!"
@main def hello(): Unit =
println(Greeting.message("Scala"))Then create src/test/scala/example/GreetingSuite.scala. MUnit's FunSuite provides the test and assertEquals methods used here.
package example
class GreetingSuite extends munit.FunSuite:
test("greets the supplied name") {
assertEquals(Greeting.message("Scala"), "Hello, Scala!")
}You now have application code, a test framework dependency, and a discoverable test suite. Once this builds, our Scala case classes tutorial is a useful next language exercise.
Basic sbt Commands and Tasks
In everyday conversation, everything entered at the prompt is often called a command. More precisely, compile and package are tasks that perform build work, while commands such as reload manage the build session. Input tasks, including run and testOnly, also accept arguments.
sbt: Terminal Invocation Versus Interactive Shell
From the directory containing build.sbt, run a task directly:
sbt testOr start the interactive shell:
sbtInside that shell, enter task names without the sbt prefix. This transcript shows a test run, a focused suite run, and disconnecting from the shell:
sbt:sbt-intro> test
sbt:sbt-intro> testOnly example.GreetingSuite
sbt:sbt-intro> exitsbt 2.x uses a client-server workflow by default, so later terminal invocations can reuse the running build server. exit disconnects the interactive client; shutdown stops the server. For multiple operations in one terminal invocation, quote a semicolon-separated sequence rather than passing separate task arguments:
sbt "testFull; package"run: Execute an Entry Point
run compiles as needed and runs your program. If multiple main classes are available, sbt may ask you to select one; a project without a main class has nothing to run. Use runMain to select the example's entry point explicitly. In sbt 2.x's normal client workflow, the application runs in a fresh JVM.
sbt run
sbt "runMain example.hello"reload: Reload the Build Definition
reload reloads the build definition, recompiling build-support or plugin definitions as necessary. sbt 2.x automatically reloads build-source changes by default, but the explicit command remains useful when you want to reload deliberately.
compile: Compile Main Sources
compile compiles main sources from src/main/scala and src/main/java. Test / compile compiles test sources and their source dependencies without running tests.
sbt compile
sbt "Test / compile"Incremental compilation tracks dependencies between source definitions and recompiles only the sources affected by a change. Changing one file doesn't automatically mean recompiling everything, although a widely used API change can affect many files. Keeping the build server running also avoids repeatedly loading and warming up the compiler.
clean: Remove Generated Build Output
clean removes generated build products, including compiled classes and generated sources. Use it for deliberate clean-build checks or troubleshooting—not before every compile, when it discards useful local build output. It isn't a command to remove your application sources.
package: Produce the Configured Artifact
For this JVM project, package creates a JAR containing compiled main classes and main resources. It doesn't normally bundle dependency JARs into a self-contained executable. Packaging is scoped to a project and configuration, can be customized, and may produce artifacts for multiple projects through aggregation.
test: Compile and Run Tests
test compiles the required sources and runs tests through the configured framework. In sbt 2.x it selects tests incrementally and caches results: unchanged, previously successful tests may not execute again. Use testFull for a complete, uncached test run, or testOnly example.GreetingSuite to select that suite.
sbt testFull
sbt "testOnly example.GreetingSuite"During development, enter ~test in the sbt shell to repeat incremental testing when watched sources change. Press Enter to leave the watch loop.
Scopes and Multi-Project Builds
A scope identifies which project or configuration a task applies to. For example, Test / compile selects test compilation rather than main compilation. If a build contains a subproject whose ID is core, these shell expressions target it explicitly:
# Fragment: requires a subproject with the ID core.
core / Test / compile
core / test
core / packageAggregation means that invoking a task on one project also invokes it on its aggregated projects. A project dependency is different: .dependsOn(...) makes another project's code available on the dependent project's classpath. Don't assume that running a task at the root means exactly one project or one JAR.
Beyond these basics, sbt supports custom task definitions, concurrent execution of independent build work, and plugins that add settings and tasks. Introduce those features when the build needs them, rather than making a beginner project resemble a framework.
Common Mistakes
- Mixing up version pins:
sbt.versionselects sbt;scalaVersionselects your project's Scala compiler. - Using obsolete scope syntax: use
Test / compile, nottest:compile, which sbt 2.x no longer supports. - Assuming every test invocation reruns everything: choose
testFullwhen tests depend on external state that source-change detection cannot capture. - Treating package as deployment: check whether your deployment needs dependencies, launch scripts, or another packaging format.
- Changing CI environment variables without restarting sbt: a reused sbt 2.x server retains the environment from its initial session. Set shared variables at job level or shut down between sessions that require different environments.
For your first complete check, run the following from the project root. Commit the build definition, version pin, sources, and tests—not generated target/ output.
sbt "compile; testFull; package"FAQ
What is sbt used for in Scala?
sbt manages dependencies, compiles Scala and Java sources, runs tests, and packages project artifacts. It can also build pure Java projects, although its build definitions use Scala-based syntax.
What is the difference between build.sbt and project/build.properties?
build.sbt defines project settings, dependencies, and tasks. project/build.properties pins the sbt version through sbt.version, allowing the launcher to obtain that version when necessary.
How do I specify the Scala version in sbt?
Set scalaVersion := "3.9.0" in build.sbt, substituting the version your project requires. This selects your project's Scala compiler, not the sbt version or the Scala version used internally by sbt.
How do I add a dependency in sbt?
Add an entry to libraryDependencies in build.sbt. Use %% for Scala libraries published with Scala binary-version suffixes, and append % Test when a dependency is needed only by tests.
How do I run tests from the sbt shell?
Start sbt from the project root, then enter test without an sbt prefix. Use testOnly example.GreetingSuite for the example suite, or testFull in sbt 2.x for a complete, uncached test run.
Further Reading
- Installing the sbt runner — prerequisites and launcher setup.
- sbt build layout — source, resource, and build-support directories.
- Build definition basics — settings, keys, and Scala-based build syntax.
- Library dependency basics — dependency declarations and Scala artifact suffixes.
- Dependency resolution and repositories — resolvers, transitive dependencies, and resolution behavior.
- sbt compile reference — incremental compilation and compilation scopes.
- sbt test reference — incremental tests, full test runs, and framework integration.
- Getting started with MUnit — test suites, assertions, and sbt integration.
