🛠️ Testing, Tools & Ecosystem · Advanced

Gradle in Java

Build scripts, tasks, configurations like implementation vs api.

🧩 The mysteryChange one line deep inside a library module, and Gradle rebuilds only what's affected, while the rest of the build says UP-TO-DATE. How does it know?

Builds as code

Gradle builds are scripts (Kotlin or Groovy DSL) that configure tasks such as compileJava, test and jar, wired into a dependency graph. Running test runs compileJava first, because test depends on it.

Configurations

Dependencies go into configurations. **implementation: used internally, hidden from consumers. api** (from the java-library plugin): part of your public API, exposed to consumers. **compileOnly: needed to compile, not shipped. runtimeOnly, and testImplementation** for test code only.

plugins { `java-library` }
 
dependencies {
    api("com.acme:money:2.1.0")
    implementation("com.acme:http:1.4.0")
    testImplementation("com.acme:fakes:1.0.0")
}

A type in your public API

✗ implementation
implementation("com.acme:money:2.1.0")
// public Money total() { ... }

Consumers get 'cannot find symbol': Money isn't on their compile classpath.

✓ api
api("com.acme:money:2.1.0")
// public Money total() { ... }

Types in your public signatures must be visible to consumers.

🤔 Think first

Why so fast the second time?

You run ./gradlew test twice with no changes. The second run finishes almost instantly. Why?

Think about it, then reveal the answer

Gradle tracks each task's inputs and outputs. If nothing changed, the task is UP-TO-DATE and skipped. Change one source file and only the affected tasks run again; the build cache can even reuse outputs from earlier builds.

The wrapper

The Gradle wrapper (gradlew) lets everyone build without installing Gradle. The script downloads the exact version pinned in gradle-wrapper.properties, so laptops and CI behave the same.

🔮 Predict it

Translating from Maven

Maven has the provided scope. Which Gradle configuration plays a similar role?

  1. compileOnly
  2. provided
  3. runtimeOnly
Show the answer

**compileOnly**: available when compiling, not shipped at runtime. provided is a Maven scope; Gradle's Java plugins have no configuration with that name.

💼 In the real world

Why implementation is the default choice

In a big multi-module build, implementation keeps consumers' classpaths small and means they don't need recompiling when your internals change. Reach for api only for types that really appear in your public signatures.

Key takeaways

  1. implementation: hidden from consumers' compile classpath
  2. api (java-library plugin): exposed to consumers
  3. The wrapper (gradlew) pins the Gradle version
  4. Unchanged tasks are skipped as UP-TO-DATE
🤯 Did you know?

Gradle is the official build tool for Android apps, so every Android Studio project comes with a gradlew wrapper.

Practice questions

Your library's public method returns a type from library X, declared with `implementation`. Consumers get 'cannot find symbol' for X's type. Fix?

  1. Declare X with compileOnly
  2. Shade X into your JAR
  3. Declare X with `api` instead of `implementation`
  4. Move X to testImplementation
Check your answer

Declare X with `api` instead of `implementation`. Types that appear in your public signatures must be visible to consumers at compile time, which is what api does.

Running `./gradlew test` a second time with no changes finishes almost instantly. Why?

  1. Tasks whose inputs and outputs haven't changed are UP-TO-DATE and skipped
  2. The JVM caches test results in metaspace
  3. Gradle only runs failed tests
  4. Gradle skips tests on the second run by default
Check your answer

Tasks whose inputs and outputs haven't changed are UP-TO-DATE and skipped. Gradle tracks each task's inputs and outputs. Change a source file and only the affected tasks run again.

Both tools pull in far more libraries than you list. Next: transitive dependencies, and how a library you've never heard of ends up in your app.