🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
// DOC-DERIVED — container names and hashes vary; sentence shape is stable across JDK 8-21:
Exception in thread "main" java.lang.LinkageError: loader constraint violation: when resolving method "com.devinhyderabad.api.DaoFactory.create()Lcom/devinhyderabad/api/Dao;" the class loader "app" of the current class, com/devinhyderabad/web/ReportServlet, and the class loader org.springframework.boot.loader.LaunchedURLClassLoader @5e2de80c for the method's defining class, com/devinhyderabad/api/DaoFactory, have different Class objects for the type com/devinhyderabad/api/Dao used in the signature
at com.devinhyderabad.web.ReportServlet.doGet(ReportServlet.java:24)
at com.devinhyderabad.app.Main.main(Main.java:7)⚡ Quick Fix Works 80% of the time
Ship the shared API jar exactly once, loaded by the common parent — remove it from inside the war (WEB-INF/lib) or from BOOT-INF/lib duplicates so both sides delegate to the same loaded copy.
<dependency>
<groupId>com.devinhyderabad</groupId>
<artifactId>api</artifactId>
<version>1.8.0</version>
<!-- container/app already provides it - do not bundle again -->
<scope>provided</scope>
</dependency>🧠 Why this Happens
Tap to expand the deep technical explanation
In the JVM a type is identified by its fully-qualified name AND its defining classloader; Dao from loader A and Dao from loader B are unrelated types that happen to share text. When ReportServlet (loaded by app) calls DaoFactory.create() (loaded by the boot loader), the JVM must check the returned Dao against the signature it resolved. The two loaders each resolved their own Dao, so the runtime types differ and the signature match fails structurally — hence LinkageError rather than ClassCastException, which is what you get further along when identities are compared by instanceof.
The HITEC City Parking Spot Analogy:
Two branches of the same company each print their own staff badges. At headquarters the door reader rejects the branch badge — same photo, same name, wrong issuing system.
🔁 How to Reproduce Confirm this is your error
Put an API interface in a parent-loader-visible location and ALSO inside a webapp archive. Load the factory through the child loader and the caller through the parent, then pass the produced object into the typed signature. DOC-DERIVED — exact loader names depend on your container.
🛠️ Solutions (5 Ways to Fix)
Load shared API classes once via the common parent
👉 Use this whenever application code and libraries exchange types across a container boundary (Tomcat shared libs, Boot nested jars, OSGi bundles).
Mark shared contracts provided-scope in the webapp and install them once at the container level (Tomcat lib/, Boot external classpath). Parent delegation then guarantees one defining loader for Dao everywhere.
# Tomcat layout - api.jar lives ONCE here:
$CATALINA_HOME/lib/api-1.8.0.jar
# and in pom.xml of every war:
<dependency>
<groupId>com.devinhyderabad</groupId>
<artifactId>api</artifactId>
<version>1.8.0</version>
<scope>provided</scope>
</dependency>Audit the deployment for duplicate copies of the same jar
👉 Use this after any dependency reshuffle to prove no artifact exists twice.
List every jar in the deployed tree, hash them, and flag identical names or identical contents appearing more than once. Duplicates at different levels are the raw material for loader splits.
find . -name "*.jar" | xargs sha1sum | sort | uniq -w40 -D
# Gradle view of who pulls the duplicate
./gradlew dependencies --configuration runtimeClasspathKeep delegation parent-first for contract jars
👉 Use this if you run custom or child-first classloading (Spring Boot loader, some app servers, plugin systems).
Child-first loading maximizes isolation but breaks shared-type identity. Exclude only genuinely conflicting libraries from delegation and let stable API packages always resolve from the parent.
# Spring Boot PropertiesLauncher - parent-first for chosen archives
loader.classpathIndexFile=classpath.idx # keep api.jar OUT of BOOT-INF/lib
# or classic Tomcat context.xml:
<Loader delegate="true" />Exchange data through primitives, maps, or serialization at the boundary
👉 Use this when plugin architectures cannot share a parent loader by design.
Where isolation is the point (scripting plugins, hot-reload), never pass rich typed objects across. Convert to String/byte[]/Map payloads at the border so neither side resolves the other’s classes.
// plugin returns plain data, host interprets
Map<String, Object> result = plugin.execute(Map.of("query", q));
int total = ((Number) result.get("total")).intValue();Diagnose with -verbose:class before restructuring
👉 Use this to confirm which loader defined each copy before moving jars around.
The verbose log prints [Loaded com.devinhyderabad.api.Dao from jrt:... / file:.../WEB-INF/lib/api-1.7.0.jar ...] lines naming the defining source. Two loads with different sources = confirmed split.
java -verbose:class -jar app.jar 2>&1 | grep "com.devinhyderabad.api.Dao"
# count definitions - two lines with different sources = confirmed split
java -verbose:class -jar app.jar 2>&1 | grep -c "Loaded.*com.devinhyderabad.api.Dao"📋 Version Notes
Same semantics under ExtClassLoader/AppClassLoader hierarchies.
Boot loader renamed platform/system split; message wording identical.
Named loaders print as "app"/"platform"; Spring Boot LaunchedURLClassLoader common offender.
Unchanged; message gained no new fields.
🛡️ How to Prevent This Next Time
One owner per shared contract jar, provided-scope discipline for anything a container supplies, CI job that fails on duplicate artifacts in the deployable, and load-once verification in staging using -verbose:class.