Library Dependencies in Mill

Beside the dependencies between Mill modules, most non-trivial source projects have dependencies to other libraries.

Mill uses coursier to resolve and download dependencies. Once downloaded, they are located in the coursier specific cache locations. For more details about coursier, refer to the coursier documentation.

Dependencies in General

Mill dependencies have the form:


When working in other Java and Scala projects, you will find some synonyms, which typically all mean the same.

For example in the Maven ecosystem, the organization is called the group and the name is called the artifact. The whole tripplet is ofthe called GAV.

In Mill we use the additional term artifactId which is identical to the name when used in the normal form shown above. When a different form is used, e.g. some double-colons are used between the parts, the artifactId typically contains suffixes, but the name doesn’t.

Example for a simple Java Dependency
def ivyDeps = Agg(

Scala dependencies

Scala major releases up until version 2.13 are binary incompatible. That means, mixing dependencies of different binary platforms will result in non-working runtimes and obscure and hard to debug issues.

To easily pick only a compatible version, a convention was established to append the scala major version as a suffix to the package name.[1] E.g. to select the Scala 2.13 version of a library foo, the final artifactId will contain the additional suffix _2.13, such that the final artifactId is foo_2.13.

To always pick the right version and support cross compiling, you can omit the scala version and instead use a double colon (::) between the organization and the name, e.g. ivy"com.typesafe.akka:akka-actor_2.12:2.5.25". Your module needs to extends ScalaModule though.

If you want to use dependencies that are cross-published against the full Scala version, e.g. 2.12.12, you can use three colons (:::) between organization and name, e.g.: ivy"org.scalamacros:::paradise:2.1.1".

def ivyDeps = Agg(
  // explicit scala version suffix, NOT RECOMMENDED!

Scala 3 interoperability

Since the release of Scala 3, the binary compatibility story for Scala has changed. That means, Scala 3 dependencies can be mixed with Scala 2.13 dependencies. In fact, the Scala 3 standard library is the same as for Scala 2.13.

As Scala 3 and Scala 2.13 have different binary platforms, but their artifacts are in general compatible, this introduces new challenges.

There is currently no mechanism, that impedes to bring the same dependency twice into the classpath (one for Scala 2.13 and one for Scala 3).

Using Scala 2.13 from Scala 3

If your Scala version is a Scala 3.x, but you want to use the Scala 2.13 version of a specific library, you can use the .withDottyCompat method on that dependency.

def scalaVersion = "3.2.1"
def ivyDeps = Agg(
  ivy"com.lihaoyi::upickle:2.0.0".withDottyCompat(scalaVersion()) //1
1 This will result in a Scala 2.13 dependency com.lihaoyi::upicke_2.13:2.0.0

Do you wonder where the name "dotty" comes from?

In the early development of Scala 3, the Scala 3 compiler was called "Dotty". Later, the name was changed to Scala 3, but the compiler project itself is still named "dotty".

The dotty compiler itself is an implementation of the Dependent Object Types (DOT) calculus, which is the new basis of Scala 3. It also enhances the type system to a next level and allows features like union-types and intersection-types.

Test dependencies (there is no test scope)

One difference between Mill and other build tools like sbt or Maven is the fact, that tests are ordinary submodules on their own. For convenience, most modules already come with a pre-configured trait for a test submodule, which already inherits all dependencies of their parent module. If you need additional test dependencies, you simply add them by overriding def ivyDeps, as you would do with normal library dependencies.

When migrating a sbt project and seeing a dependency like this: "ch.qos.logback" % "logback-classic" % "1.2.3" % "test", simply add it to the test module’s ivyDeps as ordinary dependency. There is no special test scope in Mill.

object main extends JavaModule {
  object test extends Tests {
    def ivyDeps = Agg(

Compile-only dependencies (provided scope)

If you want to use a dependency only at compile time, you can declare it with the compileIvyDeps target.

def compileIvyDeps = Agg(

When Mill generated file to interact with package manager like pom.xml for Maven repositories, such compile-only dependencies are mapped to the provided scope.

Please note, that dependencies with provided scope will never be resolved transitively. Hence, the name "provided", as the target runtime needs to "provide" them, if they are needed.

Runtime dependencies

If you want to declare dependencies to be used at runtime (but not at compile time), you can use the runIvyDeps targets.

def compileIvyDeps = Agg(

It is also possible to use higher version of the same library dependencies already defined in ivyDeps, to ensure you compile against a minimal API version, but actually run with the latest available version.

Excluding transitive dependencies

You can use the .exclude method on a dependency. It accepts organization and name tuples, to be excluded. Use the special name * to match all organizations or names.

ScalaJS dependencies

Scala.js introduces an additional binary platform axis. To the already required Scala version, there comes the Scala.js version.

You can use two colons (::) between name and version to define a Scala.js dependency. Your module needs to extends ScalaJSModule to accept Scala.js dependencies.

Scala Native dependencies

Scala Native introduces an additional binary platform axis. To the already required Scala version, there comes the Scala Native version.

You can use two colons (::) between name and version to define a Scala Native dependency. Your module needs to extends ScalaNativeModule to accept Scala Native dependencies.

1. Scala 2 versions have the unusual version format: {epoch}.{major}.{minor}.