Mid-level 3 min · March 09, 2026

Gradle: Jackson NoClassDefFoundError on Upgrade

Jackson NoClassDefFoundError in WAR after Spring Boot upgrade? Gradle picks highest version but third-party overrides.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Gradle build script is a declarative configuration file (build.gradle or build.gradle.kts) that defines project build, test, and deployment steps
  • Uses Directed Acyclic Graph (DAG) to determine optimal task execution order
  • Three-phase lifecycle: Initialization, Configuration, Execution — code outside doLast runs in Configuration phase
  • Performance insight: Incremental builds skip unchanged tasks, reducing build times by 70-90% in large projects
  • Production insight: Configuration phase slowdowns (network calls, file I/O) affect every Gradle command, even gradle tasks
  • Biggest mistake: Treating build script as imperative code instead of declarative DSL, leading to slow builds and maintainability issues
Plain-English First

Think of a Gradle Build Script as a high-tech recipe for your software. Instead of just listing ingredients (dependencies), it provides a set of smart instructions on how to mix, cook, and package them. If the kitchen (your environment) changes, the script adapts to ensure the final dish (your JAR file) always tastes exactly the same.

Gradle Build Script Basics is a fundamental concept in Java and Spring Boot development. It moves beyond the rigid XML structure of Maven to provide a flexible, code-centric approach to automation. Understanding the build script is essential for managing complex project lifecycles effectively.

In this guide, we'll break down exactly what a Gradle build script is, why its Directed Acyclic Graph (DAG) architecture was designed this way, and how to use it correctly in real-world Java projects.

By the end, you'll have both the conceptual understanding and practical code examples to use Gradle with confidence in any 'TheCodeForge' production environment.

What Is a Gradle Build Script and Why Does It Exist?

A Gradle build script (typically build.gradle or build.gradle.kts) is the declarative configuration that defines how a project is compiled, tested, and deployed. It exists to solve the problem of build rigidity found in older tools. By utilizing a powerful Domain Specific Language (DSL), Gradle allows developers to define custom logic while maintaining high performance through incremental builds and build caching. It treats your build as a set of interdependent tasks that can be executed in parallel where possible.

The core strength lies in how Gradle models the build as a Directed Acyclic Graph (DAG). This means every task knows exactly what it needs to run before it can start. If you run a 'build' task, Gradle calculates the most efficient path to get there, skipping tasks whose inputs and outputs haven't changed since the last run.

build.gradleGROOVY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* 
 * io.thecodeforge standard build configuration
 * Strategy: Use implementation for internal dependencies to avoid leaking transitive deps
 */
plugins {
    id 'org.springframework.boot' version '3.2.3'
    id 'io.spring.dependency-management' version '1.1.4'
    id 'java'
}

// Organization-wide branding and metadata
group = 'io.thecodeforge'
version = '1.0.0-RELEASE'
sourceCompatibility = JavaVersion.VERSION_17

repositories {
    mavenCentral() // Primary source for production-grade artifacts
}

dependencies {
    // Standard Spring Boot starter for RESTful services
    implementation 'org.springframework.boot:spring-boot-starter-web'
    
    // Monitoring and health checks for io.thecodeforge production nodes
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    
    // Test suite dependencies
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
    testLogging {
        events "passed", "skipped", "failed"
    }
}
Output
BUILD SUCCESSFUL in 2s
3 actionable tasks: 3 executed
Key Insight:
The most important thing to understand about Gradle is its three-phase lifecycle: Initialization, Configuration, and Execution. Code written outside of a task's 'doLast' block runs during the Configuration phase, which can impact build speed if overused.
Production Insight
Configuration phase code that performs network I/O or file scans will degrade every Gradle command. At TheCodeForge, we traced a 30-second gradle tasks command to a println that triggered a remote service call.
Rule: Keep configuration phase pure and fast — only declare tasks and configurations, never execute them.
Key Takeaway
Gradle's DAG enables parallel and incremental execution.
Configuration avoidance (tasks.register) keeps builds fast.
The build script is declarative, not imperative.

Common Mistakes and How to Avoid Them

When learning Gradle, most developers fall into the trap of treating the build script like a standard imperative program. This leads to common mistakes such as performing heavy I/O operations or network calls during the configuration phase, which slows down every build command. Another frequent error is ignoring the 'Gradle Wrapper' (gradlew), leading to 'it works on my machine' syndrome where different team members use conflicting Gradle versions.

At TheCodeForge, we enforce the 'Configuration Avoidance API'. This means registering tasks instead of creating them eagerly. When you use tasks.register, the task is only configured if it's actually going to be executed, saving precious seconds during every developer's inner-loop build cycle.

io/thecodeforge/scripts/BuildOptimization.gradleGROOVY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge: Avoiding Configuration Phase overhead

// ANTI-PATTERN: This runs EVERY time you run 'gradle tasks' or any other command
// println "Connecting to external service for metadata..." 

// CORRECT: Registering a task for TheCodeForge deployment checks
// This only configures and runs when explicitly called: ./gradlew forgeEnvCheck
tasks.register('forgeEnvCheck') {
    group = 'verification'
    description = 'Validates environment variables for io.thecodeforge deployment.'
    
    doLast {
        def forgeKey = System.getenv('FORGE_API_KEY')
        if (forgeKey == null) {
            throw new GradleException("FORGE_API_KEY is missing from environment!")
        }
        println "Environment Check: SUCCESS for io.thecodeforge production context."
    }
}
Output
Task :forgeEnvCheck
Environment Check: SUCCESS for io.thecodeforge production context.
Watch Out:
Never hardcode versions across multiple modules. Use the 'libs.versions.toml' (Version Catalog) to centralize dependency management, ensuring consistency across all io.thecodeforge microservices.
Production Insight
Hardcoding versions across multiple modules leads to silent build failures when a dependency unexpectedly resolves to a different version. Version catalogs eliminate this risk.
Rule: Use libs.versions.toml for all dependency versions.
Key Takeaway
Prefer tasks.register over tasks.create.
Use version catalogs for centralized dependency management.
The Gradle Wrapper ensures build reproducibility.

The Three-Phase Lifecycle: Initialization, Configuration, Execution

Gradle's build lifecycle is divided into three distinct phases: Initialization, Configuration, and Execution. During Initialization, Gradle locates the settings file (settings.gradle) and creates the project hierarchy. In the Configuration phase, it evaluates all build scripts and task configurations, building the DAG of tasks. The Execution phase runs the tasks that are actually needed for the requested command.

A critical nuance: any code you write outside of a task's doLast block executes in the Configuration phase. This includes println statements, file reads, and network calls. If you write such code at the top level of build.gradle, it runs every time you run any Gradle command, even gradle tasks. This is the single biggest source of build slowdowns.

Gradle's configuration avoidance API (tasks.register) defers task configuration until the task is actually executed, speeding up the Configuration phase significantly.

build.gradleGROOVY
1
2
3
4
5
6
7
8
9
10
11
// ANTI-PATTERN: This prints every time any task is run
println "Loading project metadata..."

// CORRECT: Use a task with doLast
tasks.register('printMetadata') {
    doLast {
        println "Loading project metadata..."
    }
}

// The Configuration phase should only declare, not execute
Lifecycle Analogy
  • Initialization: Assemble the cast (projects).
  • Configuration: Read the script (build scripts) and plan scenes (tasks).
  • Execution: Film only the scenes needed for the final cut (requested tasks).
  • Any line spoken during script reading slows down the whole production.
Production Insight
A team at TheCodeForge had a gradle tasks taking 45 seconds. Profiling revealed a top-level apply from: 'http://...' that fetched a remote script each time. Moved to a local file and cached the result — tasks dropped to 3 seconds.
Rule: Never perform network calls or heavy computation in the configuration phase.
Key Takeaway
Configuration code runs on every command.
Use tasks.register to defer configuration.
Profile with --profile to find configuration bottlenecks.

Dependency Management: implementation vs api vs compile

Gradle offers several dependency configurations to control how dependencies are exposed to consumers. The most important distinction is between implementation and api (provided by the java-library plugin).

  • implementation: The dependency is used by the module but not exposed to consumers. If your module depends on library A via implementation, projects that depend on your module cannot access library A's classes. This improves compilation speed because changes to library A only trigger recompilation of your module, not its consumers.
  • api: The dependency is part of the module's public API. Consumers of your module will see library A on their compile classpath. Use this when the types from the dependency appear in the module's public method signatures.
  • compile (deprecated): The older, broader configuration that always exposed dependencies. It has been replaced by implementation and api to give finer control.
build.gradleGROOVY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge: correct dependency scoping
plugins {
    id 'java-library'  // enables 'api' configuration
}

dependencies {
    // Internal dependency, not exposed to consumers
    implementation 'com.google.guava:guava:32.1.3-jre'
    
    // API dependency — consumers will need these types
    api 'org.springframework.data:spring-data-commons:3.2.0'
    
    // Only for test code
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}
Migration Note
If you see compile in older scripts, replace it with implementation unless you explicitly need to expose the dependency. The java-library plugin adds api; without it, api is not available.
Production Insight
Overusing api causes a recompilation cascade. At TheCodeForge, a change in a low-level logging library triggered recompilation of 15 microservices because it was declared as api in a shared utility module. Changing it to implementation reduced build time by 60%.
Rule: Default to implementation; only use api when the dependency's types are part of your public API.
Key Takeaway
implementation hides dependencies from consumers.
api exposes them, causing slower recompilation.
Use java-library plugin to get api configuration.
Default to implementation — always.

Build Performance: Incremental Builds, Build Cache and Configuration Avoidance

Gradle's incremental build capability is its strongest performance feature. A task is considered UP-TO-DATE if its inputs and outputs haven't changed since the last execution. Gradle tracks inputs (source files, properties, other tasks' outputs) and outputs (generated files, archives). To leverage this, define your custom tasks with @InputFiles, @Input, @OutputDirectory, and similar annotations.

The Build Cache extends this concept across machines. When enabled, Gradle stores task outputs in a shared cache (remote or local). If another developer or CI runs the same task with the same inputs, Gradle downloads the output instead of re-executing. This can dramatically speed up CI pipelines.

Configuration avoidance (using tasks.register instead of tasks.create) ensures that the Configuration phase only processes tasks that are actually needed. This reduces the 'time-to-first-task' and is especially beneficial in multi-project builds.

build.gradleGROOVY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge: Declarative custom task with input/output annotations
abstract class ForgeReportTask extends DefaultTask {
    @InputFile
    abstract RegularFileProperty getInputFile()

    @OutputDirectory
    abstract DirectoryProperty getOutputDir()

    @TaskAction
    void generateReport() {
        def input = inputFile.get().asFile
        def outDir = outputDir.get().asFile
        // generate report from input
    }
}

// Register task: configuration is lazy
tasks.register('generateForgeReport', ForgeReportTask) {
    inputFile = layout.projectDirectory.file('data.csv')
    outputDir = layout.buildDirectory.dir('reports')
}
Build Cache Tip
Use a remote build cache (e.g., Gradle Enterprise or an S3 bucket) in CI. Local cache on developers' machines helps too, but remote cache yields the biggest gains for teams.
Production Insight
A misconfigured build cache can cause stale outputs. TheCodeForge had an issue where the CI cache key did not include environment variables, leading to outdated artifacts being reused after a config change. Always include all relevant inputs in your cache key.
Rule: Ensure cache keys cover all inputs — source, environment, and build configuration.
Key Takeaway
Incremental builds rely on correct input/output annotations.
Build cache speeds up CI but must include all inputs.
Use tasks.register for configuration avoidance.
Profile with --build-cache to measure cache effectiveness.
● Production incidentPOST-MORTEMseverity: high

Gradle Dependency Resolution Breaks Spring Boot Upgrade

Symptom
After updating the Spring Boot version in build.gradle, the build fails with NoClassDefFoundError for a Jackson library. The error appears only in the compiled WAR, not in tests.
Assumption
The team assumed that upgrading the Spring Boot BOM would automatically align all transitive dependencies. They did not check for exclusions or version overrides.
Root cause
Gradle resolves dependencies by picking the highest version of each module, but a third-party library (used for XML parsing) explicitly requested an older Jackson version via its own POM, causing a classpath clash. The BOM did not force the newer version because the dependency was declared separately.
Fix
Use constraints or force to lock Jackson version across the project. Add an explicit dependency with implementation('com.fasterxml.jackson.core:jackson-databind:2.15.2') and use resolutionStrategy.force in the build script. Also, run dependencies task to inspect the tree.
Key lesson
  • Always run gradle dependencies before and after major upgrades.
  • Use Gradle's 'consistent resolution' feature or a version catalog (libs.versions.toml) to centralize dependency versions.
  • Do not trust BOM alone — third-party libraries can override effectively.
Production debug guideSymptom -> Action guide for common Gradle build issues5 entries
Symptom · 01
Build fails with 'Could not resolve all dependencies'
Fix
Run gradle dependencies to see the full dependency tree. Look for version conflicts or missing repositories. Add missing repository or exclude transitive dependency.
Symptom · 02
Build is slow even on no-code-change
Fix
Check if configuration phase code is expensive. Use --profile flag to generate a build scan. Move expensive logic into doLast blocks. Use tasks.register instead of tasks.create.
Symptom · 03
Test phase fails with random classpath errors
Fix
Check testImplementation dependencies for scope misconfiguration. Ensure you're using testImplementation not implementation for test-only libs. Run tests with --info logging to see classpath.
Symptom · 04
JVM runs out of memory during build
Fix
Increase Gradle daemon memory via gradle.properties: org.gradle.jvmargs=-Xmx4g. Also check for memory leaks in build scripts, e.g., accumulating large data structures in configuration.
Symptom · 05
Build succeeds locally but fails on CI
Fix
Ensure CI uses the same Gradle wrapper version. Check gradle-wrapper.properties. Also verify that CI has access to required repositories and that the daemon is not causing inconsistency (disable daemon on CI with --no-daemon).
★ Quick Debug Cheat Sheet for GradleImmediate commands and fixes for common Gradle build issues
Dependency conflict error
Immediate action
Run `gradle dependencies --configuration compileClasspath`
Commands
gradle dependencies > deps.txt
grep -i 'conflict' deps.txt
Fix now
Add a resolutionStrategy.force for the required version in the build script.
Build too slow, need profile+
Immediate action
Add `--profile` flag and open the HTML report
Commands
gradle build --profile
open build/reports/profile/profile-*.html
Fix now
Move heavy logic out of configuration phase; use doLast for task actions.
Out of memory during build+
Immediate action
Increase daemon memory via `gradle.properties`
Commands
echo 'org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m' >> gradle.properties
gradle clean build --no-daemon
Fix now
Restart daemon: gradle --stop then rerun build.
Task not running (UP-TO-DATE)+
Immediate action
Check if inputs changed. Force run with `--rerun-tasks`.
Commands
gradle test --rerun-tasks
gradle -p build --info test
Fix now
Ensure task inputs/outputs are declared properly. Use @InputFiles and @OutputDirectory annotations in custom tasks.
Gradle wrapper not found or version mismatch+
Immediate action
Generate or update wrapper
Commands
gradle wrapper --gradle-version 8.5
./gradlew tasks
Fix now
Commit the wrapper files to version control. On CI use ./gradlew to ensure consistent version.
AspectManual Compilation (javac)Gradle Build Automation
Dependency MgmtManual JAR downloadsAutomated via MavenCentral/Ivy
Build SpeedSlow (Full recompilation)Fast (Incremental & Cached)
FlexibilityNoneHigh (Groovy/Kotlin DSL scripts)
StandardizationVaries by developerUnified (Build-as-Code)
LifecycleNot existingStructured Phases (Init/Config/Exec)

Key takeaways

1
Gradle uses a Directed Acyclic Graph (DAG) to manage task execution and dependencies efficiently.
2
Always use the Gradle Wrapper (./gradlew) instead of a local system installation to ensure reproducibility.
3
Declarative configuration is preferred over imperative logic to keep scripts maintainable and fast.
4
Incremental builds rely on accurate task input/output definitions—structure tasks correctly to leverage Gradle's up-to-date checking.
5
Configuration avoidance is key
prefer tasks.register over tasks.create to keep your build's 'time-to-start' low.

Common mistakes to avoid

4 patterns
×

Running logic in the Configuration Phase

Symptom
Build script sluggish even for simple commands like 'gradle tasks'
Fix
Use doLast { } blocks or task registration to defer execution to the Execution phase.
×

Hardcoding local file paths

Symptom
Build breaks for other team members and CI/CD pipelines
Fix
Always use project-relative paths via 'projectDir' or 'layout.buildDirectory'.
×

Using 'compile' or 'api' when 'implementation' is sufficient

Symptom
Slower compilation and larger classpath issues due to over-exposed dependencies
Fix
Use 'implementation' to restrict the dependency to the current module; only use 'api' if the dependency is part of your public API.
×

Not using the Gradle Wrapper

Symptom
Version drift across team members and CI — 'it works on my machine' syndrome
Fix
Always use './gradlew build' to ensure the exact version defined in gradle-wrapper.properties is used.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the three phases of the Gradle Build Lifecycle and what specific...
Q02SENIOR
How does Gradle's 'Implementation' configuration improve build performan...
Q03SENIOR
Describe how Gradle determines if a task is 'UP-TO-DATE'. What inputs an...
Q04JUNIOR
What is the Gradle Wrapper and why is it considered a best practice for ...
Q05SENIOR
What is a 'Daemon' in the context of Gradle, and how does it contribute ...
Q01 of 05SENIOR

Explain the three phases of the Gradle Build Lifecycle and what specifically happens in the 'Configuration' phase versus the 'Execution' phase.

ANSWER
The three phases are Initialization, Configuration, and Execution. During Initialization, Gradle builds the project hierarchy from settings.gradle. The Configuration phase evaluates all build scripts and configures tasks; any code outside of a task's doLast block runs here. The Execution phase runs the tasks that are actually needed for the requested command. A key insight: the Configuration phase runs for every command, even gradle tasks, so it should be kept lightweight. Use tasks.register to defer configuration until execution is needed.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the difference between 'implementation' and 'api' in a build script?
02
Why is my Gradle build slow even when I haven't changed any code?
03
How do I upgrade the Gradle version using the wrapper?
04
Can I use Kotlin instead of Groovy for my build scripts?
🔥

That's Build Tools. Mark it forged?

3 min read · try the examples if you haven't

Previous
Maven Dependency Management Explained
5 / 5 · Build Tools
Next
Java Threads and Runnable Explained