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.
Learning objectives
-
Implement a main multi-project build with
:coreand:appplus one independentexternal-libincluded 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
projectsoutput lists:coreand:app. - No build script uses
project(":external-lib"). - App declares
dev.academy:external-lib:1.0.0. -
Included build declares matching
groupand root project name. -
dependencyInsightproves local substitution while the include exists. :core:builddoes not require app.-
:app:runproves 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?
:, :core, and :app.
external-lib is a separate included build, not a
main subproject.
Why does app use
dev.academy:external-lib:1.0.0 instead of
project()?
The dependency contract is independently versioned/publishable; composite substitution supplies local source without changing that contract.
What should removing
includeBuild("external-lib") change?
Module resolution for the external-lib coordinate; it should not
remove or rename :core/:app project
paths.
Why preserve the no-include failure output?
It proves the boundary distinction and gives diagnostic evidence instead of hiding the original cause.
Does buildExternal make external-lib a
subproject?
No. It is an orchestration task depending on a task in another build through the included-build API.
What does Chapter 22 inherit from this chapter?
The practice of naming and inspecting graph/lifecycle boundaries explicitly—next for test source sets, suites, tasks, reports, and verification gates.
Official references and version notes
- Gradle Multi-Project Builds — root/subproject structure, project paths, and project dependencies.
-
Gradle Composite Builds (Included Builds)
—
includeBuild(), task interaction, dependency substitution, isolation, limitations, and disabling substitution. - Declaring Dependencies — module dependencies versus project dependencies.
-
Best Practices for Structuring Builds
— current guidance favoring explicit
build-logicincluded builds for scalable build logic. - Convention Plugins — reusable project policy and included-build/buildSrc placement.
-
Settings File Basics
— root project identity and
include(). - Gradle 9.7.1 Release Notes — pinned chapter baseline.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.