AEM content package dependencies and build order strategy
When teams scale Adobe Experience Manager implementations across multiple brands, the build pipeline becomes a source of friction that no one plans for. Content packages, the deployable units that bundle JCR nodes, workflows, templates, and OSGi configurations, accumulate as the platform grows. Each new module carries an implicit promise about the order in which it must reach the running instance. When that order is wrong, builds fail in ways that are difficult to diagnose from a stack trace alone.
In Australian engineering teams working on government portals, retail platforms, and media sites, this problem surfaces frequently because projects tend to grow organically. A Sydney-based agency might inherit an AEM installation that started with a single content package and now ships thirty. The package manager does not care about history; it cares about consistency at install time, so the burden of expressing intent falls on the developer writing the pom.xml files.
The deeper issue is that dependencies in AEM are not just about which packages must install first. They also cover which packages must not install together, which overlays should win when two packages claim the same path, and how shared client libraries resolve across modules. A build that succeeds on a developer's laptop in Brisbane can fail on a shared CI runner for reasons unrelated to the code.
This article walks through how to model those dependencies clearly, how to express them in a build tool that respects module order, and how to keep the system honest as the codebase evolves. It draws on patterns discussed by practitioners at events like the past CIRCUIT conferences, where AEM architects have shared approaches to scaling modular installations.
Understanding AEM content package structure
An AEM content package is essentially a zip file with metadata. The vault.xml document describes what the package contains, what filters were used to include content, and how it should be installed. When the package manager processes the package, it walks the filter list in order and applies changes to the JCR tree. If two packages include the same node, the later package wins by default, unless dependency rules redirect the resolution.
For Australian teams shipping multilingual content, this default behaviour is rarely what is wanted. A component structure created by a foundation package should not be silently overwritten by a brand-specific package that targets the same path. Module order in the build pipeline must therefore reflect the layering of concerns: foundation, then configuration, then brand-specific content, then translations. The metadata also records an import and export mode that controls how the package manager handles existing nodes during installation, and treating these modes as defaults rather than deliberate choices will eventually strip customisations made on the author instance.
The vault filter and module boundaries
Filters define the boundaries of a content package. A typical filter root such as /apps/myproject/components restricts the package to a subtree, and any node outside that subtree is invisible to the package manager when it installs the artifact. Filter hygiene matters because overlapping filter roots create ambiguity. Two packages that both claim /apps/myproject/components/widgets will produce different installation outcomes depending on which arrives first.
A useful discipline is to assign each filter root to a single module. If a team owns the components package, no other package should write into /apps/myproject/components. This rule sounds simple, but it is routinely violated when developers copy patterns from older parts of the codebase without understanding the original intent. The vault file also supports excludes, which refine a filter root without removing the root itself, and these introduce a subtle dependency: if module A excludes /content/myproject/cache but module B does not, the two packages can disagree about the state of the cached subtree and leave the repository inconsistent until a later package forces reconciliation.
Defining dependencies in Maven and the FileVault plugin
The filevault-package-maven-plugin is the standard tool for building AEM content packages. It accepts a configuration block in pom.xml where dependency declarations live. Each dependency references another package by its group, name, and version coordinates, mirroring the conventions of Maven artifacts more broadly. When the plugin runs, it bundles these references into the package metadata so that the AEM package manager can resolve them at install time.
The choice between a hard dependency, a soft dependency, and no dependency is a design decision. A hard dependency forces the referenced package to install first, which is appropriate for foundation components. A soft dependency only installs the referenced package if it is not already present, which is useful for optional integrations. Choosing soft when hard is required produces intermittent failures that depend on the state of the target environment.
| Dependency type | Install behaviour | Use case | Risk if misapplied |
|---|---|---|---|
| Hard | Required, installs before this package | Foundation components, shared OSGi configs | Brittle builds that break on missing artifacts |
| Soft | Installed only if absent | Optional integrations, analytics overlays | Hidden coupling that surfaces under load |
| None | Order independent | Truly isolated brand packages | Race conditions on shared filter roots |
| Override | Replaces existing nodes | Hotfixes and patches | Silent loss of customisations |
For teams operating across Australian time zones, the predictable behaviour of hard dependencies is often preferred because it removes one variable from a distributed pipeline. A team in Perth committing late in the day wants the overnight CI job to behave the same way regardless of which packages have been promoted to the shared artifact store.
Resolving circular and conflicting references
Circular dependencies between packages are usually a symptom of poor module boundaries rather than a configuration mistake. If package A depends on package B and package B depends on package A, the package manager has no way to honour both relationships, and the install fails. The fix is almost always to extract the shared concern into a third package that both A and B depend on, leaving the cycle behind.
Conflicts arise when two packages modify the same node in incompatible ways. The package manager does not perform deep merges; it applies changes in sequence. If two packages both define a sling:resourceType for the same component, the one that installs later wins outright. In regulated Australian contexts, such as government services handling personally identifiable information under the Privacy Act 1988, conflicts are particularly dangerous because a configuration that flips a security policy because of an ordering bug can pass unit tests but fail compliance review, which is why dependency declarations should be treated as auditable artifacts stored alongside the source code that produced them.
CI/CD pipeline considerations for module order
Continuous integration for AEM does not end at compiling Java. The packages must be built, validated, and deployed in an order that mirrors what will happen in production. Most teams express this order in their CI configuration, often a Jenkinsfile or GitHub Actions workflow, where each stage handles a specific tier of packages. The Maven reactor enforces the build-time order, while the package manager enforces the runtime order.
The pricing structure of CI runners also affects how teams plan builds. Self-hosted runners are common in Australian enterprises that prefer to keep artifacts in country for sovereignty reasons, and pricing in Australian dollars plus GST pushes teams toward incremental builds that only rebuild packages whose inputs have changed. A reliable pattern is to assign each module a single owning team and surface dependency changes in pull request review, where a change that adds a new hard dependency should require sign-off from the owning team of the referenced package because it imposes a constraint on their release cadence.
Testing and validating build sequences
Even with careful dependency declarations, builds can fail in subtle ways. Unit tests that exercise individual packages are not sufficient; the system needs integration tests that install packages in the proposed order and assert the resulting repository state. Tools like the AEM testing framework and Jackrabbit Oak's fixtures allow teams to spin up an in-memory repository, install packages, and verify that expected nodes are present.
Snapshot testing adds another layer of confidence by capturing the JCR tree after a clean install and comparing it to a checked-in baseline. Australian teams that invest in this practice find that the upfront cost is recouped quickly through faster diagnosis of broken builds, particularly when the team is distributed and synchronous debugging is impractical. Local developer environments should reproduce the same installation order as production, because the CRX Package Manager does not enforce global ordering across workspaces, so a developer who installs packages in an ad hoc order may never see the conflict that surfaces in CI.
Build order in AEM is a contract between modules, and like any contract it needs to be written down, reviewed, and enforced. Teams that treat content package dependencies as first-class engineering artifacts spend less time triaging flaky builds and more time shipping features. Engineers interested in the operational side of AEM development can find recordings and session notes from past events through the event registration archive, which keeps historical materials accessible to practitioners across Australia and beyond.