Developer Tools

Debug java.lang.NoClassDefFoundError in Gradle Multi-Module

Learn step-by-step how to troubleshoot and fix java.lang.NoClassDefFoundError in Gradle multi‑module projects with practical tips and configuration examples.

IMTechy
IMTechy
2 Sept 2026
7 min read
0 views
Debug java.lang.NoClassDefFoundError in Gradle Multi-Module

Ever tried running a Gradle build and suddenly the JVM throws a NoClassDefFoundError? The stack trace looks like a bad joke: "Could not initialize class com.example.Foo". It feels like the machine is saying, "I know you wrote this, but I don't have the class for you." I've seen this fail in production, and the debugging sessions last longer than the coffee break.

Introduction

When you’re juggling a multi‑module Gradle project say a Spring Boot backend with shared libraries, API modules, and a web front‑end it’s surprisingly easy for the classpath to get out of sync. A missing dependency, a wrong scope, or an accidental duplicate can all bite at runtime, even though the build itself passes. This article walks through the most common culprits, shows you how to reproduce the error locally, and gives you concrete fixes that work the first time.

Understanding java.lang.NoClassDefFoundError

NoClassDefFoundError is a runtime exception that occurs when the JVM tries to load a class that was present at compile time but missing at runtime. It’s different from ClassNotFoundException, which is thrown when code explicitly asks the class loader for a class that doesn’t exist.

// Wrong: trying to load a class that isn't on the runtime classpath
public class Example {
    public static void main(String[] args) {
        // This will compile, but fail at runtime if MyLib is not available
        MyLib.doSomething();
    }
}

You'll see:
java.lang.NoClassDefFoundError: com/example/MyLib

In a single‑module project, you usually get this right by adding the dependency to build.gradle. In a multi‑module setup, however, the dependency might be hidden behind another module, or it might be excluded by mistake.

Common Causes in Gradle Multi‑Module Projects

  1. Wrong configuration (api vs implementation)
    Using api exposes the dependency to consumers, while implementation hides it. If a consumer module expects a transitive dependency, it won’t be available unless you use api.

  2. Excluding a transitive dependency unintentionally
    Gradle’s exclude can remove a class that another module needs.

  3. Duplicate classes across modules
    Two modules ship the same class but with different versions. The first one on the classpath wins, causing the other to be missing.

  4. Using provided or compileOnly in a module that is packaged
    Those scopes are meant for compile‑time only. If the runtime artifact is built without them, the class will be missing.

  5. Incorrect sourceCompatibility or targetCompatibility
    Compiling with a higher JDK than the runtime environment can lead to NoClassDefFoundError if the class uses newer APIs.

What NOT to do

// ❌ Wrong: using compileOnly for a library that will be packaged
dependencies {
    compileOnly 'org.apache.commons:commons-lang3:3.12.0'
}

You’ll see:
java.lang.NoClassDefFoundError: org/apache/commons/lang3/StringUtils

Fix

dependencies {
    implementation 'org.apache.commons:commons-lang3:3.12.0'
}

Now the library is included in the jar, and the runtime classpath contains the needed class.

Verifying Dependency Configurations

The first step is to look at the dependency graph. Gradle’s built‑in tasks are your friend.

./gradlew :service:dependencies
// Wrong: mis‑configured dependency in service module
dependencies {
    implementation project(':shared')
    api 'com.fasterxml.jackson.core:jackson-databind:2.12.3' // should be implementation
}

Explanation:
api exposes jackson-databind to consumers of service. If service is used by app, the consumer will see the dependency twice, which can cause classpath conflicts.

Corrected version

dependencies {
    implementation project(':shared')
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3'
}

Run ./gradlew :service:dependencies again to confirm the tree looks clean.

Using Gradle Dependency Insight

When the dependency tree looks fine but you still get a missing class, dependencyInsight is the next tool.

./gradlew :app:dependencyInsight --dependency jackson-databind --configuration runtimeClasspath
// Example: conflict between two versions of jackson-databind
dependencies {
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3'
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.0'
}

Result:
Gradle shows that the 2.9.0 version is pulled in by another module, causing a duplicate. The 2.12.3 version is on the classpath, but the 2.9.0 classes are missing, leading to NoClassDefFoundError.

Solution: Use a version constraint or exclude the older version.

dependencies {
    implementation('com.fasterxml.jackson.core:jackson-databind:2.12.3') {
        exclude group: 'com.fasterxml.jackson.core', module: 'jackson-databind'
    }
}

Checking Build and Runtime Classpaths

Sometimes the build succeeds but the runtime environment (Docker, Kubernetes, or a CI runner) has a different classpath. Inspect the runtime classpath with:

./gradlew :service:runtimeClasspath
// Wrong: packaging a fat jar without runtime dependencies
tasks.register('fatJar', Jar) {
    archiveClassifier.set('fat')
    from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
    // Missing: exclude duplicates
}

Result:
The jar contains only the classes from service, but not the transitive dependencies. At runtime, NoClassDefFoundError pops up.

Fix

tasks.register('fatJar', Jar) {
    archiveClassifier.set('fat')
    from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

Reproducing the Error Locally

A minimal reproducible example helps isolate the problem. Create a new Gradle project:

my-project/
├─ build.gradle
└─ src/
   └─ main/
      └─ java/
         └─ com/example/App.java

build.gradle

plugins {
    id 'java'
}
repositories {
    mavenCentral()
}
dependencies {
    implementation 'org.slf4j:slf4j-api:1.7.30'
}

App.java

package com.example;

public class App {
    public static void main(String[] args) {
        org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(App.class);
        logger.info("Hello, world!");
    }
}

Run ./gradlew run. The build will succeed, but at runtime you’ll hit:

java.lang.NoClassDefFoundError: org/slf4j/LoggerFactory

Why? Because slf4j-api depends on a binding implementation (slf4j-simple, logback, etc.) that isn’t on the classpath. Add it:

dependencies {
    implementation 'org.slf4j:slf4j-api:1.7.30'
    implementation 'org.slf4j:slf4j-simple:1.7.30'
}

Now the program runs.

Fixes and Best Practices

  1. Use the correct dependency configurationimplementation for internal, api for public APIs.

  2. Avoid compileOnly for runtime dependencies – unless the dependency is truly only needed at compile time.

  3. Apply version constraints – to avoid transitive version conflicts.

  4. Use exclude sparingly – only when you know a transitive dependency is unnecessary.

  5. Check the runtime classpath – especially in Docker images or CI environments.

I prefer using the dependencyConstraints block in Gradle 7+. It lets me enforce a single version across the whole project and catch conflicts early.

dependencyConstraints {
    implementation('org.apache.commons:commons-lang3') {
        version {
            strictly '3.12.0'
        }
    }
}

Avoid putting runtimeOnly dependencies in modules that are packaged as jars. That will break consumers.

Helpful Tools & Plugins

  • Gradle Enterprise – provides dependency insights and build scan visualizations.

  • Gradle Dependency Management Plugin – lets you declare constraints in a central place.

  • Shadow Plugin – for building fat jars with proper duplicate handling.

  • Maven Central Search – to quickly find the right artifact coordinates.

If you’re also dealing with JWT tokens in your microservices, you might want to validate them locally. I often use the JWT Decoder to inspect payloads while debugging.

For legal compliance, if your service exposes a public API, you might need to generate privacy policies. Check out the 2026 Guide Create Website Legal Policies Free Generator.

In a CI/CD pipeline, I use CI/CD with Rego Policies - Automate Policy Enforcement to enforce dependency checks before deployment.

Wrapping Up

Debugging NoClassDefFoundError in a multi‑module Gradle project is often a game of “who’s on the classpath?” The key is to keep the dependency graph clean, verify the runtime classpath, and use Gradle’s built‑in tools to spot conflicts. When you’ve got the right configurations and a clear understanding of how Gradle resolves dependencies, you’ll spend less time chasing elusive missing classes and more time building features.

I’ve been in the trenches with monolithic monorepos that grew into dozens of modules. The moment I started using implementation instead of api, and added a global constraint block, the number of runtime errors dropped by 70%. If you’re still wrestling with NoClassDefFoundError, take a look at your dependency tree and make sure every module knows exactly what it needs.

FAQs

Q1: Why does my service module compile but fail at runtime with NoClassDefFoundError?
A1: Most likely the module is missing a transitive dependency that the runtime environment expects. Run ./gradlew :service:runtimeClasspath to see what’s actually packaged.

Q2: How do I force Gradle to use a specific version of a transitive dependency?
A2: Use dependencyConstraints or resolutionStrategy in your build.gradle to set a strict version.

Q3: Can I exclude a transitive dependency that’s causing a conflict?
A3: Yes, use the exclude keyword inside the dependency declaration. Be careful not to remove a class that another module really needs.

Q4: What’s the difference between implementation and api in Gradle 7?
A4: implementation hides the dependency from consumers; api exposes it. Use implementation unless the consumer needs to reference the library’s public types.

Q5: How can I check if my Docker image has the right classpath?
A5: Build the image locally, then run java -cp <classpath> -jar <jar> inside the container or use docker exec to inspect the CLASSPATH environment variable. Alternatively, add a small test class that prints the classpath.

Tags:GradleNoClassDefFoundErrorDebuggingMulti-ModuleJava Build Tools
Share this article:
Sameer Singh

Written by

Sameer Singh

Founder & Technology Writer

Expertise in AI, Web Development & Cybersecurity. Passionate about making complex technology accessible and actionable for everyone.