Starter dependencies — how they work

java dev.to

What a starter actually is

When you build a real application, you rarely need just one library. To expose a web endpoint in the Spring world, for example, you need a web framework, a JSON serializer, a validation library, an embedded server to run it all, and the glue that wires them together. Picking each one, finding versions that agree with each other, and listing them in your build file is tedious and easy to get wrong.

A starter is Spring Boot's answer to that chore. It is a single dependency you add to your build, and it pulls in a curated, version-aligned bundle of everything you need for one kind of job. Add one line, get a working stack. You meet starters the moment you generate any Spring Boot project — that spring-boot-starter-web in your pom.xml is one.

The surprising part: a starter contains almost no code of its own. Open one up and it is nearly empty. To understand how an empty jar can pull in an entire web stack, we need to look at how build tools handle dependencies.

Dependencies that bring friends

When you declare a dependency in Maven or Gradle, you are not only asking for that one jar. You are also asking for everything it needs to run.

Say your project depends on library A, and A was itself built against library B. Your build tool reads A's own metadata, sees that it requires B, and quietly downloads B too. B is a transitive dependency — a dependency you never asked for by name, pulled in because something you did ask for needs it.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

That one entry looks small. But spring-boot-starter-web declares its own dependencies — Spring MVC, the Jackson JSON library, a validation implementation, an embedded Tomcat server — and each of those drags in more. Your build tool walks the whole tree and assembles it for you.

So a starter is just a normal jar whose real value is its dependency list. It ships no classes worth mentioning; it exists so that depending on it transitively depends on the right set of libraries. That is the whole trick — a starter is a curated list of transitive dependencies dressed up as a single dependency.

Why not just list the libraries yourself?

You could. You could open Tomcat, Spring MVC, Jackson, and the rest, and add each one by hand. Two problems make that painful.

First, there are more pieces than you expect. A web stack is a dozen jars once you count the transitive ones. Nobody wants to track that list.

Second — and this is the real trap — those jars have to agree on versions. Spring MVC expects a particular version of the core Spring context. Jackson's databind module expects a matching Jackson core. Mix a new version of one with an old version of another and you get errors that surface only at runtime, often as a confusing NoSuchMethodError deep inside a library you never touched.

A starter removes the first problem by bundling the list. But it does not, by itself, solve the second. Version alignment is handled by a separate mechanism, and it is worth understanding on its own.

The bill of materials keeps versions in step

Notice what was missing from the dependency snippet above: a version number. You wrote the group and the artifact, but never said which version of spring-boot-starter-web you wanted. That is deliberate, and it is where the second piece comes in.

Spring Boot ships a bill of materials, usually shortened to BOM — a single document that lists a known-good version for every library in the Spring Boot ecosystem and its common companions. Think of it as one master version sheet that has already been tested as a set.

Your project connects to that sheet in one of two ways. The common one is inheriting from the Spring Boot parent:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
</parent>
Enter fullscreen mode Exit fullscreen mode

That parent imports the BOM. From then on, whenever you declare a dependency that the BOM knows about, you leave the version off and the BOM fills it in. You pin the Spring Boot version in exactly one place — the parent — and every library version flows from it, guaranteed to be mutually compatible.

So the two ideas work as a pair. The starter decides which libraries you get. The BOM decides which versions of them you get. Together they turn "assemble a compatible web stack" into two lines you never have to revisit.

What a starter does not do

It is easy to assume a starter also turns things on. It does not, and keeping this straight will save you real confusion later.

A starter only puts jars on your classpath — the set of libraries available to your running program. That is all. It does not configure a single object.

The turning-on is a separate Spring Boot feature called auto-configuration: at startup, Spring Boot looks at what is actually on the classpath and, for each library it recognizes, creates sensible default objects for you. See Tomcat on the classpath? It starts an embedded web server. See Jackson? It sets up JSON conversion.

The starter and auto-configuration are designed to work together, but they are different things:

  • The starter is a build-time concern. It shapes what your build tool downloads.
  • Auto-configuration is a runtime concern. It reacts to what ended up on the classpath.

The starter loads the gun; auto-configuration is what notices it is loaded and acts. You can add libraries without a starter and auto-configuration will still react to them — the starter is just the convenient way to get the right ones there.

The naming convention tells you who made it

Starters follow a naming rule precise enough to read like a label.

Official starters, maintained by the Spring team, always begin with spring-boot-starter- followed by the job: spring-boot-starter-web, spring-boot-starter-data-jpa, spring-boot-starter-security. If you see that prefix, it came from the core project.

Third-party starters — the ones a database vendor or another framework publishes — must not borrow that prefix. The convention flips: the project's own name comes first, as in acme-spring-boot-starter. This is not a style nicety. It reserves the spring-boot-starter- namespace for official artifacts, so a glance at the name tells you whether you are trusting the Spring team or a third party.

When the defaults are wrong: excluding and swapping

Because a starter is just a bundle of transitive dependencies, you are not stuck with every piece it brings. You can remove one.

The classic example: spring-boot-starter-web includes embedded Tomcat. Suppose you would rather run on Jetty. You exclude Tomcat from the starter, then add the Jetty starter alongside it:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Here the two ideas from earlier pay off. The <exclusion> prunes Tomcat out of the transitive tree, and auto-configuration — which only reacts to the classpath — now sees Jetty instead and starts that server. You changed the outcome by changing the classpath, without touching a line of application code.

This is also the escape hatch for the version trap. If some other dependency drags in a conflicting version of a library, you can exclude the unwanted copy and let the BOM-managed one win.

Rolling your own starter

Once starters click as "a curated dependency list plus, optionally, some auto-configuration," writing one is straightforward — and teams do this to standardize setup across many services.

By convention you split it into two jars. The first is the autoconfigure module: it holds the actual configuration code that creates default objects when your library is on the classpath. The second is the starter module: a near-empty jar whose only job is to depend on the autoconfigure module and on the libraries your feature needs. Consumers add the starter; they get the code and its dependencies in one move — exactly the experience the official starters give.

Splitting them lets a cautious team pull in the autoconfigure jar alone and wire things up by hand, while everyone else takes the batteries-included starter. Same code, two doors in.

The whole picture

Step back and the mechanism is simple, and it rests on plain build-tool behavior you already knew.

A starter is a nearly-empty jar that exists for its transitive dependencies — add one line, and your build tool pulls in a whole curated set of libraries. The BOM, imported through the Spring Boot parent, decides the versions of those libraries so they are guaranteed to agree, which is why you write starters without version numbers. Getting those jars onto the classpath is a build-time act; auto-configuration is the separate, runtime step that notices them and wires up defaults. And because it is all just transitive dependencies, you stay in control — exclude a piece, swap a server, or package your own starter — by editing the dependency tree, never the application logic.

That is the payoff behind that one unremarkable line in your build file: a compatible, running stack, assembled from parts you never had to name.

Source: dev.to

arrow_back Back to Tutorials