Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Maven parent POM: Circular dependencies

We have a modular project with about 10 artifacts:

parent
 +- artifact1
 +- artifact2
 +- artifact3
 +- ...
 +- artifact10

Furthermore, some of the artifacts have dependencies among each other:

artifact1
 +-> artifact2
 +-> artifact3
 +-> ...
 +-> artifact10

artifact2
 +-> artifact3

artifact4
 +-> artifact3

artifact4
 +-> artifact5

artifact5
 +-> artifact6

Our current setup looks like this:

  • parent is an artifact containing the parent POM.
  • This parent POM defines all necessary dependencies (like Spring, JPA, ...) in .
  • All of our artifacts are defined in , too.
  • Our artifacts reference the parent artifact as - stating the obvious - parent.
  • Only the parent POM defines versions. All other POMs don't.

We use a versioning scheme with three numbers:

<major version>.<minor version>.<patch level>

For example:

0.1.0-SNAPSHOT (a young artifact in development)
0.1.0 (the same artifact once it has been released)
0.1.1 (the same artifact after a hotfix)

The problem:

Once we change the version of an artifact (e.g.: 0.1.0 => 0.1.1), our parent artifact version (12.7.3) needs to be updated because it references the old artifact version (0.1.0). Since we change this reference in the parent POM (0.1.0 => 0.1.1), we need to increase the parent POM's version, too (12.7.3 => 12.7.4). Now, our artifact still references the previous parent version (12.7.3), i.e. we need to update it again... That's circular.

What is the best way to resolve such circular parent-child relationships? We could remove our own dependencies from the parent POM and define their versions in all of the other artifact's POMs but this implies that we would need to update all artifacts once a dependency changed.

EDIT

A simplified directory structure that contains our artifacts:

.
├── [api:0.14.0-SNAPSHOT]
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java ...
│       │   └── webapp ...
│       └── test
├── [dao:1.21.0-SNAPSHOT]
│   ├── pom.xml
│   └── src
│       ├── main ...
│       └── test ...
├── [parent:0.11.0-SNAPSHOT]
│   ├── pom.xml
│   └── src
│       ├── main ...
│       └── test ...
├── [pdf-exporter:0.2.0-SNAPSHOT]
│   ├── pom.xml
│   └── src
│       ├── main ...
│       └── test ...
├── [docx-exporter:0.3.0-SNAPSHOT]
│   ├── pom.xml
│   └── src
│       ├── main ...
│       └── test ...
├── [exporter-commons:0.9.0-SNAPSHOT]
│   ├── pom.xml
│   └── src
│       ├── main ...
│       └── test ...
└── [security:0.6.0-SNAPSHOT]
    ├── pom.xml
    └── src
        ├── main ...
        └── test ...

The artifact directories (in square brackets; along with the artifact version) are independent from each other, i.e. they are only in a common root directory (".") for convenience. Every artifact has its own git repository. "api" is the artifact that gets deployed on an application server. All artifacts reference "parent" like this (during development):

<parent>
    <groupId>com.acme</groupId>
    <artifactId>parent</artifactId>
    <version>0.11.0-SNAPSHOT</version>
</parent>

<artifactId>api</artifactId>
<version>0.14.0-SNAPSHOT</version>

Scenario:

  • exporter-commons gets an update: 0.9.0-SNAPSHOT => 0.9.1-SNAPSHOT.
  • docx-exporter and pdf-exporter reference exporter-commons without version, i.e. no change is necessary.
  • parent needs to be updated to reflect the update of exporter-commons: 0.11.0-SNAPSHOT => 0.12.0-SNAPSHOT.

Problem: api:0.14.0-SNAPSHOT references parent:0.11.0-SNAPSHOT. api:0.14.0-SNAPSHOT is then updated to reference parent:0.12.0-SNAPSHOT. api:0.14.0-SNAPSHOT becomes api:0.15.0-SNAPSHOT. But the pom.xml in parent:0.12.0-SNAPSHOT references api:0.14.0-SNAPSHOT. => Vicious circle.

(Note: The artifact names are made up for simplicity.)

like image 685
Alexander Müller Avatar asked Jan 28 '14 20:01

Alexander Müller


People also ask

How do you solve cyclic dependency in Pom?

One solution is the one you already mentioned, to create another project. Another one would be to just move some classes from B to C or vice versa when this helps. Or sometimes it is correct to merge project B and C to one project if there is no need to have two of them.

How do you avoid circular dependencies?

Avoiding circular dependencies by refactoring The NestJS documentation advises that circular dependencies be avoided where possible. Circular dependencies create tight couplings between the classes or modules involved, which means both classes or modules have to be recompiled every time either of them is changed.

What causes circular dependency?

A circular dependency occurs when two classes depend on each other. For example, class A needs class B, and class B also needs class A. Circular dependencies can arise in Nest between modules and between providers. While circular dependencies should be avoided where possible, you can't always do so.


2 Answers

Suggestion

For simplify dependency configuration use versions ranges.

For example artifact A needs artifact B with version 0.1.0. Configure dependency as range <version>[0.1.0, 0.2.0)</version>.

This mean that A requires B with version greater or equal than 0.1.0 and less than 0.2.0 (so all hotfixes are good for this artifact).

This helps, because when hotfix is released, there is no need to change artifact A dependencies. Just rebuild parent project and hotfixed B will be attached to project A

This technique requires releasing parent project when hotfix is released By parent project I mean somethink like WAR with libraries or EAR, or Distribution Archive with all artifacts inside.

More: 3.4.3. Dependency Version Ranges

like image 174
MariuszS Avatar answered Nov 09 '22 09:11

MariuszS


A major source of confusion in Maven is the fact that parent pom can actually encompasses 2 distinct types of relationships:

  • parent-child relationship:
    • declared once per child inside its <parent> tag
    • inherit plugins, dependencies, properties, versions, etc declared inside pom.xml
  • aggregator-submodule relationship:
    • declared once at top-level pom via <modules> tag
    • goals passed in via cmd line (eg mvn clean install) are passed along to submodules

This distinction is irrelevant if all modules remain on the same version (ie always perform releases from top-level). But as soon as the versions start to fragment (ie release one submodule but not the other) then it becomes necessary to create 2 separate poms for each task.

project/
  parent/
    parent_pom.xml      # declare dependency versions as ranges [0.1.0, 0.2.0)
  children/
    aggregator_pom.xml  # <modules> section lists api/dao/etc
    api/
      pom.xml           # declare parent_pom as parent
    dao/
      pom.xml           # declare parent_pom as parent

Why is this complex structure needed?

Why not just go with MariuszS suggestion of using ranges at top level parent?

Imagine one of the basic components, say the api, is extremely stable. You don't want to rebuild or re-release it if it's avoidable.

Meanwhile, let's say 2 other components that depend on each other like pdf-exporter and docs which you release and/or branch quite often so that you frequently change version ranges: 0.1.x -> 0.2.x -> 0.3.x etc.

Then you would would be forced to modify and release your parent pom to reflect the relationship between pdf-exporter and docs quite often, but you wouldn't necessarily want to release api since it doesn't care about these changes. Hence the need to put parent to the side and ensure releasing it doesn't trigger an unnecessary re-release of api submodule.

like image 40
333kenshin Avatar answered Nov 09 '22 09:11

333kenshin