1. The role of bootJar
To be honest, “the code exists” is not even half the success. Code is like ingredients in the kitchen: flour, eggs, milk. Until you combine them into something whole, it is not a “pancake,” but a “set of prospects.” The job of bootJar is to turn the catalog-service project into a portable artifact that you can hand to someone else or run on another machine without explaining, “now open IntelliJ, click the green triangle, and do not touch anything, it will work by itself...”.
In the Spring Boot world, this artifact is most often an executable jar. That is, a file that is launched with the standard Java command:
# Run the assembled Boot jar (no IDE and no manual classpath)
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar
And this is where the magic starts... or rather, ends. Because java -jar is the most honest test possible: the JVM knows nothing about your IDE, about a “convenient classpath,” or about “I added env vars in Run Configuration.” The JVM sees only one file and tries to run it as a program.
For such a launch to be possible, the jar must contain:
1) your classes and resources,
2) dependencies (all libraries needed at runtime),
3) an entry point for the JVM (main class),
4) a mechanism that assembles all of this into a correct classpath.
A regular “java-jar with classes” from Gradle (jar) usually handles only items 1 and 3. And bootJar handles all four.
2. jar and bootJar: the difference
It is very tempting to think like this: “Well, jar is a jar, bootJar is a jar... so they are almost the same.” That is like saying, “A milk carton and a cement bag are both in bags, so they are similar.” Formally, yes, both are zip-like; by purpose, they are completely different.
In simplified terms, the difference is this: a regular jar is a library or an “archive with classes”, while bootJar is a “self-contained application in one file”.
Let us put this into a table to make it easier to keep in mind:
| Question | Regular jar (Gradle task ) |
Boot jar (Gradle task ) |
|---|---|---|
| Purpose | “Package my classes” (often as a library) | “Assemble a ready-to-run application” |
| Dependencies inside | Usually no | Yes, they are packed inside in a special structure |
| Running with java -jar | Sometimes possible, but often pointless / does not work without a classpath | This is the primary scenario |
| Entry point | Your Main-Class (if you configured it) | Main-Class points to the Boot Launcher, and your class is specified as Start-Class |
| What needs to be kept nearby | You often need to assemble the classpath separately | Nothing, just one file |
Important: in a Boot project, having bootJar is not a “nice-to-have feature,” but part of the philosophy. A Boot service should run as an independent process. That is why you focus on bootJar by default.
3. The bootJar artifact: where to find it
The move to artifact-oriented thinking usually trips up beginners in a very funny place: a person runs java -jar, but... runs the wrong jar. Sometimes yesterday's. Sometimes from a different folder. Sometimes not a Boot jar at all.
Let us agree on one simple but important habit: after the build, we first find the artifact, and only then run it.
Build and file verification usually look like this:
# clean — so we do not confuse the new artifact with the old one in build/
./gradlew clean bootJar
# build/libs — the standard place where Gradle puts jar files
ls -lh build/libs
The comment here is more important than the command. clean clears the build/ folder so that you do not confuse the new artifact with the old one. And build/libs is the standard place where Gradle places jar files.
The typical artifact name in a training project will include the name and version:
catalog-service-0.0.1-SNAPSHOT.jar
SNAPSHOT means “a version under development.” This is not an error, just a sign that you are not doing a release. But there is a practical consequence: the file name can change if you change version in build.gradle.kts, and all your launch commands must remain up to date.
If you want stability (especially for a training project), we will show a little later how to fix the name in the bootJar configuration.
4. The structure of a Boot jar
From the outside, catalog-service-0.0.1-SNAPSHOT.jar looks like a single file. Inside, it is a rather neatly organized “suitcase with compartments.” And to stop being afraid of bootJar, it is enough to see those compartments once.
A jar is essentially a zip archive. That means it can be “looked into” with the jar tf command:
# Quickly inspect what is actually inside the jar (the first lines of the list)
jar tf build/libs/catalog-service-0.0.1-SNAPSHOT.jar | head
The output will be large, but you do not need to read everything. You need to learn to recognize the main “anchor” directories:
- BOOT-INF/classes/ — your compiled classes and resources.
- BOOT-INF/lib/ — dependencies (Spring, Jackson, Logback, and everything else).
- org/springframework/boot/loader/ — the Boot Loader (the built-in launcher).
- META-INF/MANIFEST.MF — the manifest that tells it what to run.
If we sketch the structure very roughly, it looks like this:
catalog-service-0.0.1-SNAPSHOT.jar
├─ META-INF/
│ └─ MANIFEST.MF
├─ org/springframework/boot/loader/...
└─ BOOT-INF/
├─ classes/
│ ├─ com/example/catalogservice/...
│ ├─ application.yaml
│ ├─ catalog-data.yaml
│ └─ static/index.html
└─ lib/
├─ spring-boot-...
├─ spring-webmvc-...
├─ jackson-...
└─ ...
At this point it is useful to take a micro-pause and realize: a Boot jar contains both your code and your dependencies. That is why it can be handed over as a single file. It does not need jar files placed next to it, as often happens with a regular Java application where you manually assemble the classpath.
### A small diagram of how this relates to launch
To reinforce this, here is a simple diagram (without internal details) of what happens during startup:
flowchart TD
A["java -jar catalog-service.jar"] --> B["Reading MANIFEST.MF"]
B --> C["Launching Boot Launcher"]
C --> D["Launcher assembles classpath from BOOT-INF/classes and BOOT-INF/lib"]
D --> E["Launching your Start-Class: CatalogServiceApplication"]
E --> F["SpringApplication.run(...) and service startup"]
If you keep this picture in your head, it becomes much easier to answer questions like “why does it start as a single file.”
5. MANIFEST.MF: Main-Class and Start-Class
From the JVM's perspective, java -jar is a very simple command. It opens the jar, reads META-INF/MANIFEST.MF, and looks for Main-Class there. Then it calls public static void main(String[] args) of that class. There is no Spring magic at this stage yet.
In a Boot jar, Main-Class usually points not to your class, but to the Spring Boot launcher class. Your class is recorded as Start-Class. It is like “the main host of the launch” and “the guest who speaks after the introduction.”
You can inspect the manifest like this:
# Extract and print the manifest: this is the most direct way to see Main-Class/Start-Class
jar xf build/libs/catalog-service-0.0.1-SNAPSHOT.jar META-INF/MANIFEST.MF
cat META-INF/MANIFEST.MF
There will be many lines there, but the two we care about are these (I will show an approximate form, without trying to guess every header perfectly):
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.example.catalogservice.CatalogServiceApplication
And this is the key to understanding it: java -jar launches JarLauncher, and that class then launches your CatalogServiceApplication from inside itself.
To connect this with what you write in code, let me remind you: your main class looks something like this:
package com.example.catalogservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
// Main application configuration + Spring Boot entry point
@SpringBootApplication
public class CatalogServiceApplication {
public static void main(String[] args) {
// Start the Spring context and bring up the application as a service
SpringApplication.run(CatalogServiceApplication.class, args);
}
}
So your main() does not disappear. JVM just reaches it through the launcher layer. That is the price of the convenience of “one file starts itself and carries dependencies inside.”
6. Checking the contents of a Boot jar
Checking the contents of a jar is not paranoia and not “something for DevOps.” It is a completely normal engineering skill at junior level: you do not trust “it seems to be there,” you check “it is definitely there.”
The most useful commands are simple and short.
View the file list
# Look for the anchor directories of the Boot jar: BOOT-INF/classes and BOOT-INF/lib
jar tf build/libs/catalog-service-0.0.1-SNAPSHOT.jar | grep BOOT-INF | head
Even if you are not confident with grep yet, you can simply do this:
# Just print the first lines so you can see the archive structure with your eyes
jar tf build/libs/catalog-service-0.0.1-SNAPSHOT.jar | head -n 40
And see BOOT-INF/classes and BOOT-INF/lib with your own eyes.
Check that your resources made it inside
For example, catalog-service is expected to carry application.yaml, catalog-data.yaml, and static/index.html (landing page). You can check it like this:
# Verify that the resources are actually packaged (otherwise they do not "exist" at runtime)
jar tf build/libs/catalog-service-0.0.1-SNAPSHOT.jar | grep -E "application.yaml|catalog-data.yaml|static/index.html"
This is not about “beauty.” It is about practice: if a resource is not in the jar, then it does not exist for the packaged application. In the project, it may live in src/main/resources, but at runtime only what is packaged will be “seen.”
Unpack the jar into a folder if you want to “touch it with your hands”
A jar is a zip, so it can be unpacked:
# Unpack the jar into a separate folder so you can browse BOOT-INF manually
mkdir -p build/jar-view
cd build/jar-view
jar xf ../libs/catalog-service-0.0.1-SNAPSHOT.jar
After that, you can go into BOOT-INF/classes and really see the structure of packages and resources. This sometimes helps beginners faster than abstract explanations.
A small programmatic “beacon”: where the code is loaded from
This example is convenient because it works the same in the IDE, in bootRun, and in java -jar. It shows where your code is actually loaded from: from a folder with .class files or from a jar.
Let us add a small ApplicationRunner (you can place it in your StartupConfiguration, which we already had in the project):
package com.example.catalogservice.config;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.URL;
@Configuration
public class StartupConfiguration {
@Bean
ApplicationRunner codeSourceReporter() {
return args -> {
// Determine where this class is actually loaded from (IDE/bootRun or jar file)
URL location = StartupConfiguration.class
.getProtectionDomain()
.getCodeSource()
.getLocation();
System.out.println("Code source location = " + location);
// IDE/bootRun: file:/.../build/classes/java/main/
// java -jar: file:/.../build/libs/catalog-service-0.0.1-SNAPSHOT.jar
};
}
}
Yes, this is System.out.println, and in a “real” service a logger is better. But here we use it as a training flashlight: you literally see whether you are living “as a project” or “as an artifact.”
Optional: fix the artifact name in build.gradle.kts
For today, this is an optional step. The basic scenario already works: Gradle builds a versioned jar in build/libs, and that is enough to run the service as an artifact. But if you want to simplify the launch commands and the README, you can fix the file name separately.
When you start running the service as a file, the thing that used to be a “small detail” suddenly becomes important: the artifact name. Today you run catalog-service-0.0.1-SNAPSHOT.jar, tomorrow — catalog-service-0.0.2-SNAPSHOT.jar. And every time you need to remember the exact name, copy it, make a mistake in one digit, and feel like a person typing a password in the dark.
You do not need complicated infrastructure for this. It is enough to agree with Gradle once on how the final jar should be named and keep that name stable.
The simplest option is to configure bootJar:
tasks.named<org.springframework.boot.gradle.tasks.bundling.BootJar>("bootJar") {
// Fix the artifact name so the launch command does not depend on the project version
archiveFileName.set("catalog-service.jar")
}
After that, you will have an artifact with a fixed name in build/libs. If you intentionally choose this option, the launch command becomes shorter:
java -jar build/libs/catalog-service.jar
If you do not make this setting, nothing breaks: later in the day you can safely use the versioned jar path and not treat catalog-service.jar as a new mandatory baseline.
If you see that you have multiple main() methods in the project (this happens when someone adds a second class with public static void main “for experimentation”), Gradle may not guess the correct entry point. Then you can explicitly specify the main class in the Spring Boot Gradle plugin configuration (I am showing it as an idea “if needed,” not as a mandatory step):
springBoot {
// Explicitly specify the main application class if auto-detection gives the wrong result
mainClass.set("com.example.catalogservice.CatalogServiceApplication")
}
The main idea: the artifact must be predictable. Because that is what you run, check, hand to another developer, and use as proof that the service exists outside the IDE.
7. Typical mistakes when working with bootJar
Mistake #1: confusing “I built a jar” with “I built a Boot jar.”
A beginner often runs ./gradlew jar, sees a file in build/libs, and happily does java -jar .... Then there are two scenarios: either the application does not start at all, or it starts “somehow strangely” (without dependencies). The fix is simple: for a Spring Boot application, focus on bootJar, because that is what assembles the executable artifact with the launcher and dependencies.
Mistake #2: not running clean and launching yesterday's artifact.
This is a classic: you changed the code, ran java -jar, and “the changes did not apply.” And then the shamanism begins: “Is Spring caching? Is Gradle caching? Is Java being picky?” Usually the reason is simpler: you launched an old jar or did not rebuild the new one. The habit of ./gradlew clean bootJar before an important check saves hours of life.
Mistake #3: looking for the jar in src/ or in the IDE folder.
If you have just “left the IDE,” your brain will naturally look for the final file near the source files. But Gradle is not an archiver “in the project folder”; it builds into build/. The final artifact is conventionally located in build/libs. As soon as you stop looking for it “by hand” and start remembering this place, things become calmer.
Mistake #4: not checking the jar contents and hoping for luck.
If a resource, such as catalog-data.yaml or static/index.html, did not make it into the artifact, the application may behave unexpectedly. The worst part is that you will fix the code while the problem is actually packaging. jar tf and grep are a quick way to confirm that “the file is really inside.” And this is exactly the case where one command replaces ten guesses.
Mistake #5: changing the artifact name and forgetting that the launch command depends on the name.
You configured archiveFileName, it became convenient — great. But if you then change the name again and an old one remains somewhere in notes, the terminal, or documentation, you will again be launching the wrong thing. In packaging stories, one rule always works: the build script and launch commands must be consistent. Otherwise the artifact exists, but as if it does not.
GO TO FULL VERSION