Home
Jan 20, 2026

First Impressions of Mill After Years of sbt

scalamillbuild-tools

I’ve been writing Scala professionally for a while now, and like everyone else I’ve mostly accepted sbt as the price of admission. It works, but I’ve never been able to predict it, settings vs tasks, scopes, the := vs += vs ++= dance, and error messages that assume you’ve read the internals.

For a side project I’m starting (more on that in a future article), I decided to finally try Mill. First impressions after two weekends.

A Build Is Just Objects and Methods

This is the core idea, and it clicked immediately. In Mill, a module is an object, and a task is a def:

1object backend extends ScalaModule {
2  def scalaVersion = "3.6.2"
3
4  def mvnDeps = Seq(
5    mvn"com.lihaoyi::cask:0.10.2",
6    mvn"com.lihaoyi::upickle:4.1.0"
7  )
8}

There’s no separate concept of “settings” living in some scope matrix. If you want to know what backend.compile depends on, you follow the method calls, like in any normal Scala code. Overriding behaviour is literally just override def.

Caching That Makes Sense

Every task in Mill is treated like a pure function: inputs → output. The output goes into the out/ folder, and if the inputs didn’t change, the task doesn’t re-run. That’s the whole model.

1out/backend/compile.json
2out/backend/compile.dest/

You can cat the metadata of any task and see exactly what it produced and from what. After years of sbt’s opaque incremental state (and the ritual sbt clean when things get weird), being able to just look at the cache feels almost unfair.

Small Things That Made Me Smile

  • mill __.test, run every test task in every module. The __ wildcard works for everything.
  • mill show backend.mvnDeps, print what any task evaluates to, as JSON.
  • mill -w backend.compile, watch mode built in, no plugin.
  • Startup is fast, and there’s no project/ folder with a meta-build inside a meta-build.

What I’m Not Sure About Yet

The ecosystem is obviously smaller than sbt’s. Some plugins I take for granted have no Mill equivalent, and you end up writing a bit of build code yourself, although honestly, writing build logic in plain Scala is exactly what Mill makes pleasant.

The real test will be what I actually picked Mill for: a full-stack Scala app with a Scala.js frontend and a shared module between them. Mill’s cross-compilation story looks much cleaner than sbt’s crossProject, that will be its own article once it works.