What are JAR and WAR — and Why Package at All?
A compiled Java application is, physically, hundreds of separate
.class files plus resources plus configuration — none of
that is anything you'd want to copy file-by-file to a server or hand to
a container runtime. A JAR (Java Archive) is a ZIP file
with a Java-specific structure and metadata that lets the JVM run code
directly from it; a WAR (Web Application Archive) is a
JAR with an additional, standardized directory layout that a servlet
container (covered on the previous two pages) knows how to unpack and
deploy as a running web application.
// BEFORE — deploying an application as loose files
$ scp -r target/classes/ user@server:/opt/myapp/
$ scp -r target/dependency/ user@server:/opt/myapp/lib/
$ ssh user@server "cd /opt/myapp && java -cp classes:lib/* com.shop.Main"
// Every file transferred individually, no single artifact to version, no
// manifest describing what this even is or how to run it correctly
// AFTER — one artifact, self-describing, single command to run
$ scp target/myapp.jar user@server:/opt/myapp/
$ ssh user@server "java -jar /opt/myapp/myapp.jar"
// One file. The manifest inside it already declares the entry point —
// java -jar reads META-INF/MANIFEST.MF to find Main-Class automatically.
# A JAR/WAR is a real ZIP file underneath — this always works:
unzip myapp.jar -d extracted/
jar -tf myapp.jar # list contents
jar -xf myapp.jar # extract contents
JAR Files — Structure and the Manifest
myapp.jar
├── META-INF/
│ └── MANIFEST.MF # metadata: entry point, classpath, version
├── com/shop/
│ ├── Main.class
│ ├── ProductService.class
│ └── util/Helper.class
├── application.properties
└── static/logo.png
# META-INF/MANIFEST.MF
Manifest-Version: 1.0
Main-Class: com.shop.Main
Class-Path: lib/gson-2.9.0.jar lib/commons-lang3-3.12.0.jar
Implementation-Title: Shop Catalog Service
Implementation-Version: 1.0.0
| Type | Description | How to run |
|---|---|---|
| Library JAR | Reusable code, no Main-Class | Add to another project's classpath |
| Executable JAR | Has Main-Class in the manifest | java -jar app.jar |
| Fat/Uber JAR | Bundles dependencies too — covered in detail below, since "bundled" means something different depending on the tool | java -jar app.jar |
Fat JARs — Two Genuinely Different Mechanisms Behind the Same Word
"Fat JAR" gets used as if it describes one technique. It doesn't — the two common approaches produce structurally different artifacts, and confusing them is a real source of production bugs, not just terminology pedantry.
Maven Shade — flattens everything into one namespace
<!-- pom.xml -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
</execution>
</executions>
</plugin>
// Every dependency's .class files are extracted and merged directly into
// ONE flat package namespace inside the final JAR — as if you'd manually
// unzipped every dependency JAR into the same directory.
When two dependencies both ship a file at the same path inside
META-INF/ — a common occurrence with SPI registration
files, or with Spring's own META-INF/spring.factories /
META-INF/spring/*.imports auto-configuration
registrations — a naive shade merge keeps only one of them,
silently. The symptom is a dependency that works perfectly in
isolation but mysteriously stops auto-configuring itself once shaded
alongside others. Maven Shade supports transformers
(ServicesResourceTransformer and similar) specifically
to merge rather than overwrite these files correctly — but only if
you configure them; the default behavior silently picks one.
Spring Boot repackage — nested JARs, not a flattened merge
<!-- pom.xml — this is what spring-boot-starter-parent wires up by default -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals><goal>repackage</goal></goals>
</execution>
</executions>
</plugin>
// The resulting structure is deliberately NOT flattened:
myapp.jar
├── org/springframework/boot/loader/ # Spring Boot's own bootstrap classloader
├── BOOT-INF/classes/ # YOUR compiled classes, untouched
└── BOOT-INF/lib/ # every dependency, as WHOLE, UNMODIFIED jars
├── spring-web-6.1.0.jar
├── gson-2.9.0.jar
└── ...
Because every dependency stays intact as its own nested JAR rather
than being merged into one namespace, the
META-INF/spring.factories collision problem described
above simply doesn't arise — Spring Boot's own
JarLauncher (declared as Main-Class in the
outer manifest) sets up a classloader that reads each nested JAR
independently at startup. The trade-off is that this JAR can no
longer be added directly to another project's classpath the way a
shaded JAR can — its layout is specific to being launched via
java -jar through Spring Boot's own loader, not treated
as a generic library dependency.
WAR Files — Structure and the WEB-INF Boundary
mywebapp.war
├── META-INF/
│ └── MANIFEST.MF
├── WEB-INF/ # PROTECTED — never served directly over HTTP
│ ├── web.xml # deployment descriptor
│ ├── classes/com/shop/ProductServlet.class
│ └── lib/gson-2.9.0.jar, mysql-connector-8.0.jar
├── index.html # PUBLIC — /mywebapp/index.html
└── css/style.css # PUBLIC — /mywebapp/css/style.css
# Reachable directly:
http://localhost:8080/mywebapp/index.html ✓
http://localhost:8080/mywebapp/css/style.css ✓
# NEVER reachable directly — the container itself refuses to serve it:
http://localhost:8080/mywebapp/WEB-INF/web.xml ✗ 404
http://localhost:8080/mywebapp/WEB-INF/classes/ ✗ 404
# This is enforced by the servlet specification itself, not a configurable
# option — every compliant container treats WEB-INF/ as off-limits to direct
# requests. That's precisely why compiled code, configuration with database
# credentials, and dependency JARs all belong inside it.
JSP templates that should only ever be reached by being forwarded to
from a servlet — never requested directly by URL — are conventionally
placed inside WEB-INF/ (e.g.
WEB-INF/views/product.jsp) for exactly this reason: the
container's own protection guarantees no client can bypass your
controller logic and hit the template directly.
<!-- pom.xml -->
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope> <!-- the container supplies this — never bundle it -->
</dependency>
</dependencies>
JAR vs WAR — Side by Side
| Aspect | JAR | WAR |
|---|---|---|
| Full name | Java Archive | Web Application Archive |
| Purpose | General Java apps and libraries | Web applications deployed to a servlet container |
| Structure | META-INF/ + classes + resources | WEB-INF/ (protected) + public resources |
| Run/deploy | java -jar, or added to another project's classpath | Deployed into a servlet container, which extracts and manages it |
| Dependencies | External classpath, or bundled (fat JAR — two different mechanisms, see above) | Always in WEB-INF/lib/ |
| Hot deployment | N/A — restart the process | Supported by the container without restarting the whole server |
Spring Boot: JAR or WAR?
JAR — the default, recommended path
<packaging>jar</packaging>
# java -jar myapp-1.0.0.jar — embedded Tomcat included, no external
# container to install or configure, natural fit for a Docker image
WAR — only when an external container is a hard requirement
<packaging>war</packaging>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope> <!-- the target container supplies its own Tomcat -->
</dependency>
@SpringBootApplication
public class ShopApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder app) {
return app.sources(ShopApplication.class);
// This bridge is what lets the SAME application also start correctly
// when deployed as a WAR to an external Tomcat, which calls this
// instead of your own main() method.
}
}
| Use case | Recommended | Reason |
|---|---|---|
| New Spring Boot application | JAR | Simpler, self-contained, no separate installation step |
| Cloud/container deployment | JAR | One process per container is the natural Docker/Kubernetes fit |
| Corporate standard requiring WildFly/WebLogic | WAR | Existing infrastructure investment, not a technical requirement of Spring Boot itself |
| Reusable library | JAR | Not a deployable web application at all |
Production Reality — Layered JARs and Docker Build Caching
Copying one monolithic fat JAR into a Docker image works, but it wastes the layer cache: any code change — even a single line — invalidates the entire image layer containing the JAR, forcing Docker to re-push every dependency on every deploy, even though your dependencies almost never change between builds. Since Spring Boot 2.3, the executable JAR supports being split into layers along exactly this boundary.
# Extract the JAR into its constituent layers
java -Djarmode=layertools -jar myapp.jar extract
# Produces (typically): dependencies/, spring-boot-loader/,
# snapshot-dependencies/, application/ — ordered from LEAST to MOST
# frequently changing
# Multi-stage Dockerfile using the extracted layers
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY target/myapp.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
FROM eclipse-temurin:21-jre
WORKDIR /app
# Copied in order: layers that rarely change first, so Docker can reuse
# these cached layers across builds where only your own code changed
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
# Check your Spring Boot version's own documentation for the exact
# launcher class name — it moved packages between major versions, and
# getting this wrong is a common source of a container that builds fine
# but fails immediately on "docker run"
A typical deploy changes application/ (your own compiled
code) far more often than dependencies/ (third-party
libraries, which only change when you bump a version). Copying the
rarely-changing layers first means Docker's build cache serves them
from cache on almost every routine deploy, and only the small
application/ layer actually needs to be rebuilt and
re-pushed — a meaningfully faster and lighter CI/CD pipeline than
shipping one undivided JAR as a single layer every time.
EAR Files — Enterprise Archive, for Completeness
enterprise-app.ear
├── META-INF/application.xml # declares which modules this EAR bundles
├── web-module.war
├── ejb-module.jar
└── lib/shared-utils.jar
An EAR bundles multiple WARs and JARs — typically a web tier plus an EJB tier — into one deployable unit for a full Jakarta EE application server. It was common in the Java EE era of monolithic enterprise deployments; today's equivalent goal (multiple coordinated modules deployed together) is far more often achieved with independent microservices, each its own JAR or container image, than with a single EAR bundling everything into one deployment unit.
Common Issues
"no main manifest attribute"
$ java -jar myapp.jar
no main manifest attribute, in myapp.jar
// Cause: the manifest has no Main-Class entry. Fix in pom.xml:
<manifest>
<mainClass>com.shop.Main</mainClass>
</manifest>
ClassNotFoundException in what should be a fat JAR
# Verify what actually got bundled:
jar -tf myapp.jar | grep gson
# Nothing printed → dependencies were never included. Check that the
# repackage/shade execution actually ran during the build (mvn package,
# not mvn compile), and that the plugin is bound to the "package" phase.
javax vs jakarta package mismatch
java.lang.NoClassDefFoundError: javax/servlet/Servlet
// A javax.servlet-based WAR deployed to Tomcat 10+. Full explanation of
// this migration is on Servlet Containers and Tomcat Overview — the fix
// here is updating every affected import and dependency to jakarta.*.
Best Practices and Common Pitfalls
✅ Do
- Use
spring-boot-maven-plugin'srepackagegoal for Spring Boot applications — it's already wired up byspring-boot-starter-parentand avoids the resource-collision risk of a shaded jar - Configure Maven Shade's resource transformers explicitly (e.g.
ServicesResourceTransformer) whenever shading a project that includes SPI-registering dependencies - Place JSP templates and anything not meant to be requested directly under
WEB-INF/, relying on the container's own protection rather than application-level checks - Use layered JAR extraction in multi-stage Dockerfiles to keep CI/CD builds fast and image layers cache-friendly
- Mark the servlet API dependency as
providedin a WAR project — the container supplies it; bundling your own copy risks a version conflict with the container's own classes
❌ Don't
- Don't assume "fat JAR" means one specific mechanism — a Maven Shade jar and a Spring Boot repackaged jar have fundamentally different internal structures and different failure modes
- Don't shade a Spring Boot project with Maven Shade instead of the Boot plugin's own repackage goal — you'll very likely lose auto-configuration registrations to silent resource-file collisions
- Don't copy a single monolithic fat JAR into a Docker image as one layer — you lose the build-cache benefit that layered extraction was specifically built to provide
- Don't try to protect a JSP or config file with only application-level logic when placing it under
WEB-INF/gives you a specification-guaranteed 404 for free
Interview Questions
Q: What is the difference between a JAR and a WAR file?
Both are Java archive formats (ZIP files with extra structure and
metadata), but a JAR is general-purpose — libraries or standalone
executable applications — while a WAR has an additional, standardized
structure (WEB-INF/) specifically so a servlet container
knows how to deploy it as a web application.
Q: Why can't a client request WEB-INF/web.xml directly by URL?
The Servlet specification itself requires every compliant container to
treat WEB-INF/ as off-limits to direct HTTP requests. This
protects configuration files, compiled classes, and dependency JARs from
ever being served as plain content, regardless of how the application
itself is written.
Q: What does the manifest's Main-Class entry do?
It tells java -jar which class contains the
public static void main method to execute. Without it,
running java -jar on that archive fails with "no main
manifest attribute."
Q: A team shades a Spring Boot application with maven-shade-plugin instead of using spring-boot-maven-plugin, and several auto-configured starters silently stop working. Explain the mechanism.
Spring Boot's auto-configuration relies on registration files under
META-INF/ (such as spring.factories or the
newer META-INF/spring/*.imports files) — and multiple
starter dependencies each ship their own copy of these files at the
exact same path. Maven Shade's default merge behavior, when it encounters
two files at an identical path across different source JARs, keeps only
one and silently discards the rest — there's no build failure, no
warning, just missing auto-configuration for whichever starters lost
that collision. Spring Boot's own repackage mechanism avoids this
entirely by never merging dependency contents in the first place — each
dependency stays a whole, separate JAR nested under
BOOT-INF/lib/, so there's no shared namespace for these
registration files to collide in at all.
Q: Why does Spring Boot's executable JAR need its own custom classloader (JarLauncher) instead of relying on the standard JVM classpath mechanism?
The standard JVM classpath and the Class-Path manifest
attribute both expect referenced JARs to exist as separate files on the
filesystem or be flatly present in the same archive — neither mechanism
understands "a JAR nested inside another JAR" as a valid classpath entry.
Since Spring Boot's structure deliberately keeps every dependency as an
intact, separate JAR under BOOT-INF/lib/ rather than
flattening them, something has to teach the JVM how to load classes from
those nested archives at runtime — that's exactly what
org.springframework.boot.loader.launch.JarLauncher
(declared as the outer Main-Class) does: it constructs a
custom ClassLoader capable of reading class bytes directly
out of each nested JAR, before ever handing control to your own
application's actual main method.
Q: A Docker image build takes just as long on every deploy even though only application code changed, not dependencies. The Dockerfile copies one fat JAR with a single COPY instruction. What's the fix, and why does instruction order matter?
Docker caches image layers and only rebuilds a layer (and everything
after it) when its own inputs change. A single COPY of one
monolithic JAR means the entire layer — dependencies included — is
invalidated by even a one-line code change, since Docker has no way to
see "inside" the JAR to know only a small part of it actually changed.
The fix is extracting the JAR into Spring Boot's layered structure
(java -Djarmode=layertools -jar app.jar extract) and
copying each resulting layer into the image with separate
COPY instructions, ordered from least to most frequently
changing — dependencies first, your own compiled application last. Docker
then only needs to rebuild and re-push the small, fast-changing
application layer on a typical deploy, reusing its cache for every layer
above it that didn't change.