Gradle Deep Dive
If you come from TypeScript you know npm/yarn/pnpm plus package.json:
simple, declarative, works. If you come from Go you know go mod plus go.mod —
even simpler. JVM projects need more, and Gradle is what fills the gap. This module
maps every Gradle concept back to the npm/go-mod tools you already use.
This module assumes you’ve completed the language fundamentals (modules 01–04), have JDK 21 installed, and are comfortable on the command line.
Why Another Build Tool?
Section titled “Why Another Build Tool?”JVM languages need more than package.json because:
- Compilation step — Kotlin/Java must be compiled before running (unlike TS with
ts-nodeor Go’s fast compiler). - Complex classpaths — the JVM loads classes at runtime from JARs; dependency resolution matters more.
- Multi-module monorepos — enterprise JVM projects often have 10–100+ modules in one repo.
- Build variants — test vs. production, different JVM targets, native images, fat JARs.
Gradle handles all of this. It uses Kotlin (or Groovy) as its build-script language, so your build files are actual programs, not just config.
Gradle vs. Maven
Section titled “Gradle vs. Maven”You’ll see Maven in older projects. Quick comparison:
| Aspect | Gradle | Maven |
|---|---|---|
| Config language | Kotlin DSL (.kts) or Groovy | XML (pom.xml) |
| Speed | Incremental + build cache | Full rebuilds |
| Flexibility | Turing-complete scripts | Plugin-only extension |
| Learning curve | Steeper | Simpler (less powerful) |
| Kotlin support | First-class | Via plugin, works fine |
Mental Model: npm / go mod / Gradle
Section titled “Mental Model: npm / go mod / Gradle”| Concept | npm (TypeScript) | go mod (Go) | Gradle (Kotlin) |
|---|---|---|---|
| Project manifest | package.json | go.mod | build.gradle.kts |
| Lock file | package-lock.json | go.sum | gradle.lockfile (opt-in) |
| Dependency install | npm install | go mod download | ./gradlew build (auto) |
| Run scripts | npm run dev | go run . | ./gradlew run |
| Test | npm test | go test ./... | ./gradlew test |
| Workspaces / modules | npm workspaces | Go workspace | Multi-module project |
| Registry | npmjs.com | proxy.golang.org | Maven Central / Google |
| Version pinning | ^1.2.3 in package.json | v1.2.3 in go.mod | Version catalogs |
| Dev dependencies | devDependencies | N/A (build tags) | testImplementation |
| Build output | dist/ | binary in $GOPATH | build/ directory |
| Task runner | npm scripts / Makefile | Makefile | Gradle tasks |
| Project settings | package.json name/version | go.mod module path | settings.gradle.kts |
Project Structure
Section titled “Project Structure”The minimal Kotlin project layout, with each file annotated:
Directorymy-app/
- build.gradle.kts build configuration (like package.json)
- settings.gradle.kts project name + module declarations
- gradle.properties build properties (JVM args, versions)
Directorygradle/
Directorywrapper/
- gradle-wrapper.jar Gradle bootstrap (committed to git)
- gradle-wrapper.properties Gradle version config
- libs.versions.toml version catalog (centralized deps)
- gradlew Unix wrapper script (committed to git)
- gradlew.bat Windows wrapper script
Directorysrc/
Directorymain/
Directorykotlin/ Kotlin source files
Directorycom/example/
- App.kt
Directoryresources/ non-code files (config, templates)
- application.yml
Directorytest/
Directorykotlin/ test source files
Directorycom/example/
- AppTest.kt
Directoryresources/ test-specific resources
- …
The same project, compared across ecosystems:
Directorymy-ts-app/
- package.json ≈ build.gradle.kts
- tsconfig.json ≈
kotlin { jvmToolchain(21) } - package-lock.json ≈ gradle.lockfile
Directorynode_modules/ ≈ ~/.gradle/caches/ (global, not per-project)
- …
Directorysrc/
- index.ts ≈ src/main/kotlin/App.kt
Directorytests/
- index.test.ts ≈ src/test/kotlin/AppTest.kt
Directorymy-go-app/
- go.mod ≈ build.gradle.kts + settings.gradle.kts
- go.sum ≈ gradle.lockfile
- main.go ≈ src/main/kotlin/App.kt
- main_test.go ≈ src/test/kotlin/AppTest.kt (same dir in Go)
Directoryinternal/
Directoryhandler/
- handler.go ≈ src/main/kotlin/com/example/handler/
Directorymy-app/
- build.gradle.kts build configuration
- settings.gradle.kts project name + module declarations
- gradle/libs.versions.toml version catalog
- gradlew wrapper script
Directorysrc/
Directorymain/kotlin/com/example/
- App.kt
Directorymain/resources/
- application.yml
Directorytest/kotlin/com/example/
- AppTest.kt
Where Are Dependencies Stored?
Section titled “Where Are Dependencies Stored?”| System | Location |
|---|---|
| npm | ./node_modules/ (per-project) |
| Go | $GOPATH/pkg/mod/ (global cache) |
| Gradle | ~/.gradle/caches/ (global cache) |
Gradle never copies dependencies into your project. They live in a global cache and
are resolved at build time. This is why there’s no node_modules equivalent to
accidentally commit.
build.gradle.kts In Depth
Section titled “build.gradle.kts In Depth”Here’s the minimal build script, with each block mapped to a concept you know:
plugins { kotlin("jvm") version "2.1.0" // Kotlin compiler plugin application // Enables `./gradlew run`}
group = "com.example"version = "1.0.0"
repositories { mavenCentral() // Where to find dependencies (like npmjs.com)}
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") testImplementation(kotlin("test")) // Kotlin test framework}
kotlin { jvmToolchain(21) // Target JDK 21}
application { mainClass.set("com.example.AppKt") // Entry point for `./gradlew run`}
tasks.test { useJUnitPlatform() // Use JUnit 5 test runner}The same project’s manifest in the ecosystems you already know:
{ "name": "com.example.my-app", "version": "1.0.0", "main": "src/index.ts", "scripts": { "build": "tsc", "start": "node dist/index.js", "test": "jest" }, "dependencies": { "some-async-lib": "^1.9.0" }, "devDependencies": { "typescript": "^5.0.0", "jest": "^29.0.0", "@types/jest": "^29.0.0" }, "engines": { "node": ">=21" }}module github.com/example/my-app
go 1.21
require ( github.com/some/async-lib v1.9.0)Breaking Down Each Block
Section titled “Breaking Down Each Block”plugins { }
Section titled “plugins { }”Plugins extend Gradle’s capabilities. Think of them as npm packages that modify the build process itself.
plugins { // The Kotlin JVM compiler plugin -- without this, Gradle can't compile .kt files kotlin("jvm") version "2.1.0"
// Adds `run` task and creates start scripts for distribution application
// Spring Boot plugin (when doing Spring) id("org.springframework.boot") version "3.4.1"
// Ktor plugin (when doing Ktor) id("io.ktor.plugin") version "3.0.3"}| npm analogy | Gradle plugin |
|---|---|
typescript (devDep) | kotlin("jvm") |
ts-node | application plugin |
jest | Built into Kotlin test plugin |
repositories { }
Section titled “repositories { }”Where Gradle downloads dependencies from. There’s no single default like npmjs.com.
repositories { mavenCentral() // The main public repo (like npmjs.com) google() // Google's repo (Android, some libs) maven("https://jitpack.io") // JitPack (build from GitHub repos) mavenLocal() // Your local ~/.m2 cache}dependencies { }
Section titled “dependencies { }”This is the big one. The configurations control where a dependency is visible:
dependencies { // Your app needs this at runtime -- like npm "dependencies" implementation("io.ktor:ktor-server-netty:3.0.3")
// Like implementation, but also exposes the dependency to consumers // Only matters in library modules -- like npm "peerDependencies" api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
// Only for tests -- like npm "devDependencies" (test-only) testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
// Needed at runtime but not at compile time // Example: database drivers, logging backends runtimeOnly("org.postgresql:postgresql:42.7.4")
// Needed at compile time but not at runtime (rare) compileOnly("org.projectlombok:lombok:1.18.36")
// Annotation processors annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")}Dependency Coordinate Format
Section titled “Dependency Coordinate Format”A JVM dependency coordinate is group:artifact:version, e.g.
"org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0":
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0│ │ │├── Group ID ├── Artifact ID └── Version (like an npm scope) (like a package name)| Part | npm Equivalent | Example |
|---|---|---|
group | npm scope (@scope/) | org.jetbrains.kotlinx |
artifact | package name | kotlinx-coroutines-core |
version | version | 1.9.0 |
In Go terms, it’s like github.com/org/repo v1.9.0 but split into group + artifact.
Dependencies
Section titled “Dependencies”Dependency Configurations Compared
Section titled “Dependency Configurations Compared”| Gradle Configuration | npm Equivalent | Go Equivalent | When to Use |
|---|---|---|---|
implementation | dependencies | require | Library your code calls directly |
api | peerDependencies | N/A | Library you expose in your public API |
testImplementation | devDependencies (test) | _test.go imports | Test-only libraries |
runtimeOnly | - | - | Needed at runtime, not compile (drivers) |
compileOnly | @types/* packages | - | Needed for compilation only |
implementation vs api
Section titled “implementation vs api”This distinction only matters when you’re writing a library module that other
modules depend on. Use implementation when the dependency stays internal, and
api when a type from it appears in your module’s public interface.
// Module: data-layerdependencies { // INTERNAL: only data-layer sees this dependency implementation("com.zaxxer:HikariCP:6.2.1")
// EXPOSED: any module depending on data-layer also gets this api("org.jetbrains.exposed:exposed-core:0.57.0")}In TypeScript terms: implementation is an import used only inside this package,
while api is an import that shows up in your package’s .d.ts types.
Finding Dependencies
Section titled “Finding Dependencies”| Need | Where to Search | npm Equivalent |
|---|---|---|
| Any JVM library | search.maven.org | npmjs.com |
| Kotlin libraries | search.maven.org | npmjs.com |
| Version info | mvnrepository.com | npmjs.com versions tab |
| GitHub-hosted | jitpack.io | npm from GitHub |
Viewing the Dependency Tree
Section titled “Viewing the Dependency Tree”# Show all dependencies (like `npm ls`)./gradlew dependencies
# Show dependencies for a specific configuration./gradlew dependencies --configuration runtimeClasspath
# Show why a specific dependency is included (like `npm why`)./gradlew dependencyInsight --dependency kotlin-stdlibExcluding Transitive Dependencies
Section titled “Excluding Transitive Dependencies”Sometimes a library pulls in something you don’t want:
dependencies { implementation("some.library:core:1.0") { // Exclude a transitive dependency exclude(group = "commons-logging", module = "commons-logging") }}The npm equivalent uses overrides:
{ "overrides": { "some-package": { "unwanted-dep": "npm:empty-package@1.0.0" } }}Forcing a Dependency Version
Section titled “Forcing a Dependency Version”configurations.all { resolutionStrategy { // Force a specific version (like npm overrides) force("com.google.guava:guava:33.4.0-jre")
// Fail on version conflicts instead of silently resolving failOnVersionConflict() }}BOMs (Bill of Materials)
Section titled “BOMs (Bill of Materials)”A BOM locks a set of related dependencies to compatible versions — like having a
curated package.json from a framework author.
dependencies { // Import a BOM -- all Spring Boot dependencies use compatible versions implementation(platform("org.springframework.boot:spring-boot-dependencies:3.4.1"))
// Now you can omit versions -- the BOM provides them implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-data-jpa")}Version Catalogs
Section titled “Version Catalogs”Version catalogs solve the “dependency version scattered everywhere” problem. They’re Gradle’s single source of truth for dependency versions.
Without a catalog, versions are scattered and easy to drift out of sync:
// build.gradle.kts (module A)dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")}
// build.gradle.kts (module B) -- hope this matches!dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")}The Solution: libs.versions.toml
Section titled “The Solution: libs.versions.toml”Create gradle/libs.versions.toml with [versions], [libraries], [bundles], and
[plugins] sections:
[versions]kotlin = "2.1.0"coroutines = "1.9.0"ktor = "3.0.3"spring-boot = "3.4.1"kotest = "5.9.1"exposed = "0.57.0"logback = "1.5.15"postgresql = "42.7.4"koin = "4.0.2"
[libraries]# Format: module = { group = "...", name = "...", version.ref = "..." }kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" }ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" }ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation", version.ref = "ktor" }ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }ktor-server-status-pages = { group = "io.ktor", name = "ktor-server-status-pages", version.ref = "ktor" }ktor-server-test-host = { group = "io.ktor", name = "ktor-server-test-host", version.ref = "ktor" }
spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web", version.ref = "spring-boot" }spring-boot-starter-test = { group = "org.springframework.boot", name = "spring-boot-starter-test", version.ref = "spring-boot" }
kotest-runner-junit5 = { group = "io.kotest", name = "kotest-runner-junit5", version.ref = "kotest" }kotest-assertions-core = { group = "io.kotest", name = "kotest-assertions-core", version.ref = "kotest" }
exposed-core = { group = "org.jetbrains.exposed", name = "exposed-core", version.ref = "exposed" }exposed-jdbc = { group = "org.jetbrains.exposed", name = "exposed-jdbc", version.ref = "exposed" }
logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }postgresql = { group = "org.postgresql", name = "postgresql", version.ref = "postgresql" }
koin-ktor = { group = "io.insert-koin", name = "koin-ktor", version.ref = "koin" }
[bundles]# Group related dependencies into bundlesktor-server = ["ktor-server-core", "ktor-server-netty", "ktor-server-content-negotiation", "ktor-serialization-kotlinx-json", "ktor-server-status-pages"]kotest = ["kotest-runner-junit5", "kotest-assertions-core"]exposed = ["exposed-core", "exposed-jdbc"]
[plugins]kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }ktor = { id = "io.ktor.plugin", version.ref = "ktor" }spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }Then reference them type-safely in build.gradle.kts:
plugins { alias(libs.plugins.kotlin.jvm) // Uses version from catalog alias(libs.plugins.ktor)}
dependencies { // Single library implementation(libs.kotlinx.coroutines.core)
// Bundle (multiple related libs at once) implementation(libs.bundles.ktor.server)
// Test dependencies testImplementation(libs.bundles.kotest)}Catalog vs. npm/Go
Section titled “Catalog vs. npm/Go”| Feature | npm | Go | Gradle Version Catalog |
|---|---|---|---|
| Central version file | package.json | go.mod | libs.versions.toml |
| Auto-completion | No | No | Yes (IDE support) |
| Type-safe references | No | No | Yes (libs.ktor.server.core) |
| Bundles | No | N/A | Yes (group related deps) |
| Shared across modules | npm workspaces | Go workspace | Built-in |
Multi-Module Projects
Section titled “Multi-Module Projects”In npm land you might use npm workspaces or a monorepo tool (Turborepo, Nx). In Go, you might have a Go workspace. In Gradle, multi-module projects are first-class.
Use multi-module when you have:
- Shared library code between services
- A separate API module from implementation
- Independent build/test cycles for different parts
- Architectural boundaries to enforce (module A can’t import module B’s internals)
A typical platform layout:
Directorymy-platform/
- settings.gradle.kts declares all modules
- build.gradle.kts root: shared config for all modules
Directorygradle/
- libs.versions.toml shared version catalog
Directorycommon/ shared library module
- build.gradle.kts
Directorysrc/main/kotlin/
- …
Directoryapi-models/ shared DTOs/models
- build.gradle.kts
Directorysrc/main/kotlin/
- …
Directoryservice-users/ microservice 1
- build.gradle.kts
Directorysrc/main/kotlin/
- …
Directoryservice-orders/ microservice 2
- build.gradle.kts
Directorysrc/main/kotlin/
- …
settings.gradle.kts (Root)
Section titled “settings.gradle.kts (Root)”The root settings.gradle.kts names the project and declares every module:
rootProject.name = "my-platform"
include("common")include("api-models")include("service-users")include("service-orders")The npm workspaces and Go workspace equivalents:
// package.json (npm workspaces){ "workspaces": ["common", "api-models", "service-users", "service-orders"]}go 1.21
use ( ./common ./api-models ./service-users ./service-orders)Root build.gradle.kts (Shared Configuration)
Section titled “Root build.gradle.kts (Shared Configuration)”The root build script holds config shared by all subprojects:
plugins { kotlin("jvm") version "2.1.0" apply false // Declare but don't apply to root}
// Configuration shared by ALL subprojectssubprojects { apply(plugin = "org.jetbrains.kotlin.jvm")
group = "com.example.platform" version = "1.0.0"
repositories { mavenCentral() }
// All modules use JDK 21 configure<org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension> { jvmToolchain(21) }
tasks.withType<Test> { useJUnitPlatform() }}The apply false pattern is like installing a package globally but not importing it —
it makes the plugin available to subprojects without applying it to the root project.
Module build.gradle.kts Files
Section titled “Module build.gradle.kts Files”Each module declares only what it needs; the version came from the root.
plugins { kotlin("jvm") // No version needed -- declared in root}
dependencies { // Only api() here -- this is a library module api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
testImplementation(kotlin("test"))}plugins { kotlin("jvm") application}
dependencies { // Depend on sibling modules implementation(project(":common")) implementation(project(":api-models"))
// Service-specific dependencies implementation("io.ktor:ktor-server-netty:3.0.3")
testImplementation(kotlin("test"))}
application { mainClass.set("com.example.users.AppKt")}Inter-Module Dependencies
Section titled “Inter-Module Dependencies”implementation(project(":common")) is like importing from a workspace package in
npm (import { User } from "@platform/common") or in Go
(import "github.com/example/platform/common").
allprojects vs subprojects
Section titled “allprojects vs subprojects”// Applies to root + all modulesallprojects { repositories { mavenCentral() }}
// Applies to all modules but NOT rootsubprojects { apply(plugin = "org.jetbrains.kotlin.jvm")}Convention Plugins (Advanced)
Section titled “Convention Plugins (Advanced)”For large projects, use buildSrc or a convention plugin to avoid duplicating build
logic:
Directorymy-platform/
DirectorybuildSrc/
- build.gradle.kts
Directorysrc/main/kotlin/
- kotlin-conventions.gradle.kts shared build logic
Directoryservice-users/
- build.gradle.kts just applies the convention
- …
plugins { `kotlin-dsl`}
repositories { mavenCentral()}plugins { kotlin("jvm")}
group = "com.example.platform"version = "1.0.0"
repositories { mavenCentral()}
kotlin { jvmToolchain(21)}
tasks.test { useJUnitPlatform()}plugins { id("kotlin-conventions") // Apply your custom convention application}
dependencies { implementation(project(":common"))}Custom Tasks
Section titled “Custom Tasks”Tasks are Gradle’s unit of work. Every action (build, test, run) is a task, and
you can create your own. The npm equivalent is scripts in package.json; the Go
equivalent is Makefile targets.
List the available tasks like you’d look at package.json scripts:
# List all tasks./gradlew tasks
# List tasks with descriptions./gradlew tasks --allCreating Custom Tasks
Section titled “Creating Custom Tasks”// Simple task -- just prints somethingtasks.register("hello") { group = "custom" // Groups in `./gradlew tasks` output description = "Prints a greeting" doLast { println("Hello from Gradle!") }}
// Task with inputstasks.register("printVersion") { group = "custom" description = "Prints the project version" doLast { println("Version: ${project.version}") }}./gradlew hello# > Task :hello# Hello from Gradle!
./gradlew printVersion# > Task :printVersion# Version: 1.0.0doFirst vs doLast
Section titled “doFirst vs doLast”Code directly inside the task block runs during configuration; doFirst/doLast
blocks run during execution:
tasks.register("lifecycle") { // Runs during CONFIGURATION phase (avoid doing real work here) println("This runs during configuration!")
doFirst { // Runs first during EXECUTION phase println("First action") }
doLast { // Runs last during EXECUTION phase println("Last action") }}Task Dependencies
Section titled “Task Dependencies”tasks.register("generateDocs") { group = "documentation" doLast { println("Generating documentation...") }}
tasks.register("publishDocs") { group = "documentation" dependsOn("generateDocs") // generateDocs runs first doLast { println("Publishing documentation...") }}./gradlew publishDocs# > Task :generateDocs# Generating documentation...# > Task :publishDocs# Publishing documentation...Typed Tasks
Section titled “Typed Tasks”Gradle has built-in task types for common operations. Note the
tasks.register<Copy>(...) syntax — the type goes in angle brackets:
// Copy filestasks.register<Copy>("copyConfigs") { from("src/main/resources") into("build/configs") include("*.yml", "*.properties")}
// Delete filestasks.register<Delete>("cleanLogs") { delete("logs/")}
// Execute a commandtasks.register<Exec>("dockerBuild") { commandLine("docker", "build", "-t", "my-app:latest", ".")}
// Create a ZIP archivetasks.register<Zip>("packageApp") { from("build/libs") archiveFileName.set("my-app-${project.version}.zip") destinationDirectory.set(layout.buildDirectory.dir("dist"))}Hooking Into Existing Tasks
Section titled “Hooking Into Existing Tasks”// Run something before/after teststasks.test { doFirst { println("Setting up test environment...") } doLast { println("Cleaning up test environment...") }}
// Make build depend on your custom tasktasks.build { dependsOn("generateDocs")}Practical Example: Database Migration Task
Section titled “Practical Example: Database Migration Task”tasks.register<Exec>("dbMigrate") { group = "database" description = "Run database migrations" commandLine( "docker", "exec", "kotlin-course-postgres", "psql", "-U", "dev", "-d", "kotlin_course", "-f", "/migrations/migrate.sql" )}
tasks.register<Exec>("dbReset") { group = "database" description = "Reset the database" commandLine( "docker", "exec", "kotlin-course-postgres", "psql", "-U", "dev", "-d", "kotlin_course", "-c", "DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public;" )}The npm equivalent is a couple of scripts entries:
{ "scripts": { "db:migrate": "docker exec postgres psql -U dev -d mydb -f /migrations/migrate.sql", "db:reset": "docker exec postgres psql -U dev -d mydb -c 'DROP SCHEMA...'" }}Build Lifecycle
Section titled “Build Lifecycle”Gradle builds happen in three distinct phases. Understanding this prevents subtle bugs.
flowchart LR I["1. INIT<br/>Which projects?<br/>Read settings.gradle.kts"] --> C["2. CONFIGURATION<br/>Configure all tasks<br/>(even ones you won't run)"] C --> E["3. EXECUTION<br/>Run requested tasks only"]
-
Initialization — reads
settings.gradle.kts, determines which projects (modules) to include, and createsProjectinstances. -
Configuration — executes ALL
build.gradle.ktsfiles, creates and configures ALL tasks (even ones you won’t run), and sets up the task dependency graph. This is where most code inbuild.gradle.ktsruns. -
Execution — runs only the requested tasks and their dependencies. The
doFirst { }anddoLast { }blocks execute here.
// BAD: This runs during configuration, every time, even for `./gradlew help`tasks.register("deploy") { val version = Runtime.getRuntime().exec("git describe --tags") // WRONG! .inputStream.bufferedReader().readText().trim() doLast { println("Deploying version $version") }}
// GOOD: Defer work to execution phasetasks.register("deploy") { doLast { val version = providers.exec { commandLine("git", "describe", "--tags") }.standardOutput.asText.get().trim() println("Deploying version $version") }}Think of it this way: the configuration phase is npm reading package.json to
understand what scripts exist; the execution phase is npm actually running
npm run build.
The Gradle Wrapper
Section titled “The Gradle Wrapper”The Gradle Wrapper (gradlew) is a script that downloads and runs a specific Gradle
version. It ensures everyone on the team uses the same version — which is why you
always use ./gradlew instead of gradle.
These wrapper files should be committed to git:
Directorygradle/
Directorywrapper/
- gradle-wrapper.jar small bootstrap JAR (~60KB)
- gradle-wrapper.properties specifies Gradle version
- gradlew Unix shell script
- gradlew.bat Windows batch script
The version lives in gradle-wrapper.properties:
distributionBase=GRADLE_USER_HOMEdistributionPath=wrapper/distsdistributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zipnetworkTimeout=10000validateDistributionUrl=truezipStoreBase=GRADLE_USER_HOMEzipStorePath=wrapper/distsUpdate the wrapper with the wrapper task, then commit the changes:
# Update to a specific version./gradlew wrapper --gradle-version 8.12
# Update to the latest release./gradlew wrapper --gradle-version latestWhy Not Just Install Gradle Globally?
Section titled “Why Not Just Install Gradle Globally?”| Approach | npm Equivalent | Problem |
|---|---|---|
| Global Gradle | Global npm | Version conflicts between projects |
| Gradle Wrapper | npx / corepack | Consistent versions, no global install |
In Go terms: it’s like if go.mod also pinned the Go compiler version and
automatically downloaded it. Go doesn’t do this, but Gradle does.
Common Commands Cheat Sheet
Section titled “Common Commands Cheat Sheet”Everyday commands:
# Build the project (compile + test + package)./gradlew build
# Run the application (requires `application` plugin)./gradlew run
# Run tests./gradlew test
# Clean build outputs./gradlew clean
# Clean and rebuild./gradlew clean build
# Run a specific test class./gradlew test --tests "com.example.UserServiceTest"
# Run a specific test method./gradlew test --tests "com.example.UserServiceTest.should create user"
# Run tests with console output./gradlew test --info
# Continuous build (re-run on file changes, like nodemon)./gradlew build --continuous# or./gradlew -t buildDependency commands:
# Show all dependencies (like `npm ls`)./gradlew dependencies
# Show runtime dependencies only./gradlew dependencies --configuration runtimeClasspath
# Why is a dependency included? (like `npm why`)./gradlew dependencyInsight --dependency kotlin-stdlib --configuration runtimeClasspath
# Check for dependency updates# (requires the versions plugin: id("com.github.ben-manes.versions"))./gradlew dependencyUpdatesProject info commands:
# List all tasks./gradlew tasks
# List all tasks including hidden ones./gradlew tasks --all
# Show project properties./gradlew properties
# Show subprojects./gradlew projectsMulti-module commands:
# Build everything./gradlew build
# Build a specific module./gradlew :service-users:build
# Run a specific module./gradlew :service-users:run
# Test a specific module./gradlew :common:testPerformance commands:
# Build with build scan (performance analysis)./gradlew build --scan
# Build with parallel execution./gradlew build --parallel
# Build with more memory./gradlew build -Dorg.gradle.jvmargs="-Xmx4g"
# Show build cache stats./gradlew build --build-cacheFull comparison table:
| Task | Gradle | npm | Go |
|---|---|---|---|
| Install deps | ./gradlew build (auto) | npm install | go mod download |
| Build | ./gradlew build | npm run build | go build ./... |
| Run | ./gradlew run | npm start | go run . |
| Test | ./gradlew test | npm test | go test ./... |
| Clean | ./gradlew clean | rm -rf dist/ | go clean |
| Dep tree | ./gradlew dependencies | npm ls | go mod graph |
| Watch mode | ./gradlew -t build | nodemon | N/A (fast compile) |
| Format | ./gradlew ktlintFormat | npx prettier --write . | go fmt ./... |
| Lint | ./gradlew ktlintCheck | npx eslint . | go vet ./... |
| Package | ./gradlew build (JAR) | npm pack | go build -o app |
Plugins Deep Dive
Section titled “Plugins Deep Dive”Plugins add tasks, configurations, and conventions to your build — think of them as
build-system middleware. The npm analogy: tools like eslint, prettier, and jest
that plug into your pipeline.
Applying Plugins
Section titled “Applying Plugins”plugins { // Core plugin (built into Gradle) application `java-library`
// Community plugin with version kotlin("jvm") version "2.1.0"
// Plugin from version catalog alias(libs.plugins.kotlin.jvm)
// Plugin by ID id("io.ktor.plugin") version "3.0.3"}Essential Plugins for Kotlin
Section titled “Essential Plugins for Kotlin”| Plugin | Purpose | npm Equivalent |
|---|---|---|
kotlin("jvm") | Compile Kotlin | typescript |
application | Run as app, create dist | ts-node / start script |
kotlin("plugin.serialization") | kotlinx.serialization | None (built into JSON.parse) |
kotlin("plugin.spring") | Open classes for Spring | None (classes open by default in TS) |
id("org.springframework.boot") | Spring Boot packaging | None (Express has no build step) |
id("io.ktor.plugin") | Ktor fat JAR, Docker | None |
id("com.google.devtools.ksp") | Kotlin Symbol Processing | Babel plugins |
The Shadow Plugin (Fat JARs)
Section titled “The Shadow Plugin (Fat JARs)”A “fat JAR” bundles your app plus all dependencies into a single JAR file — like pkg
or webpack for Node.js, one file to deploy.
plugins { kotlin("jvm") version "2.1.0" id("com.gradleup.shadow") version "8.3.5" application}
application { mainClass.set("com.example.AppKt")}
// Now ./gradlew shadowJar creates build/libs/my-app-all.jar// Run it with: java -jar build/libs/my-app-all.jarThe Ktlint Plugin (Linting)
Section titled “The Ktlint Plugin (Linting)”plugins { id("org.jlleitschuh.gradle.ktlint") version "12.1.2"}
// Now you can:// ./gradlew ktlintCheck -- lint (like `npx eslint .`)// ./gradlew ktlintFormat -- auto-fix (like `npx prettier --write .`)Gradle Properties and Configuration
Section titled “Gradle Properties and Configuration”gradle.properties holds project-level settings — like .env but for Gradle:
# JVM memory for the Gradle daemonorg.gradle.jvmargs=-Xmx2g -XX:+UseParallelGC
# Enable parallel buildorg.gradle.parallel=true
# Enable build cacheorg.gradle.caching=true
# Enable configuration cache (faster repeat builds)org.gradle.configuration-cache=true
# Project properties (accessible in build.gradle.kts)kotlin.code.style=officialmyapp.version=1.0.0Accessing Properties in build.gradle.kts
Section titled “Accessing Properties in build.gradle.kts”// Read from gradle.properties (the delegate name must match the property key)val myVersion: String by project
// Or with findProperty (null-safe)val dbUrl = findProperty("db.url")?.toString() ?: "jdbc:postgresql://localhost:5432/dev"
// Or from command line: ./gradlew build -Penv=productionval env = findProperty("env")?.toString() ?: "development"System Properties vs. Project Properties
Section titled “System Properties vs. Project Properties”Project properties use -P and are read with project.findProperty(...); system
properties use -D and are read with System.getProperty(...):
# Project property (-P): accessible via project.findProperty()./gradlew build -PmyProp=value
# System property (-D): accessible via System.getProperty()./gradlew build -Dmy.system.prop=value
# In build.gradle.kts:# project.findProperty("myProp") -> "value"# System.getProperty("my.system.prop") -> "value"You can also override property defaults on the command line:
./gradlew run -Pdb.url=jdbc:postgresql://prod-host:5432/mydbTroubleshooting
Section titled “Troubleshooting”“Could not resolve dependency” — usually the wrong repository. Make sure the right one is declared:
repositories { mavenCentral() // Most libraries google() // Google/Android libs maven("https://jitpack.io") // GitHub-hosted libs}“Unresolved reference” after adding a dependency — the IDE hasn’t synced. In
IntelliJ, click the elephant icon in the Gradle toolbar (or press Ctrl+Shift+O); from
the terminal, ./gradlew build forces resolution.
Build cache issues — clear progressively:
# Nuclear option: clear everything./gradlew cleanrm -rf ~/.gradle/caches/rm -rf .gradle/
# Less aggressive: just clean build outputs./gradlew clean build --no-build-cache“Daemon was stopped” or out of memory — give the daemon more heap:
# gradle.properties -- increase memoryorg.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryErrorDependency version conflicts — find and inspect them:
# Find the conflict./gradlew dependencies --configuration runtimeClasspath | grep -i "FAILED\|CONFLICT"
# See why a specific version was chosen./gradlew dependencyInsight --dependency problematic-libSlow builds — turn on the performance flags:
org.gradle.parallel=true # Parallel module buildsorg.gradle.caching=true # Reuse outputs from previous buildsorg.gradle.daemon=true # Keep Gradle process warm (default)org.gradle.configuration-cache=true # Cache configuration phase# Generate a build scan for analysis./gradlew build --scanSummary
Section titled “Summary”| Concept | What You Learned |
|---|---|
| Project structure | build.gradle.kts + settings.gradle.kts + gradle.properties |
| Dependencies | implementation vs api vs testImplementation vs runtimeOnly |
| Version catalogs | libs.versions.toml for centralized, type-safe dependency management |
| Multi-module | include() in settings, project(":module") for inter-module deps |
| Custom tasks | tasks.register("name") { doLast { } } with typed tasks |
| Build lifecycle | Init → Configuration → Execution |
| Gradle wrapper | Always use ./gradlew, commit wrapper files |
| Build optimization | Parallel builds, build cache, configuration cache |
Practice
Section titled “Practice”Put the build system to work — wire up a real multi-module project end to end.