Comparison with a fat jar

The usual way to ship a single runnable jar is a fat jar (or uber jar): a tool such as Maven Shade unpacks every dependency and merges all their class files into one flat jar. It runs with java -jar, which is the appeal - but the flattening throws away exactly what Jenesis treats as a feature. This chapter sets the two approaches side by side, so you can see what a launcher jar keeps that a fat jar loses.

Both approaches explode their dependencies. The difference is where the bytes land: a fat jar merges them into one namespace; a launcher jar gives each dependency its own subfolder. Three things follow from that single choice.

module-info files collide

Every modular dependency carries a module-info.class at its jar root. Merge two of them into one flat jar and they land at the same path - module-info.class can only exist once. The shading tool's only options are to drop all but one, or to rename them into something the module system no longer reads. Either way the modules stop being modules: their descriptors are gone, so nothing at run time knows what each one requires, exports, or opens.

A launcher jar never merges. Each dependency keeps its own module-info.class inside its own modulepath/<name>/ subfolder, so every descriptor survives intact and is read back at startup.

META-INF/services must be merged by hand

Service files are the other casualty of a flat namespace. Two dependencies that each provide, say, a java.sql.Driver both ship META-INF/services/java.sql.Driver - again the same path. A naive merge keeps one file and silently drops the other's providers, so a ServiceLoader lookup that used to find both now finds one. Fat-jar tooling works around this with hand-configured transformers (Shade's ServicesResourceTransformer and friends) that concatenate the colliding files - a step you have to know to add, and one that only covers the collisions the tool was told about.

Because a launcher jar keeps each dependency in its own subfolder, no two service files share a path. Nothing is merged, nothing is dropped, and there is no transformer to configure - every provider file stays where its dependency put it, and ServiceLoader sees them all.

The module graph is lost

The deepest loss is the one you cannot patch with a transformer. Once every class sits in one flat namespace with the descriptors gone, there is no way to reconstruct a module graph at run time. A fat jar runs as one big class path: encapsulation is gone, requires edges are gone, strong module boundaries are gone. Modular libraries silently degrade to running as unnamed-module code.

A launcher jar rebuilds the graph instead. At startup it resolves the modulepath/ subfolders into a fresh ModuleLayer, so the modules come back as real named modules with their requires and exports edges enforced - the faithful equivalent of a real -p modulepath. Non-modular dependencies become the unnamed module of the same loader, the analogue of -cp classpath.

Side by side

The two jars run the same way - java -jar app.jar - but rebuild very different worlds:

Fat jar (flat merge) Launcher jar (subfolders)
Dependency layout merged into one namespace each in its own classpath/ or modulepath/ subfolder
module-info.class collides - all but one dropped or renamed kept, one per module subfolder
META-INF/services collides - needs a merge transformer kept, no merge needed
Module graph at run time gone; everything is one class path reconstructed into a real ModuleLayer
Class reads from a rewritten flat jar on demand, straight from the outer jar
A launcher jar is not a fat jar with the rough edges filed off - it is a different reconstruction. Where a fat jar destroys the module system to fit one namespace, the launcher preserves each dependency whole and rebuilds, in process, exactly what java -p modulepath -cp classpath -m module/main would have done. See How it works for that reconstruction in detail.

Choosing a launcher jar is therefore the right call precisely when your application - or any library it depends on - is modular and you want it to stay modular in the shipped artifact. If nothing you bundle is modular, a fat jar and a launcher jar behave alike; the launcher only starts to matter once a real module graph is worth keeping.