A bit of clickbait in the title. But you can’t run Scala’s hello world without downloading the spring-framework-bom. Why does a programming language depend on an application framework? What is going on here?

The observation

So you’re looking at Scala for some reason and you go through the getting started guide to install Scala.

If you don’t want to deal with the clean up. Starting a docker container and installing Scala there wouldn’t be a bad idea.

docker run -it ubuntu:25.10 bash

Then you create your hello world program:

cat >> hello.scala << EOF
//> using scala 3.7.4

@main
def hello(): Unit =
  println("Hello, World!")
EOF

Once you run it Scala starts downloading a compilation server and its dependencies. This will take a while, but this should catch your eye:

~# scala run hello.scala
Downloading compilation server 2.0.13
https://[...]/spring-framework-bom/5.3.32/spring-framework-bom-5.3.32.pom
0.0% [          ] 0B (0B / s)

What is Spring doing here? Was the compilation server written using Spring? It is not impossible…

https://[...]/javax.mail/1.6.2/javax.mail-1.6.2.pom
0.0% [          ] 0B (0B / s)

And what does a compliation server need email for? That doesn’t seem plausible anymore.

https://[...]//kafka-clients/3.9.1/kafka-clients-3.9.1.pom
0.0% [          ] 0B (0B / s)

Franz, here? Now this getting ridicilous.

So what is going on?

When we got started, we didn’t quite install Scala. We installed a whole tool-kit. Part of that tool kit is the Scala CLI and that is what was aliased to scala.

To run a Scala program the CLI has to compile it and for that it needs to install Bloop. Bloop is a compilation server and conceptually similair to the Gradle Deamon. Bloop was downloaded using Coursier. Coursier is Scalas dependency manager and also was installed as part of the tool kit. Coursier downloads to ~/.cache/coursier/v1/.

So let’s see if Bloop is in there.

~# find ~/.cache/coursier/v1/ -type f | grep bloop | grep ".pom$"
...
/[...]/bloop-cli_2.13/2.0.10/bloop-cli_2.13-2.0.10.pom
/[...]/bloop-backend_2.12/2.0.13/bloop-backend_2.12-2.0.13.pom
/[...]/bloop-frontend_2.12/2.0.13/bloop-frontend_2.12-2.0.13.pom
...

So parts of Bloop are definitly there. But does Bloop depend on Spring?
With cs resolve we ask Coursier can print Bloops dependency trees:

~# cs resolve --tree ch.epfl.scala:bloop-cli_2.13:2.0.10 | grep spring

~# cs resolve --tree ch.epfl.scala:bloop-backend_2.12:2.0.13  | grep spring

...

Well. Nothing. So Bloop doesn’t depend on Spring at runtime. But we did see that the spring-framework-bom was downloaded. So lets try to find what is referencing Spring in the cache.

~# grep -r -l spring  ~/.cache/coursier/v1/ 
...
/[...]/org/apache/logging/log4j/log4j-bom/2.23.0/log4j-bom-2.23.0.pom
/[...]/org/apache/logging/log4j/log4j/2.23.0/log4j-2.23.0.pom
...

There are a load of references here. But seeing Log4j in there is a good sign. Everything needs to writes logs. Bloop won’t be any different. But let’s double check that with Coursier.

~# cs resolve --tree ch.epfl.scala:bloop-backend_2.12:2.0.13  | grep log4j
...
   │  │  ├─ org.scala-sbt:util-logging_2.12:1.11.0
   │  │  │  │ ...
   │  │  │  ├─ org.apache.logging.log4j:log4j-core:2.17.1 -> 2.23.0 (possible incompatibility)
   │  │  │  │  └─ org.apache.logging.log4j:log4j-api:2.23.0

Bingo!

Now for the same reason I wouldn’t expect Scala to have a dependency on the Spring Framework, I would also not expect Log4 to have one. This raises the question: Why does it?

Looking at the log4j-core.xml there does not appear to be anything Spring related there. But in the parent POM, the log4j-bom we see:

  <dependencyManagement>
    <dependencies>
       ...
       <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-framework-bom</artifactId>
          <version>${spring-framework.version}</version>
          <type>pom</type>
          <scope>import</scope>
       </dependency>
       ...
    </dependency>
   ...
</dependencies>

Oh… Well, that is rather interesting.

Log4j uses Dependency managment to centralize the dependency information for the projects different modules. So when resolving the log4-core dependency, Maven has to download the log4-bom to understand its dependencies. In turn to understand the log4-bom, the imported pom dependencies from the dependency management section also have to be downloaded.

Testing the theory

Now this is all according to theory. Lets see if we can reproduce this in a minimal fashion.

I initially tried to reproduce this with Coursier, but it failed to work after deleting the ~/.cache folder. So we’re using Maven instead. The dependency resolution mechanism is essentially the same.

Starting with a fresh docker:

docker run -it maven:eclipse-temurin bash

Then we have to warm up Maven’s cache by downloading something unrelated.

mvn dependency:get -Dartifact="io.cucumber:cucumber-parent:4.6.0:pom"

And we download log4j-core:

~# mvn dependency:get -Dartifact="org.apache.logging.log4j:log4j-core:2.25.2"
...
[INFO] --- dependency:3.7.0:get (default-cli) @ standalone-pom ---
[INFO] Resolving org.apache.logging.log4j:log4j-core:jar:2.25.2 with transitive dependencies
...
Downloading from central: https://[...]/org/springframework/spring-framework-bom/5.3.39/spring-framework-bom-5.3.39.pom
Downloaded from central: https://[...]/org/springframework/spring-framework-bom/5.3.39/spring-framework-bom-5.3.39.pom (5.7 kB at 157 kB/s)
...

Theory confirmed.

Summary

Putting everything together we get this rather surprising dependency tree. Not what I would have expected at all. And I’m left wondering why Log4J was setup the way it is. But I’ve already spend way too much time down this rabit hole. So that will forever remain a mystery.

  ---
config:
    layout: elk
---
erDiagram
"scala run Hello.scala" ||..o|  "scala" : "calls"
"scala" ||..o|  "scala-cli" : "is aliased to"
"scala-cli" ||..o|  "Bloop Complilation Server" : "downloads"
"Bloop Complilation Server" ||..o| "ch.epfl.scala:bloop-backend" : "consists of"
"ch.epfl.scala:bloop-backend" ||..o| "org.scala-sbt:util-logging" : "depends on"
"org.scala-sbt:util-logging" ||..o| "org.apache.logging.log4j:log4j-core" : "depends on"
"org.apache.logging.log4j:log4j-core" ||..o| "org.apache.logging.log4j:log4j-bom" : "has as parent pom"
"org.apache.logging.log4j:log4j-bom" ||..o| "org.springframework:spring-framework-bom" : "uses in dependency management"