Chapter 21Lesson 03~190 minutes

Gradle Multi-Project Builds, Composite Builds, Included Builds, and Build Structure Design: Configuration, Design Choices, and Tradeoffs

Choose deliberately between one multi-project build, independent included builds, buildSrc, explicit build-logic, project dependencies, and published-module boundaries.

ArchitecturebuildSrcPublished modulesOwnershipCI boundaries

Learning objectives

  • Choose between a monolithic multi-project build and a composite of independently owned builds based on lifecycle and release boundaries.
  • Compare buildSrc and an explicit included build-logic build using visibility, invalidation, and ownership criteria.
  • Choose project dependencies versus published module coordinates without using filesystem proximity as the decision rule.
  • Balance central conventions with team autonomy while keeping executable build policy reviewable.
  • Connect structure choices to CI throughput, cache behavior, interoperability, and upgrade cost.
  • Use a decision table to justify a structure with observable Gradle behavior.

1. Structure is an ownership decision disguised as a directory decision

Gradle can technically compose many shapes, so “can Gradle do it?” is not the deciding question. Ask instead: which changes must be atomic, which artifacts release independently, who owns the settings/plugins/repositories, which CI jobs should run together, and what must remain independently buildable?

Directory proximity is weak evidence. Two directories in one repository can be separate builds; two repositories can be temporarily composed. The build boundary should model lifecycle and ownership rather than the file explorer.

2. One multi-project build versus a composite of independent builds

Dimension Multi-project build Composite/included builds
Settings/project hierarchy One settings model; subprojects share one hierarchy. Each build has its own settings and project hierarchy.
Dependency between components Usually project() within the build. Usually external module coordinates plus substitution for co-development.
Release lifecycle Naturally favors coordinated/atomic source changes. Preserves independent publication/release identities.
Configuration sharing Easy—but hidden cross-project configuration can become coupling. No implicit sharing; common policy must be explicit.
CI targeting Project-task paths inside one build; broad root invocations can configure more projects. Each build can have independent CI plus optional composite integration lanes.
Failure blast radius Root settings/build logic can affect every subproject. A build remains independently executable, though composite integration can still expose cross-build issues.

Do not use a composite merely to avoid understanding a multi-project build, and do not collapse independent products into one build merely because local development feels easier. Composite substitution exists specifically so independent publication contracts can coexist with source-level integration.

3. buildSrc versus build-logic included build

Question buildSrc Explicit build-logic
Setup Automatic special directory. Explicit included build.
Mental model Convenient but has special classpath/build behavior. Closer to an ordinary independent plugin build.
Change invalidation Changes can invalidate broad main-build configuration. Can enable narrower invalidation depending on structure/consumers.
Independent development Possible, but tied specially to the containing build. Can be opened, tested, structured, or later published as a normal build.
Recommendation for growing builds Useful for prototypes/smaller cases. Current Gradle best-practice guidance favors explicit build-logic for scalable build logic.

Neither option belongs on the runtime classpath of your application. Build logic is executable supply-chain code with access to the build process and potentially CI credentials. Review it as code, pin external plugin dependencies, and keep secrets out of it.

4. Tight project dependency versus published-module boundary

Signal Prefer project dependency Prefer published module + optional composite
Release/versioning Always changes/releases with consumer. Has its own version/release cadence.
Source ownership Same team/lifecycle. Independent team or repository contract.
Compatibility Source graph is the contract. Published metadata/artifacts are the contract.
Consumer count Primarily inside the same build. Many external builds/organizations.
Co-development Native inside one build. Use includeBuild() substitution temporarily or persistently.

A composite should not erase publication reality. Maintain at least one clean consumer lane that disables/subtracts local substitution or runs without the include so repository metadata is tested.

5. Central conventions versus team autonomy

Central convention plugins are effective when they encode true organization policy: Java toolchain range, test framework defaults, compiler encoding, or approved repository boundaries. They become harmful when they encode product-specific behavior that independent teams cannot override or understand.

Prefer explicit plugin application in each relevant project. Avoid a root script that traverses every project and mutates it based on path/name. Explicit conventions preserve local readability and make build-logic dependency edges reviewable.

6. CI and cache consequences

A giant build can amplify initialization/configuration work, especially when root scripts inspect all subprojects or when build logic changes globally. Included builds add their own configuration cost and dependency-substitution discovery. Gradle’s composite documentation notes that automatic substitution may require configuring every project in an included build to discover supplied coordinates; explicit substitutions can reduce that discovery cost in some scenarios.

Measure the actual invocation: initialization, configuration, dependency resolution, compilation/tests, packaging. Do not claim “composites are faster” or “monorepos are slower” without evidence. Chapter 24 covers configuration/build caches deeply; Chapter 25 covers daemon/workers/profiling.

7. Interoperability and publication tradeoff

Project dependencies and composite substitution consume Gradle project variants. External Maven consumers only see published Maven-compatible metadata. If a producer customizes Gradle-only capabilities/variants or publication metadata, local composite behavior can differ from repository consumption. Keep published-coordinate tests when Maven interoperability matters.

8. Worked scenario: choose a topology

You have 25 JVM components. Twenty are internal modules changed atomically by one team. Five are SDKs owned by separate teams and released on independent calendars. Developers occasionally need to debug an SDK and application together.

Decision Choice Observable reason
20 internal modules One multi-project build project() edges, one settings hierarchy, atomic CI/release behavior.
Five SDKs Independent builds/modules Each has its own Wrapper/settings/publication/version and independent CI.
Co-development Composite include of selected SDK dependencyInsight shows local substitution while build script retains module coordinates.
Shared JVM conventions Versioned/reviewed build-logic included build or shared plugin strategy Projects explicitly apply policy; no root-wide hidden mutation.
Publication verification CI lane without local substitution Proves repository metadata/artifacts match the external consumer contract.

This hybrid preserves atomicity where it is real and independence where it is real. “One repository” or “one Gradle command” is not the architecture criterion.

9. Upgrade cost and public API compatibility

Every independent build can pin and migrate Gradle separately, which protects autonomy but increases fleet management. One multi-project build has one Wrapper migration but a larger compatibility blast radius. Shared convention plugins should rely on public Gradle APIs; internal API coupling can break all consumers during an upgrade. Record the supported Gradle/JDK range of shared build logic just as you would a library.

10. Decision table

Question If yes, lean toward
Must changes across components be atomic most of the time? Same multi-project build.
Must a component publish/version independently for external consumers? Independent build/module; composite for co-development.
Is shared logic policy rather than product code? Convention plugin/build-logic, not a runtime project dependency.
Does the build require repository-consumer compatibility proof? Keep a no-substitution publication/consumer test.
Are root-wide scripts creating hidden coupling or configuration cost? Move policy to explicit convention plugins and consider structural boundaries.
Would splitting a build only mirror organization charts with no lifecycle independence? Stay multi-project; avoid accidental architecture bureaucracy.

11. Bridge to diagnostics

Architecture choices become concrete only when failures can be located. Lesson 4 deliberately breaks project/module/task namespaces, substitution, and build logic so the diagnostic sequence becomes repeatable rather than intuition-driven.

Knowledge check

Does putting two projects in separate Git repositories require a composite build?

What is the strongest signal for using a published-module boundary?

Why keep a CI lane without composite substitution?

Why can a giant subprojects {} block hurt maintainability?

Is buildSrc always wrong?

What should shared build logic depend on?

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.