In a standalone Tomcat, the same library jar sits both in $CATALINA_BASE/lib and in a web application's WEB-INF/lib. Which copy does the application's code load, and what goes wrong when instances of that library's classes have to pass between Tomcat's own code and the application?
answer
- webapp first, not parent first
- two loaders, one class name
- cast fails against itself
- container code sees only the shared lib
basics
~20 sThe application's own copy wins: Tomcat's web application class loader searches WEB-INF/classes and WEB-INF/lib before the shared loader that owns $CATALINA_BASE/lib. Both copies then exist as distinct classes, so objects crossing between container and application fail with ClassCastException or NoClassDefFoundError.
solid answer
~50 sTomcat deliberately breaks the usual parent-first rule for web applications. After its own cache, the web application class loader consults the JVM's bootstrap classes, then `/WEB-INF/classes`, then the jars in `/WEB-INF/lib`, and only then the common loader that owns `$CATALINA_BASE/lib` and `$CATALINA_HOME/lib`. So the bundled copy shadows the shared one — that is the point, since it lets each application pin its own library versions. The cost is that the two copies are different runtime classes even though the names match: a `com.example.Thing` created by Tomcat's loader cannot be cast to `com.example.Thing` as the webapp knows it, and you get `ClassCastException` or `LinkageError` with a confusingly self-identical message. Two exceptions matter: JVM classes always come from the parent, and Tomcat refuses to load a jar from `WEB-INF/lib` that contains the Servlet API. `<Loader delegate="true"/>` in the context restores parent-first if you want it.
code
xml · 4 lines<Context>
<!-- Restore standard parent-first delegation for this application only -->
<Loader delegate="true"/>
</Context>go deeper
Know that a web application's own jars in WEB-INF/lib take precedence over Tomcat's shared lib directory, and that shipping the same jar in both places is asking for trouble.
Recite the actual lookup order — cache, JVM classes, WEB-INF/classes, WEB-INF/lib, then common — and explain why class identity includes the loader, which is what turns a duplicate jar into a ClassCastException.
Diagnose from symptoms: read a self-identical cast failure or a LinkageError back to two copies of a jar, know why container-created objects such as JDBC pools only see the shared directory, and decide deliberately which jars are shared.
Set the policy for a shared Tomcat estate — what is allowed in the shared lib directory, how applications pin versions without colliding, and whether shared-container hosting is worth its isolation cost at all.
## The loader hierarchy Tomcat builds A running Tomcat has several class loaders stacked up: - **Bootstrap / platform** — the JVM's own, holding the Java runtime classes. - **System** — the classes on the launch classpath, essentially `bootstrap.jar` and `tomcat-juli.jar`. - **Common** — everything in `$CATALINA_BASE/lib` and `$CATALINA_HOME/lib`. This is visible to Tomcat's internals *and* to every deployed application, and it is where the Servlet API itself lives. - **Web application** — one per deployed Context, owning `/WEB-INF/classes` and the jars in `/WEB-INF/lib`. The web application loader is created fresh for each deployment, which is what makes redeploy possible and what makes leaked loaders expensive. ## The search order, and why it is inverted The standard Java delegation model is parent-first: ask the parent, and only load it yourself if the parent cannot. Tomcat's web application loader deviates on purpose, in line with the Servlet specification's allowance for it. Its order is: 1. its own cache of already-loaded classes, 2. the JVM's bootstrap classes, 3. `/WEB-INF/classes`, 4. the jars under `/WEB-INF/lib`, in alphabetical order, 5. the common loader (`$CATALINA_BASE/lib`, `$CATALINA_HOME/lib`), and above it the system and bootstrap loaders. Step 2 is the safety valve: an application cannot shadow `java.lang.String` no matter what it packages. Steps 3–4 before step 5 are the inversion: **your jar wins over the container's copy**. That is exactly what you want when an application needs a newer library than the one the shared directory happens to hold. Two further rules bound it. Tomcat will not load a jar from `WEB-INF/lib` that contains the Servlet API classes — it logs that the jar was not loaded and skips it, because the container and the application must agree on `HttpServletRequest` and friends or nothing can work. And you can opt back into parent-first for one application: ```xml <Context> <Loader delegate="true"/> </Context> ``` ## What actually breaks with two copies A class's runtime identity is *(name, defining class loader)*, not name alone. With the library in both places, `com.example.Thing` loaded by the common loader and `com.example.Thing` loaded by the webapp loader are two unrelated types. As long as each side only talks to itself, nothing is noticed. The moment an instance crosses the boundary you get one of: - **`ClassCastException`** whose message names the same class twice — the tell-tale symptom, e.g. `com.example.Thing cannot be cast to com.example.Thing`. - **`NoClassDefFoundError` / `LinkageError`** when a class loaded by one loader references a class the other loader cannot see, or when a *loader constraint* is violated because two loaders disagree on a type appearing in a method signature. - **`ClassNotFoundException` from container code**, the classic case being a JDBC driver: a `<Resource>` DataSource is built by Tomcat's own code using the common loader, so a driver jar sitting only in `WEB-INF/lib` is invisible to it. That is why JDBC drivers for container-managed pools belong in `$CATALINA_BASE/lib`. A related trap is the **thread context class loader**. Tomcat sets it to the webapp loader while serving a request, and libraries that call `Thread.currentThread().getContextClassLoader()` will therefore resolve into the application even when the library itself came from the shared directory. That is how a common-loader library can end up holding a reference into a webapp — the seed of a redeploy leak. ## Deciding where a jar belongs The rule of thumb: - **`WEB-INF/lib`** for anything the application alone uses. Default, and it keeps applications independent. - **`$CATALINA_BASE/lib`** only for things the *container* must touch: JDBC drivers behind container-managed pools, JNDI resource factories, valves, realms, and libraries you deliberately want shared by every application on the instance. - **Never both.** If a jar must be shared, remove it from the WAR — most build tools express this as a `provided` dependency so the class is compiled against but not packaged. ## Diagnosing it Turn the question into evidence rather than argument: print `Thing.class.getClassLoader()` and `Thing.class.getProtectionDomain().getCodeSource().getLocation()` from inside the application to see which file the class actually came from, and grep the deployed unit for duplicate artifacts. Tomcat's own logs help too — the context start-up log names jars that were skipped, and the Manager application can list a context's resources. Once you can name the two files, the fix is always to delete one of them.
- Why does a JDBC driver for a container-managed DataSource have to live in $CATALINA_BASE/lib rather than in the WAR?Because Tomcat, not your code, instantiates the pool from the `<Resource>` element, and Tomcat's own classes are loaded by the common loader. That loader never looks inside `WEB-INF/lib`, so it cannot find the driver class and the pool fails to start. If the application creates its own pool in application code, the driver can stay in the WAR — the difference is who does the loading.
- How does the thread context class loader fit into this picture?Tomcat sets the thread's context class loader to the webapp's loader while that application's code runs, and restores it afterwards. Libraries that resolve classes reflectively — service loaders, ORM providers, serialization frameworks — use it, so a shared library can load application classes. It is also how a shared or JVM-level cache can end up holding a webapp loader alive across a redeploy.
- What does the message "ClassCastException: com.example.Thing cannot be cast to com.example.Thing" tell you immediately?That the class exists twice under different class loaders — almost always the same jar in both `WEB-INF/lib` and a shared directory, or bundled twice inside the WAR. The cast is comparing runtime identities, which include the defining loader, so identical names are not the same type. The fix is to remove one copy, not to add a cast or a reflection workaround.
Two copies of a library in Tomcat are like two people with the same passport number in different countries: each is fine at home, but the moment they have to be recognised as the same person at a border, the paperwork fails.
saying these in an interview costs you the question
- Saying Tomcat always delegates to the parent first
- Assuming same class name means same runtime class
- Putting the JDBC driver in WEB-INF/lib for a container pool
- Bundling servlet-api.jar inside the WAR
- Treating a duplicate-jar ClassCastException as an application bug