skip to content

How would you standardize toolchains, build scans, and cache configuration across many repositories using a Settings plugin?

level: principalimportance: nice to knowfreq 22%

answer

  1. Plugin<Settings> convention plugin
  2. apply develocity + foojay inside it
  3. publish to internal portal OR includeBuild
  4. one-line apply per repo
  5. version bump rolls org-wide change

basics

~10 s

Write a custom Plugin<Settings> convention plugin that applies develocity, the foojay resolver, and centralized config, publish it (or include it via includeBuild), and apply it in every repo's settings.gradle.kts.

solid answer

~40 s

To enforce build-wide standards org-wide, author a **convention Settings plugin** implementing `Plugin<Settings>`. In its `apply(settings)` it applies the upstream Settings plugins (`com.gradle.develocity`, `org.gradle.toolchains.foojay-resolver-convention`), configures `develocity {}` (scan ToS, server, cache push-on-CI), wires `toolchainManagement`/`dependencyResolutionManagement`, and can centralize the version catalog. You distribute it either by **publishing** it to an internal plugin portal/repository and applying it by ID in each repo's `settings.gradle.kts`, or for monorepo-style setups via `includeBuild("build-logic")`. Each consuming repo then has a one-line `plugins { id("com.acme.settings-conventions") version "..." }`. This gives a single governed source of truth for toolchains, scan/cache policy, and repository declarations, and lets you roll changes (e.g., a new JDK baseline) by bumping one version.

code

kotlin · 11 lines
kotlin
// In each consuming repo: settings.gradle.kts
pluginManagement {
    repositories {
        maven(url = uri("https://artifactory.acme.com/plugins"))
        gradlePluginPortal()
    }
}
plugins {
    id("com.acme.settings-conventions") version "2.3.0"
}
rootProject.name = "service-x"

go deeper

for a junior

Likely beyond junior scope; at most know a custom plugin can be applied in settings.

for a middle

Understand you can write a Plugin<Settings> that applies develocity/foojay and apply it per repo.

for a senior

Explain authoring, distribution (publish vs includeBuild), and why it must be settings-level.

for a principal

Own the governance model: versioning, supply-chain pinning, internal JDK mirror, rollout strategy, and coupling trade-offs across the org.

## The goal: governance, not copy-paste Without a convention plugin, every repo duplicates the same `settings.gradle.kts` boilerplate — develocity config, foojay resolver, repositories, catalog. That drifts. A **Settings convention plugin** centralizes it. ## Authoring it A binary plugin targeting `Settings`: ```kotlin class SettingsConventionsPlugin : Plugin<Settings> { override fun apply(settings: Settings) { settings.pluginManager.apply("com.gradle.develocity") settings.pluginManager.apply("org.gradle.toolchains.foojay-resolver-convention") settings.extensions.configure<DevelocityConfiguration>("develocity") { buildScan { termsOfUseUrl = "https://gradle.com/terms-of-service" termsOfUseAgree = "yes" publishing.onlyIf { System.getenv("CI") != null } } } settings.dependencyResolutionManagement { repositories { mavenCentral() } versionCatalogs { create("libs") { from("com.acme:catalog:1.4.0") } } } } } ``` Note the upstream plugins must themselves be **resolvable** — so the consuming repo still needs `pluginManagement.repositories` (or your plugin can't pull `com.gradle.develocity`). A common pattern: publish the convention plugin *and* its required plugin versions pinned via `pluginManagement.plugins {}`. ## Distribution options 1. **Published binary plugin** — build it as a Gradle plugin, publish to an internal Artifactory/portal, and each repo does: ```kotlin // settings.gradle.kts pluginManagement { repositories { maven(url = "https://artifactory.acme.com/plugins") ; gradlePluginPortal() } } plugins { id("com.acme.settings-conventions") version "2.3.0" } ``` 2. **includeBuild("build-logic")** — for a monorepo, keep the convention plugin in an included build and apply it; no publishing needed but it's local to that repo tree. ## Why this is a settings-level concern Toolchain resolvers, build cache endpoints, scan policy, and centralized repositories all live on the `Settings` object and must be set before projects configure. Putting them in a Settings convention plugin is the only clean way to apply them uniformly *and early*. ## Governance benefits - One version bump rolls a new JDK baseline, cache server, or scan policy to every repo. - Security/supply-chain: pin plugin versions and repositories centrally; optionally point the foojay resolver at an internal JDK mirror. - Auditability: scans publish consistently; cache push is gated to CI. ## Trade-offs - A shared plugin is a coupling point — breaking changes affect every consumer, so version and changelog it carefully. - Teams lose some local flexibility; provide escape hatches (properties/feature flags) where reasonable.

  • How do you distribute a custom Settings convention plugin to many repos?
    Publish it as a binary plugin to an internal plugin repository and apply by ID, or for a single repo tree pull it in with includeBuild("build-logic") and apply it in settings.
  • What's a risk of centralizing all settings config in one shared plugin?
    It's a coupling point: a breaking change affects every consumer at once, so it must be versioned and changelogged carefully, ideally with feature-flag escape hatches.
  • Why can't this standardization be done with a Project convention plugin instead?
    Toolchain resolvers, cache endpoints, and scan config live on the Settings object and must be set during initialization, before projects exist — a Project plugin runs too late.

saying these in an interview costs you the question

  • Proposing to copy-paste the same settings boilerplate into every repo instead of a shared plugin.
  • Forgetting the consuming repo still needs pluginManagement repositories to resolve the upstream Settings plugins.

context