How Maven's POM Model Merging and Interpolation Works: Inside the Model-Builder
Maven constructs the effective POM by recursively merging parent POMs, imported BOMs, and the super-POM via MavenModelMerger, then resolves ${...} placeholders through StringVisitorModelInterpolator using a hierarchical chain of value sources.
The apache/maven repository contains the model-builder module, which orchestrates the transformation of raw pom.xml files into an effective, ready-to-use model. Understanding POM model merging and interpolation is essential for debugging dependency conflicts, inheritance issues, and property resolution in multi-module builds.
Phase 1: Model Merging and Inheritance Assembly
Maven builds the effective POM by constructing an inheritance tree and merging models from multiple sources. This phase occurs before any property substitution takes place.
Locating the Parent and Building the Inheritance Chain
The process begins in DefaultModelBuilder (located in compat/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java), which resolves the <parent> element coordinates and loads the parent POM from the repository or file system.
The DefaultInheritanceAssembler recursively loads all ancestors until reaching the built-in super-POM—a default POM that defines standard values for plugins, reporting, and directory structures. The DefaultSuperPomProvider (see compat/maven-model-builder/src/main/java/org/apache/maven/model/superpom/DefaultSuperPomProvider.java) supplies this ultimate parent, ensuring every POM inherits sensible defaults even without an explicit parent declaration.
Merging Rules and Algorithm
The actual merging logic resides in MavenModelMerger (see compat/maven-model-builder/src/main/java/org/apache/maven/model/merge/MavenModelMerger.java). This class implements the POM specification's inheritance rules:
- Scalar values: Child overrides parent (e.g.,
<version>,<name>) - Lists: Concatenated (e.g.,
<dependencies>,<plugins>) - Maps: Merged (e.g.,
<properties>) - Management sections:
dependencyManagementandpluginManagementare injected before the child's own declarations
Helper injectors like DefaultDependencyManagementInjector, DefaultPluginManagementInjector, and DefaultProfileInjector modularize the merging of specific sections.
Importing BOMs and Dependency Management
When a POM contains <dependencyManagement> with <scope>import</scope> dependencies (BOMs), the MavenModelMerger treats these imported models as if they were part of the parent's dependency management section. This allows projects to inherit version constraints from external Bill of Materials POMs without direct parent-child relationships.
Phase 2: Model Interpolation and Property Resolution
After merging, the model contains unresolved placeholders like ${project.version} or ${env.JAVA_HOME}. Maven resolves these through a two-step interpolation process.
Pre-Processing and Value Sources
Before interpolation begins, the UrlNormalizingPostProcessor cleans up file: and http: URLs, while MavenBuildTimestamp supplies the ${maven.build.timestamp} property.
The ModelInterpolator (entry point in compat/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/ModelInterpolator.java) constructs a value source chain that determines lookup priority:
- Object-based sources:
ObjectBasedValueSourceexposes model fields like${project.version},${project.artifactId}, and${project.build.directory} - Prefixed sources:
PrefixedObjectValueSourcehandles${env.*},${settings.*}, and${java.*}lookups - System properties: JVM system properties like
${user.home}and${java.version} - User-defined properties: Values from the
<properties>section of the effective POM
The Interpolation Engine
The StringVisitorModelInterpolator (see compat/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/StringVisitorModelInterpolator.java) traverses the model tree and applies a regular expression search (\\$\\{([^}]+)\\}) to locate placeholders in string values.
For each match, the interpolator consults the value-source chain in order. If a placeholder cannot be resolved, Maven records a warning or fails if the value is required for build execution. This algorithm runs recursively until all ${...} tokens are replaced with concrete values.
Practical Implementation Examples
Building an Effective Model Programmatically
You can trigger the full merge and interpolation pipeline programmatically:
ModelBuilder builder = new DefaultModelBuilderFactory().newInstance();
ModelBuildingRequest request = new DefaultModelBuildingRequest()
.setPomFile( new File("my-module/pom.xml") )
.setModelResolver( new DefaultModelResolver( repoSystem, session ) )
.setProcessingModelInterpolator( true )
.setProcessingModelValidation( true );
ModelBuildingResult result = builder.build( request );
Model effective = result.getEffectiveModel();
System.out.println( "Effective version: " + effective.getVersion() );
System.out.println( "Resolved property: " + effective.getProperties().getProperty("my.prop") );
This code produces a fully resolved Model object with all parent inheritance and property interpolation applied.
Manual Interpolation of Specific Strings
To interpolate a string against an already-merged model:
Model model = …; // already merged
ModelInterpolator interpolator = new StringVisitorModelInterpolator();
String raw = "${project.groupId}:${project.artifactId}:${project.version}";
String resolved = interpolator.interpolate( raw, model, Collections.emptyMap() );
System.out.println( resolved ); // e.g. "org.example:my-app:1.2.3"
The interpolate method uses the same value-source chain Maven applies internally during the build process.
Inspecting Parent-Child Property Overrides
When debugging inheritance, you can compare values before and after merging:
Model child = …; // loaded from child pom.xml
Model parent = …; // loaded from parent pom.xml
System.out.println( "Child property: " + child.getProperties().getProperty("my.prop") );
System.out.println( "Parent property: " + parent.getProperties().getProperty("my.prop") );
After the merge, the child's value wins, and any ${my.prop} placeholder in the child will resolve to the child's definition, not the parent's.
Summary
- Model merging combines parent POMs, imported BOMs, and the super-POM using
MavenModelMerger, with child values overriding scalars and lists being concatenated. - Inheritance assembly is handled by
DefaultInheritanceAssemblerandDefaultModelBuilder, which recursively load the POM hierarchy starting from the specified parent and ending with the super-POM. - Interpolation occurs in two phases: pre-processing (URL normalization, timestamp injection) and resolution via
StringVisitorModelInterpolator, which uses a regex pattern to find and replace${...}placeholders. - Value resolution follows a strict hierarchy: object-based sources (
${project.*}), prefixed sources (${env.*},${settings.*}), system properties, and user-defined POM properties. - Key source files include
MavenModelMerger.java,DefaultInheritanceAssembler.java,ModelInterpolator.java, andStringVisitorModelInterpolator.javawithin thecompat/maven-model-buildermodule.
Frequently Asked Questions
What is the difference between model merging and interpolation in Maven?
Model merging combines multiple POM files (parent, child, imported BOMs, super-POM) into a single effective model by aggregating lists, overriding scalars, and injecting management sections. Model interpolation is the subsequent phase that replaces ${...} placeholders with actual values from properties, environment variables, and the project object itself. Merging occurs first to assemble the complete model, then interpolation resolves dynamic values within that merged structure.
How does Maven resolve ${project.version} and other object-based properties?
Maven uses ObjectBasedValueSource (located in compat/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/ObjectBasedValueSource.java) to expose fields of the Model object as interpolation sources. When the StringVisitorModelInterpolator encounters ${project.version}, it accesses the getVersion() method on the model instance. This works for any getter method on the model, including ${project.build.directory} and ${project.artifactId}.
Can I programmatically build an effective POM without running a full Maven build?
Yes. The DefaultModelBuilder API allows you to construct an effective POM programmatically by creating a ModelBuildingRequest, setting the POM file, and calling builder.build(request). This triggers the same merge and interpolation logic used during mvn execution, returning a fully resolved Model object without executing lifecycle phases or plugin goals.
What happens when a property is defined in both parent and child POMs?
During the merge phase, MavenModelMerger combines the properties maps using a merge strategy where child values take precedence over parent values. After merging, if the child defines <my.prop>child-value</my.prop> and the parent defines <my.prop>parent-value</my.prop>, the effective model contains the child's value. Consequently, any reference to ${my.prop} in either POM will resolve to "child-value" during interpolation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →