skip to content

How would you ensure every module in a large multi-project Gradle build emits a consistent, Central-compliant POM without copy-pasting the pom { } block?

level: seniorimportance: should knowfreq 25%

answer

  1. convention plugin in build-logic / buildSrc
  2. shared license/developer/scm/url once
  3. per-module name/description via lazy providers
  4. avoid subprojects { } cross-config
  5. uniform signing + sources/javadoc

basics

~10 s

Extract the pom { } configuration into a shared convention plugin (buildSrc or build-logic) applied by every module, parameterizing per-module bits like name/description. Each subproject then gets identical license/developer/scm metadata for free.

solid answer

~40 s

Duplicating the full `pom { }` across dozens of modules is unmaintainable and drifts. The standard answer is a **convention plugin**: put a precompiled script plugin (e.g. `build-logic/.../my.publishing-conventions.gradle.kts`) or a `buildSrc` plugin that applies `maven-publish` + `signing`, configures a `MavenPublication` `from(components["java"])`, and sets the shared `pom { }` fields — license, developers, scm, url — once. The per-module-varying bits (`name`, `description`) are wired from the project: `pom.name.set(project.name)`, `pom.description.set(provider { project.description })`, evaluated lazily. Each subproject just does `plugins { id("my.publishing-conventions") }`. This centralizes the metadata contract, makes Central-compliance a one-line opt-in, and lets you also enforce `withSourcesJar()/withJavadocJar()` and signing uniformly. Lazy providers handle the fact that `description`/`version` may be set after the plugin is applied.

code

kotlin · 10 lines
kotlin
// build-logic/src/main/kotlin/acme.publishing-conventions.gradle.kts
plugins { `java-library`; `maven-publish`; signing }
java { withSourcesJar(); withJavadocJar() }
publishing { publications { create<MavenPublication>("maven") {
  from(components["java"])
  pom.name.set(project.name)
  pom.description.set(provider { project.description ?: project.name })
  pom { /* shared url/licenses/developers/scm here */ }
}}}
signing { sign(publishing.publications["maven"]) }

go deeper

for a junior

Aware that shared config can be factored out; specifics optional.

for a middle

Name buildSrc/convention plugin and that per-module fields are parameterized.

for a senior

Design the plugin with lazy providers, uniform signing/sources, and justify over subprojects { }.

for a principal

Treat the POM contract as governance: single review point, CI POM-assertion / staging dry-run, supply-chain provenance across the org.

## The problem In a multi-project build, every publishable module needs the same license/developer/scm/url metadata to pass Central, but only `name`/`description`/coordinates differ. Copy-pasting the `pom { }` block into each `build.gradle.kts` guarantees drift: someone updates the license URL in one module and forgets the rest. ## Convention plugin (preferred) Gradle's recommended composition unit is a **convention plugin** — a precompiled script plugin living in an included `build-logic` build (or `buildSrc`). It encapsulates shared configuration and is applied like any plugin. ```kotlin // build-logic/src/main/kotlin/acme.publishing-conventions.gradle.kts plugins { `java-library` `maven-publish` signing } java { withSourcesJar(); withJavadocJar() } publishing { publications { create<MavenPublication>("maven") { from(components["java"]) // per-module: lazy so values set later are picked up pom.name.set(project.name) pom.description.set(provider { project.description ?: project.name }) pom { url.set("https://github.com/acme/monorepo") licenses { license { name.set("The Apache License, Version 2.0") url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") }} developers { developer { id.set("acme"); name.set("Acme Team"); email.set("[email protected]") }} scm { connection.set("scm:git:git://github.com/acme/monorepo.git") developerConnection.set("scm:git:ssh://github.com/acme/monorepo.git") url.set("https://github.com/acme/monorepo") } } } } } signing { sign(publishing.publications["maven"]) } ``` Each module then: ```kotlin plugins { id("acme.publishing-conventions") } description = "The networking utilities" ``` ## Why lazy matters Properties like `project.description` or `project.version` are frequently assigned *after* the plugin applies. Wrapping them in `provider { ... }` or relying on `Property` lazy evaluation means the value is read at POM-generation time, not at plugin-apply time — avoiding stale/null metadata. ## Alternatives and when - **`allprojects`/`subprojects { }` in the root build** — works but is discouraged: it cross-configures projects, hurts configuration-time performance and configuration-cache compatibility, and couples the root to children. Convention plugins are the modern, isolated approach. - **A shared `extra`/extension** for values (license URL, org name) referenced by the convention plugin keeps even the constants in one place. ## Governance angle Centralizing the POM contract means a single review point for license correctness, SCM URLs, and signing — important for OSS supply-chain hygiene. Add a test (or a Central dry-run / staging publish in CI) that generates each module's POM and asserts the required fields are present so regressions are caught before deploy.

  • Why prefer a convention plugin over subprojects { } in the root build?
    subprojects { } cross-configures children from the root, hurting config-time performance and configuration-cache compatibility, and couples root↔children. Convention plugins are isolated, reusable, and applied explicitly per module.
  • Why wrap project.description in provider { } inside the plugin?
    description is often assigned in the module's build script after the plugin applies. A lazy provider defers reading until POM generation, so the late-set value is used instead of null.
  • How do you keep license/SCM URLs from drifting?
    Define them once in the convention plugin (or a shared constants extension); modules never restate them, so there's a single edit point.

saying these in an interview costs you the question

  • Recommending copy-pasting the pom block per module.
  • Reading project.description eagerly at plugin-apply time (gets null).
  • Defaulting to subprojects { } for cross-config in modern builds.

context