<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Software-Development on Vitalii Sukach</title><link>https://sukachvitally.github.io/tags/software-development/</link><description>Recent content in Software-Development on Vitalii Sukach</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sun, 19 Jul 2026 11:14:16 +0000</lastBuildDate><atom:link href="https://sukachvitally.github.io/tags/software-development/index.xml" rel="self" type="application/rss+xml"/><item><title>From Source to Running JVM: How Gradle Builds Your Java Application</title><link>https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/</link><pubDate>Sun, 19 Jul 2026 11:14:16 +0000</pubDate><guid>https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/</guid><description>&lt;p>Build phases, task graphs, dependency resolution, and what actually happens when the JVM opens your JAR&lt;/p>
&lt;p>You type gradle build, wait a few seconds, and out comes a JAR. Most Java developers use this pipeline every day without ever looking inside it. But when a build gets slow, a dependency conflict appears, or a NoClassDefFoundError shows up in production at 2 AM, understanding the machinery underneath is the difference between guessing and fixing.&lt;/p></description><content:encoded><![CDATA[<p>Build phases, task graphs, dependency resolution, and what actually happens when the JVM opens your JAR</p>
<p>You type gradle build, wait a few seconds, and out comes a JAR. Most Java developers use this pipeline every day without ever looking inside it. But when a build gets slow, a dependency conflict appears, or a NoClassDefFoundError shows up in production at 2 AM, understanding the machinery underneath is the difference between guessing and fixing.</p>
<p>This article walks the full journey: Gradle’s three build phases, the Java plugin’s task graph, how dependency resolution actually works stage by stage, what a JAR file really is, and how the JVM reads and executes it.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/image-01.png"></p>
<h3 id="part-1-the-three-phases-every-gradle-build-goesthrough">Part 1: The Three Phases Every Gradle Build Goes Through</h3>
<p>Before a single line of your code is compiled, Gradle itself runs through a fixed lifecycle of three phases. This is true for every build, every plugin, every project.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/image-02.png"></p>
<h4 id="phase-1initialization">Phase 1 — Initialization</h4>
<p>Gradle reads settings.gradle(.kts) and answers one question: <em>which projects are part of this build?</em> For a single-module app that&rsquo;s trivial; for a multi-module monorepo this is where include(&quot;:core&quot;, &ldquo;:api&rdquo;, &ldquo;:app&rdquo;) gets evaluated and a Project object is created for each module.</p>
<h4 id="phase-2configuration">Phase 2 — Configuration</h4>
<p>Now every build.gradle(.kts) script executes top to bottom. This is the phase people misunderstand most. Your build script is <em>code that runs</em> — and it runs here, on every invocation, regardless of which task you asked for. The output of this phase is the <strong>task graph</strong>: a directed acyclic graph (DAG) of all tasks and their dependencies. Tasks are registered and wired together, but none of their actions execute yet.</p>
<blockquote>
<p><em>A</em> <em>println at the top level of build.gradle prints during Configuration — even if you run</em> <em>gradle help. Task actions run later. This trips up nearly everyone once.</em></p></blockquote>
<h4 id="phase-3execution">Phase 3 — Execution</h4>
<p>Gradle takes the task you requested, computes the minimal subgraph of tasks needed to produce it, and executes them in dependency order. Tasks whose inputs and outputs haven’t changed since the last run are marked UP-TO-DATE and skipped entirely — this is Gradle&rsquo;s incremental build engine, and it&rsquo;s why the second build is always faster than the first.</p>
<h3 id="part-2-the-java-plugin-taskgraph">Part 2: The Java Plugin Task Graph</h3>
<p>Applying the java plugin registers a standard chain of tasks. When you run gradle build, this is the graph that gets executed:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/image-03.png"></p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/image-04.png"></p>
<p>The practical shortcuts worth memorizing:</p>
<pre tabindex="0"><code># fastest feedback: compile only
gradle compileJava
# package without running tests
gradle jar
# full build, skip test execution
gradle build -x test
# nuke build/ and start clean
gradle clean build
</code></pre><h3 id="part-3-how-gradle-dependency-resolution-actuallyworks">Part 3: How Gradle Dependency Resolution Actually Works</h3>
<p>This is the part of Gradle most developers treat as magic. You write three lines in a dependencies block, and somehow the right JARs appear on your classpath. Here&rsquo;s the machinery, stage by stage.</p>
<h4 id="configurations-buckets-of-dependencies">Configurations: buckets of dependencies</h4>
<p>Every dependency you declare goes into a named <strong>configuration</strong> — a bucket with specific rules about where those dependencies are visible:</p>
<pre tabindex="0"><code>dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-web:3.3.1&#39;
    api &#39;com.fasterxml.jackson.core:jackson-databind:2.17.1&#39;
    compileOnly &#39;org.projectlombok:lombok:1.18.32&#39;
    annotationProcessor &#39;org.projectlombok:lombok:1.18.32&#39;
    runtimeOnly &#39;org.postgresql:postgresql:42.7.3&#39;
    testImplementation &#39;org.junit.jupiter:junit-jupiter:5.10.2&#39;
}
</code></pre><p><img loading="lazy" src="https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/image-05.png"></p>
<p>The implementation vs api distinction matters in multi-module builds: implementation dependencies don&rsquo;t appear on the compile classpath of modules that depend on yours. That means fewer recompilations when they change, and a cleaner API boundary. Default to implementation; reach for api only when a type from that dependency appears in your public method signatures.</p>
<h4 id="the-resolution-stages">The resolution stages</h4>
<p>Declared dependencies are just coordinates on paper. Resolution is the process of turning them into actual files, and it happens <strong>lazily</strong> — only when a task first needs a classpath (typically compileJava during Execution). The stages:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/image-06.png"></p>
<p><strong>Stage 1 — Graph construction.</strong> For each declared coordinate (group:name:version), Gradle queries your repositories <em>in the order they&rsquo;re declared</em> and downloads only the <em>metadata</em> first: the POM file or Gradle Module Metadata. That metadata lists the dependency&rsquo;s own dependencies, so Gradle recursively walks outward until the full transitive graph is known. No JARs have been downloaded yet — just descriptors.</p>
<p><strong>Stage 2 — Conflict resolution.</strong> Real dependency graphs always contain the same library requested at different versions (your app wants Jackson 2.17, some transitive path wants 2.15). Gradle’s default strategy is <em>highest version wins</em> — unlike Maven, which picks the “nearest” declaration in the tree. This default is usually right, but you can override it:</p>
<pre tabindex="0"><code>configurations.all {
    resolutionStrategy {
        // hard-pin a version, overriding everything
        force &#39;com.fasterxml.jackson.core:jackson-databind:2.17.1&#39;
        // or turn conflicts into build failures
        failOnVersionConflict()
    }
}
</code></pre><p><strong>Stage 3 — Constraints and rules.</strong> Before the graph is finalized, Gradle applies platforms (BOMs), version constraints, exclusions, and dependency substitutions. This is how Spring Boot keeps hundreds of libraries version-aligned with one line:</p>
<pre tabindex="0"><code>dependencies {
    implementation platform(&#39;org.springframework.boot:spring-boot-dependencies:3.3.1&#39;)
    // now version numbers can be omitted — the BOM supplies them
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
}
</code></pre><p>Exclusions cut unwanted transitive branches at the declaration site:</p>
<pre tabindex="0"><code>implementation(&#39;org.springframework.boot:spring-boot-starter-web&#39;) {
    exclude group: &#39;org.springframework.boot&#39;, module: &#39;spring-boot-starter-tomcat&#39;
}
</code></pre><p><strong>Stage 4 — Artifact download.</strong> Only now, with the final graph settled, does Gradle download the actual JAR files — and only for modules that survived resolution. Everything lands in the shared cache at ~/.gradle/caches/modules-2, keyed by checksum, shared across all projects on your machine. A dependency is downloaded once, ever, per machine (dynamic versions like 1.+ and snapshots get re-checked periodically).</p>
<p><strong>Stage 5 — Classpath assembly.</strong> The resolved files are ordered into a classpath and handed to whichever task requested them: compileClasspath to javac, runtimeClasspath to the test JVM, and so on.</p>
<h4 id="debugging-resolution">Debugging resolution</h4>
<p>Two commands answer 90% of “why is this version on my classpath” questions:</p>
<pre tabindex="0"><code># full resolved tree per configuration
gradle dependencies --configuration runtimeClasspath
# trace one library: who pulls it in, which versions were requested,
# and why the winner won
gradle dependencyInsight --dependency jackson-databind
</code></pre><h3 id="part-4-what-a-jar-file-reallyis">Part 4: What a JAR File Really Is</h3>
<p>The output of the jar task is nothing exotic — it&rsquo;s a ZIP archive with an agreed-upon internal layout. Rename it to .zip and any archive tool will open it:</p>
<pre tabindex="0"><code>myapp.jar
├── META-INF/
│   └── MANIFEST.MF          ← metadata
├── com/example/App.class     ← bytecode, path = package name
├── com/example/util/Helper.class
└── application.properties    ← resources sit alongside classes
</code></pre><p>The manifest is a plain-text key-value file. The line that matters most for a runnable JAR:</p>
<pre tabindex="0"><code>Manifest-Version: 1.0
Main-Class: com.example.App
</code></pre><p>One important caveat: the plain jar task packages <em>only your classes</em>. Your dependencies are not inside. For a self-contained deployable artifact you need a fat JAR — with the Shadow plugin, or Spring Boot&rsquo;s bootJar, which nests dependency JARs and adds its own launcher.</p>
<pre tabindex="0"><code>plugins {
    id &#39;com.github.johnrengelman.shadow&#39; version &#39;8.1.1&#39;
}
// gradle shadowJar → one JAR containing everything
</code></pre><h3 id="part-5-how-the-jvm-reads-yourjar">Part 5: How the JVM Reads Your JAR</h3>
<p>You run java -jar myapp.jar. Here&rsquo;s the sequence, and it&rsquo;s more interesting than it looks.</p>
<h4 id="step-1the-launcher-reads-themanifest">Step 1 — The launcher reads the manifest</h4>
<p>The java launcher opens the JAR as a ZIP, reads META-INF/MANIFEST.MF, and extracts Main-Class. Note a classic gotcha: with -jar, any -cp argument on the command line is <strong>silently ignored</strong>. The JAR itself (plus manifest Class-Path entries) becomes the entire application classpath.</p>
<h4 id="step-2the-classloader-hierarchy">Step 2 — The classloader hierarchy</h4>
<p>Classes are loaded by a chain of classloaders using <strong>parent-first delegation</strong>: every load request travels up to the top of the chain first, and only comes back down if no ancestor can satisfy it.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/image-07.png"></p>
<h4 id="step-3lazyloading">Step 3 — Lazy loading</h4>
<p>The JVM does <strong>not</strong> load every class in the JAR at startup. A class is loaded the first time it’s actively used — instantiated, statically referenced, reflected upon. This is why a missing transitive dependency can let your app boot cleanly and then blow up with NoClassDefFoundError twenty minutes later, the first time a rarely-used code path runs.</p>
<h4 id="step-4load-link-initialize">Step 4 — Load, Link, Initialize</h4>
<p>Per the JVM specification, every class goes through three phases before it’s usable:</p>
<ol>
<li><strong>Loading</strong> — the class bytes are read from the JAR, parsed, and a Class object is created in metaspace.</li>
<li><strong>Linking</strong> — three sub-steps:
<ul>
<li>Verification: the bytecode is checked for structural and type safety (stack shapes, valid jumps, access rules).</li>
<li>Preparation: static fields are allocated and set to defaults — 0, null, false. Not your values yet.</li>
<li>Resolution: symbolic references to other classes, methods, and fields are resolved to concrete ones (often lazily).</li>
</ul>
</li>
<li><strong>Initialization</strong> — static initializer blocks and static field assignments run, in source order, exactly once, on first active use. This is the point where static { &hellip; } blocks fire.</li>
</ol>
<h3 id="step-5main-and-thejit">Step 5 — main() and the JIT</h3>
<p>Once Main-Class is loaded, linked, and initialized, the JVM invokes main(String[] args) on the main thread. Execution starts in the interpreter. As methods get hot — called thousands of times — the JIT compiler (tiered: C1 first for fast, lightly-optimized code, then C2 for aggressively optimized code) compiles them to native machine code. That&rsquo;s why JVM applications famously get <em>faster</em> after warm-up.</p>
<h3 id="the-wholepicture">The Whole Picture</h3>
<pre tabindex="0"><code>src/main/java  ──javac──▶  build/classes  ──jar──▶  myapp.jar
                                                        │
                                             java -jar myapp.jar
                                                        │
                       read MANIFEST.MF → find Main-Class
                                                        │
                 classloader delegation (parent-first)
                                                        │
                 Load → Link (verify/prepare/resolve) → Init
                                                        │
                            main() runs, classes load lazily,
                            JIT compiles hot paths as you go
</code></pre><p>None of this is knowledge you need daily. But the day your build takes four minutes instead of forty seconds, or two versions of the same library fight on your classpath, or a class mysteriously fails to load in production — this is the map you’ll be reading. Better to have seen it before that day arrives.</p>
]]></content:encoded></item></channel></rss>