skip to content

How would you structure a Maven monorepo so multiple teams share configuration but keep module boundaries clean?

level: principalimportance: should knowfreq 30%

answer

  1. root = aggregator + parent
  2. versions in dependencyManagement/pluginManagement
  3. keep module graph acyclic
  4. split corporate parent vs per-repo aggregator
  5. CI: -pl ... -am incremental

basics

~10 s

Use a root POM that is both aggregator (lists modules) and parent (shared config). Put versions in <dependencyManagement>/<pluginManagement>, keep each module focused with explicit sibling dependencies, and avoid cycles so the reactor stays parallelizable.

solid answer

~50 s

I structure it around one root POM acting as both aggregator and parent. The root carries cross-cutting governance: Java version property, plugin versions in `<pluginManagement>`, and dependency versions in `<dependencyManagement>` (or imports a BOM). Children declare `<parent>` to inherit and list only the *coordinates* of dependencies (versions come from management). Each module is single-purpose with explicit sibling dependencies, and I keep the graph **acyclic** so the reactor can topologically order and parallelize (`-T`). For very large or multi-team setups I often split a published **corporate parent** (governance, rarely changes) from per-repo aggregators (build orchestration, changes often), so teams aren't blocked by each other's config churn. CI uses reactor selection (`-pl ... -am`) to build only changed modules plus their dependencies. The trade-off: a monorepo gives atomic cross-module changes and one consistent toolchain, but the reactor rebuild surface and version coupling must be actively managed.

code

bash · 5 lines
bash
# incremental CI build: changed module + its upstream deps, parallel
mvn -pl payments-service -am -T 1C verify

# full clean build of the whole monorepo
mvn -T 1C clean install

go deeper

for a junior

Can describe a root POM with modules and children that inherit.

for a middle

Centralizes versions in dependencyManagement and keeps modules focused.

for a senior

Reasons about acyclic graphs, BOM vs inheritance, and CI reactor selection trade-offs.

for a principal

Architects multi-team governance: corporate parent vs aggregator split, version strategy, build performance, and migration paths.

## Goals A good Maven monorepo balances: shared/consistent configuration, clean module boundaries, fast incremental builds, and the ability for multiple teams to move independently. ## The standard backbone One root `pom.xml`: - **Aggregator** — `<modules>` lists every module (and nested aggregators for sub-groups). - **Parent** — children point `<parent>` back at it for inheritance. - **Governance lives here:** - `<properties>` for the Java release, encoding, common versions. - `<pluginManagement>` to pin plugin versions/config once. - `<dependencyManagement>` to pin (or import via BOM) all dependency versions so children omit `<version>`. ```xml <dependencyManagement> <dependencies> <dependency> <groupId>com.acme</groupId><artifactId>core</artifactId><version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>3.3.0</version><type>pom</type><scope>import</scope> </dependency> </dependencies> </dependencyManagement> ``` ## Module boundaries - Each module is single-responsibility (e.g. `core`, `service`, `web`). - Cross-module use = a normal dependency by coordinates; the reactor wires it. - **No cycles** — a cyclic module graph breaks the reactor's topological sort and prevents parallelism. Break cycles by extracting shared abstractions into a lower module. ## Scaling to many teams - **Split parent from aggregator** when config and orchestration churn at different rates. A published `corporate-parent` (versioned, slow-moving) provides governance via inheritance/BOM import; each repo or area has its own aggregator listing local modules. This decouples 'who owns versions' from 'what builds together'. - **BOM over inheritance** when you want to share versions without forcing a single parent — consumers `import` the BOM, leaving them free to inherit a different parent. ## Build performance - Parallel reactor: `mvn -T 1C install`. - Incremental CI: `mvn -pl changed -am` (changed + upstream) or `-amd` (changed + downstream consumers). - `-rf` to resume after failures. ## Trade-offs - **Pros:** atomic cross-module refactors, one toolchain, single source of truth for versions, easy local end-to-end builds. - **Cons:** large reactor rebuild surface, tight version coupling, longer clean builds; needs CI selection discipline and a clean acyclic graph to stay fast. ## Anti-patterns - One giant module instead of real boundaries. - Versions duplicated in each child instead of `<dependencyManagement>`. - Relying on `<modules>` order for correctness. - Circular module dependencies.

  • When would you prefer a BOM import over making a single shared parent?
    When modules need different parents or you only want to share versions, not full config. A BOM imported with <scope>import</scope> shares managed versions while leaving inheritance free.
  • Why separate a corporate parent from the aggregator in large orgs?
    Governance config (versions, plugins) changes slowly and centrally, while build orchestration (which modules build together) changes per team. Splitting them decouples release cadence and avoids one team's config churn blocking others.

Think of the root POM as a building's shared utilities and rulebook (parent) plus its directory of tenants (aggregator). Tenants (modules) have their own walls but share wiring and code standards; you don't want plumbing loops (cycles) or every tenant inventing its own voltage (duplicated versions).

saying these in an interview costs you the question

  • Putting dependency versions in every child instead of dependencyManagement.
  • Allowing circular module dependencies.
  • Treating monorepo as 'one big module' with no boundaries.
  • Assuming a monorepo is always free of build-time coupling costs.

context