Chapter 21Lesson 05~250 minutes

Checkpoint Lab — Gradle Multi-Project Builds, Composite Builds, Included Builds, and Build Structure Design

Design a small workspace containing subprojects and one independently addressable included build, document all namespaces, prove targeted execution, then break and repair the composite boundary.

CheckpointNamespacesTargeted tasksBoundary failureRollback

Learning objectives

  • Implement a main multi-project build with :core and :app plus one independent external-lib included build.
  • Document build names, project paths, module coordinates, and task namespaces separately before execution.
  • Predict which projects/builds are affected by targeted tasks and verify the prediction from logs/dry-run/dependency evidence.
  • Prove composite dependency substitution for an external module coordinate without rewriting it as a project dependency.
  • Break the included-build boundary, diagnose the resulting external-module resolution failure, and restore the include.
  • Finish with a production-ready build-structure dossier and bridge to Chapter 22 testing topology.

1. Checkpoint scenario and invariants

You maintain one product build containing :core and :app. The app also consumes dev.academy:external-lib:1.0.0, an independently versioned library. During co-development, the library source lives in a sibling directory and is included as a composite build. The checkpoint deliberately uses one included product build so every namespace is easy to audit.

Invariants: :core and :app are subprojects; external-lib is not. The app keeps module coordinates for the external library. No secret/global cache change is needed. Removing the include must expose the lack of a repository-published copy in this disposable fixture.

2. Preflight and exact assumptions

Assumption Checkpoint value
Gradle 9.7.1 through previously verified Wrapper
Gradle runtime JDK 21
Java target 17
Included build Single-project external-lib, group dev.academy, version 1.0.0
Repositories None required for the external-lib dependency because composite substitution supplies source; no hosted service
Gradle User Home .gradle-user-home inside checkpoint
Build cache Not required; this checkpoint is topology/selection focused
cd gradle-structure-checkpoint
export GRADLE_USER_HOME="$PWD/.gradle-user-home"
./gradlew --version
java -version

3. Create the main build and included library

mkdir -p core/src/main/java/dev/academy/core app/src/main/java/dev/academy/app external-lib/src/main/java/dev/academy/external
cat > settings.gradle.kts <<'EOF'
rootProject.name = "gradle-structure-checkpoint"
include("core", "app")
includeBuild("external-lib")
EOF
cat > build.gradle.kts <<'EOF'
// Root intentionally has no cross-project mutation.
EOF
cat > core/build.gradle.kts <<'EOF'
plugins { `java-library` }
java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }
EOF
cat > app/build.gradle.kts <<'EOF'
plugins { application }
java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }
dependencies {
    implementation(project(":core"))
    implementation("dev.academy:external-lib:1.0.0")
}
application { mainClass = "dev.academy.app.App" }
EOF
cat > external-lib/settings.gradle.kts <<'EOF'
rootProject.name = "external-lib"
EOF
cat > external-lib/build.gradle.kts <<'EOF'
plugins { `java-library` }
group = "dev.academy"
version = "1.0.0"
java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }
EOF
cat > core/src/main/java/dev/academy/core/CoreMessage.java <<'EOF'
package dev.academy.core;
public final class CoreMessage {
    private CoreMessage() {}
    public static String text() { return "core"; }
}
EOF
cat > external-lib/src/main/java/dev/academy/external/ExternalMessage.java <<'EOF'
package dev.academy.external;
public final class ExternalMessage {
    private ExternalMessage() {}
    public static String text() { return "external"; }
}
EOF
cat > app/src/main/java/dev/academy/app/App.java <<'EOF'
package dev.academy.app;
import dev.academy.core.CoreMessage;
import dev.academy.external.ExternalMessage;
public final class App {
    public static void main(String[] args) {
        System.out.println(CoreMessage.text() + ":" + ExternalMessage.text());
    }
}
EOF

4. Write the namespace dossier before running Gradle

Namespace Checkpoint value Meaning
Main build name gradle-structure-checkpoint Settings/root build identity.
Main project paths :, :core, :app Only projects inside the main build.
Included build name/path external-lib Independent build-tree identity; not a main project path.
External module coordinate dev.academy:external-lib:1.0.0 Dependency contract declared by app and eligible for substitution.
Main task paths :core:build, :app:run Tasks in main-build projects.
Included task reference gradle.includedBuild("external-lib").task(":build") Supported cross-build task reference when explicitly needed.

Prediction 1: running :core:build should not build app or external-lib. Prediction 2: running :app:run should require core and the substituted external-lib artifact. Prediction 3: removing includeBuild("external-lib") should make the module unresolved because this fixture defines no repository containing it.

5. Capture baseline project, task, and dependency evidence

mkdir -p evidence
./gradlew projects | tee evidence/projects.txt
./gradlew :core:build --dry-run | tee evidence/core-dry-run.txt
./gradlew :app:run --dry-run | tee evidence/app-dry-run.txt
./gradlew :app:dependencyInsight --dependency external-lib --configuration runtimeClasspath | tee evidence/external-insight.txt

Inspect, do not merely save. projects should show :core and :app under the main build. dependencyInsight should explain that the requested external module is satisfied by the included project. The dry runs reveal task selection without executing product code.

6. Verify targeted execution

./gradlew :core:build | tee evidence/core-build.txt
./gradlew :app:run | tee evidence/app-run.txt

grep -E '^> Task :app:' evidence/core-build.txt && echo 'unexpected app task' || true
grep -E '^> Task :external-lib' evidence/core-build.txt && echo 'unexpected included task' || true
grep -F 'core:external' evidence/app-run.txt

Expected: the core-only invocation contains no app execution. The app run prints core:external; Gradle may execute included-build producer work as required to supply the substituted artifact. Do not rely on exact incidental task ordering as a compatibility contract—use the dependency/output evidence to prove causality.

7. Add one explicit included-build task bridge

// root build.gradle.kts
tasks.register("buildExternal") {
    group = "verification"
    description = "Build external-lib through the composite task API"
    dependsOn(gradle.includedBuild("external-lib").task(":build"))
}
./gradlew buildExternal | tee evidence/build-external.txt

This task bridge is orchestration, not a project dependency. Keep it for cross-build operational workflows only when the coupling is intentional.

8. Intentionally break the include and preserve the failure

Back up settings, then remove only the included-build line:

cp settings.gradle.kts evidence/settings.with-include.gradle.kts
sed '/includeBuild("external-lib")/d' settings.gradle.kts > settings.gradle.kts.tmp
mv settings.gradle.kts.tmp settings.gradle.kts
set +e
./gradlew :app:compileJava > evidence/no-include-failure.txt 2>&1
status=$?
set -e
printf 'no-include status=%s
' "$status"
sed -n '1,200p' evidence/no-include-failure.txt

Expected: dev.academy:external-lib:1.0.0 can no longer be satisfied from the local included build. Because this checkpoint intentionally provides no repository containing that coordinate, compile-classpath resolution fails. That is not a project-path failure; the project hierarchy remains valid. It is an external module resolution failure caused by removing the composite substitution source.

9. Restore the build boundary and independently verify repair

cp evidence/settings.with-include.gradle.kts settings.gradle.kts
./gradlew :app:dependencyInsight --dependency external-lib --configuration runtimeClasspath | tee evidence/external-insight.restored.txt
./gradlew :app:run | tee evidence/app-run.restored.txt
grep -F 'core:external' evidence/app-run.restored.txt

The restored insight should again show local composite substitution and the application should run. The evidence distinguishes three separate things: main project topology never changed; module resolution changed when the include disappeared; task selection follows the restored dependency graph.

10. Verification checklist

  • Wrapper reports Gradle 9.7.1 and the expected JDK runtime.
  • Main projects output lists :core and :app.
  • No build script uses project(":external-lib").
  • App declares dev.academy:external-lib:1.0.0.
  • Included build declares matching group and root project name.
  • dependencyInsight proves local substitution while the include exists.
  • :core:build does not require app.
  • :app:run proves both core and external library are available.
  • Removing the include produces a preserved module-resolution failure.
  • Restoring the include repairs resolution without cache deletion.
  • No normal user cache, credentials, hosted service, or production repository was changed.

11. Cleanup and rollback

# Confirm restored source state before deleting the disposable lab.
grep -F 'includeBuild("external-lib")' settings.gradle.kts
./gradlew :app:run

cd ..
rm -rf gradle-structure-checkpoint

In a real repository, rollback means reverting the settings/dependency change together and rerunning both composite and published-consumer checks. Do not use cache deletion as rollback.

12. Production operating model and bridge to Chapter 22

Chapter 21 adds an explicit build-topology model: subprojects share one project hierarchy and lifecycle; included builds retain independent settings, project/module identity, and release ownership; composite substitution joins those worlds for development without erasing the published coordinate contract. Task namespaces and build-logic boundaries remain explicit.

Chapter 22 now applies the same discipline to test topology: source sets and JVM test suites create their own configurations, tasks, classpaths, reports, and lifecycle gates. The same rule continues—green output only means something when you know exactly which graph and evidence were selected.

Knowledge check

Which project paths exist in the main checkpoint build?

Why does app use dev.academy:external-lib:1.0.0 instead of project()?

What should removing includeBuild("external-lib") change?

Why preserve the no-include failure output?

Does buildExternal make external-lib a subproject?

What does Chapter 22 inherit from this chapter?

Official references and version notes

Version-sensitive behavior was rechecked against current Gradle documentation on 2026-08-24. The mandatory labs use Gradle core plugins, the project Wrapper, JDK 21 as the Gradle runtime, Java 17 as the project target, and an isolated GRADLE_USER_HOME. No hosted CI, commercial repository, or paid service is required.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.