It was a Tuesday afternoon, 2:00 PM, exactly two days before a massive global product launch. The marketing team had spent the last three months meticulously crafting over 10,000 pages of localized content across 15 different language roots in Adobe Experience Manager. The deployment pipeline was triggered for a routine hotfix—a simple CSS update and a minor Sling Model adjustment. Thirty minutes later, the pipeline completed successfully. But in the production environment, panic erupted. The pages were gone. The entire /content/acme/us/en and /content/acme/fr/fr trees had simply vanished. There were no errors in the deployment logs. Cloud Manager reported a successful build and rollout.
What caused this catastrophic event? A single line in a filter.xml file. A developer had added a new configuration under the ui.content module to seed a few test pages. In doing so, they left the filter mode as mode="replace" instead of mode="merge" on the /content/acme root path. Jackrabbit FileVault, the engine that processes AEM deployments, did exactly what it was told: it replaced the entire repository tree in production with the exact contents of the deployment package. Since the deployment package only contained the newly seeded test pages and not the 10,000 author-created pages, FileVault meticulously deleted all 10,000 pages to ensure the repository matched the package state perfectly.
This is the most expensive mistake in AEM project setup. It is a harsh reality that your AEM project's build and deployment architecture is not merely a delivery mechanism—it is the structural foundation of your application's integrity. Most development teams fail to grasp this because they treat Maven and Jackrabbit FileVault as boilerplate artifacts. They ignore them until a build fails or content disappears. In enterprise production environments, especially on AEM as a Cloud Service (AEMaaCS), a misconfigured filter.xml, an improper module dependency, or a violation of API boundaries doesn't just cause a failed build; it wipes out content, corrupts JCR indexes, and permanently breaks CI/CD pipelines.
This comprehensive guide will systematically deconstruct the entire AEM build and deployment ecosystem. We are diving deep into the absolute core of how AEM code is structured, packaged, and deployed. We will cover:
- A full module-by-module breakdown of the AEM Maven Project Archetype with exhaustive directory structures and architectural explanations.
- The critical enforcement of immutable versus mutable content boundaries in AEM as a Cloud Service.
- A deep dive into Jackrabbit FileVault, the engine of AEM content packaging.
- The exact mechanics of
filter.xmlmodes—replace,merge, andupdate—and how to prevent catastrophic content wipes with complete real-world examples. - The migration from the legacy
content-package-maven-pluginto the modernfilevault-package-maven-pluginand what it means for your builds. - Embedding OSGi bundles and third-party dependencies using the
allcontainer package, complete with Maven POM examples. - The ruthless validation checks of the
aemanalyser-maven-plugin, including common error messages and how to fix them. - Common Cloud Manager build failures, from Java version mismatches to strict API boundary violations and banned imports.
- Expanding the archetype by creating custom modules, such as a
core.testingmodule and shared libraries. - Running the archetype generation command with an exhaustive explanation of every critical flag.
Before we begin disassembling the archetype, I highly recommend reviewing the AEM Backend Development Complete Guide for core Java and OSGi patterns, the AEM Cloud Service Complete Guide to understand the infrastructure context, the AEM Local Development Setup Complete Guide for environment parity, and the OSGi in AEM Complete Guide for deeper insights into the runtime container where your Java code executes.
What the archetype generates and why
The AEM Project Archetype generates a multi-module Apache Maven project that adheres strictly to Adobe's enterprise best practices. This generation is not just about organizing code arbitrarily; it is about separating concerns to satisfy the rigid constraints of the Apache Sling framework, the OSGi container, the Jackrabbit Oak repository, and the automated Cloud Manager CI/CD pipeline.
Every module has a distinct responsibility. Mixing these responsibilities (for instance, putting OSGi configurations in the Java core module, or mutable content in the immutable apps module) will result in immediate deployment failures in Cloud Service.
Here is a full breakdown of the archetype output, represented as a directory tree diagram:
acme-aem-project
├── pom.xml (Root Parent POM)
├── all
│ └── pom.xml (Builds the single all-in-one deployable container .zip)
├── core
│ ├── pom.xml (Builds the OSGi .jar bundle containing all Java code)
│ └── src
│ ├── main/java/com/acme/aem/core
│ │ ├── models (Sling Models)
│ │ ├── servlets (Sling Servlets)
│ │ ├── filters (Sling Filters)
│ │ └── services (OSGi Services)
│ └── test/java (JUnit Tests and AEM Mocks)
├── ui.apps
│ ├── pom.xml (Builds the immutable application package .zip)
│ └── src/main/content/jcr_root
│ ├── apps/acme
│ │ ├── components (HTL scripts, Dialogs)
│ │ ├── clientlibs (CSS/JS definitions)
│ │ └── i18n (Dictionaries)
│ └── oak:index (Custom Jackrabbit Oak Indexes)
├── ui.apps.structure
│ ├── pom.xml (Validates JCR structural roots)
│ └── src/main/content/META-INF/vault/filter.xml (Defines root nodes)
├── ui.config
│ ├── pom.xml (Builds OSGi configurations .zip)
│ └── src/main/content/jcr_root/apps/acme/osgiconfig
│ ├── config (Global OSGi configs)
│ ├── config.author (Author-only OSGi configs)
│ └── config.publish (Publish-only OSGi configs)
├── ui.content
│ ├── pom.xml (Builds mutable content package .zip)
│ └── src/main/content/jcr_root
│ ├── conf/acme (Editable Templates, Policies, Context-Aware Configs)
│ ├── content/acme (Initial page structures)
│ └── content/dam/acme (Initial assets)
├── ui.frontend
│ ├── pom.xml (Frontend build bridge)
│ ├── package.json (NPM dependencies)
│ └── src (TypeScript, SCSS, Webpack/Vite config)
├── ui.tests
│ └── pom.xml (Selenium/Cypress UI integration tests)
├── it.tests
│ └── pom.xml (Server-side integration tests)
└── dispatcher
├── pom.xml (Packages the Dispatcher configuration)
└── src
├── conf.d (Apache Virtual Hosts)
└── conf.dispatcher.d (Dispatcher farms, filters, caches)Module Responsibilities
core: This is the pure Java backend module. It compiles into an OSGi bundle (a.jarfile). It contains everything related to backend execution: Sling Models, OSGi Services, Sling Servlets, Event Listeners, Schedulers, and Workflow Processes. It is strictly limited to Java code and unit tests. You must never place HTML, XML, or frontend assets here.ui.apps: This module contains the immutable application content. It compiles into a FileVault package (.zip). It holds the building blocks of your application: components, HTL (.html) scripts, Touch UI Dialogs (_cq_dialog.xml), ClientLibrary definitions, and Oak index definitions (oak:index). Everything in this module lives under/appsor/oak:indexin the JCR. In AEM as a Cloud Service, this entire module is deployed to a read-only filesystem.ui.apps.structure: This module is frequently misunderstood. It does not contain any application code or functional components. Its sole purpose is to define the foundational JCR root structural nodes (such as/apps,/apps/acme,/content/dam,/conf) so that thefilevault-package-maven-plugincan validate that yourui.appsandui.contentpackages are not attempting to illegally modify root node properties that belong to the underlying AEM system.ui.content: This is the mutable structural content module. It includes Editable Templates, Context-Aware configurations (under/conf), initial content structures (under/content), and taxonomy tags (under/content/cq:tags). Crucially, this package contains data that authors can also modify in production. Therefore, deployments ofui.contentmust be managed with extreme care (usingmergeorupdatefilters) to avoid the catastrophic scenario outlined in the introduction.ui.config: This module holds all OSGi configurations, neatly separated by runmodes (e.g.,config.author,config.publish,config.author.dev). Historically, developers placed these configurations directly insideui.apps. Abstracting them intoui.configallows Cloud Manager to apply environment-specific configurations dynamically during the deployment phase without requiring a full code rebuild.ui.frontend: A fully decoupled frontend build environment, typically utilizing Webpack, Vite, or Parcel. It compiles source SCSS and TypeScript into optimized CSS and JavaScript, which is then automatically copied into theui.appsClientLibs directories during the Maven build process via thefrontend-maven-plugin.dispatcher: This module manages the Apache HTTP Web Server and AEM Dispatcher configurations. It includes virtual hosts, rewrite rules, cache invalidation rules, and farm configurations. Cloud Manager validates these configurations using the Dispatcher Validator tool before deployment.all: The master aggregator module. It produces a single, all-encompassing container.zippackage that embeds the OSGi bundle (core) and all content packages (ui.apps,ui.content,ui.config). Cloud Manager explicitly requires this single artifact to orchestrate the deployment to AEM.
The Immutable vs. Mutable Paradigm (The Cloud Service Enforcer)
If you are transitioning from older, on-premise or Managed Services versions of AEM (like AEM 6.4 or 6.5) to AEM as a Cloud Service, the concept of immutability is the most disruptive paradigm shift you will face. In older versions, /apps was just another path in the JCR. You could log into CRX/DE in production, navigate to a component's HTL script, modify it, and save it.
In AEM as a Cloud Service, the repository is physically and conceptually separated at the NodeStore level:
- Immutable Content (
/apps,/libs,/oak:index): This content is permanently baked into the Docker container image at build time by the Cloud Manager CI/CD pipeline. When the AEM Author or Publish pods spin up in the Kubernetes cluster, this filesystem is literally read-only. You cannot change a script, add a new component, or modify an OSGi configuration in CRX/DE. If you try, the system will block the write operation. Furthermore, if yourui.contentpackage (which is meant for mutable content) attempts to write to/apps, the Cloud Manager build pipeline will instantly fail. - Mutable Content (
/content,/conf,/var,/home,/etc): This content resides on a separate, shared Document NodeStore (typically backed by high-availability cloud storage like MongoDB Atlas or Azure CosmosDB) or a highly-available TarMK segment store. This is where all authoring activity happens. Pages, assets, users, and templates are stored here. This content persists across deployments and is completely isolated from the Docker image restarts.
The archetype intentionally separates ui.apps and ui.content to rigorously respect this boundary. The pipeline treats the ui.apps package as the source of truth for the immutable image, while the ui.content package is applied against the mutable repository.
Jackrabbit FileVault: The Engine of AEM Deployments
To master AEM deployments, you must master Jackrabbit FileVault. FileVault is the translation and mapping layer between the hierarchical JCR (Java Content Repository) tree and the standard flat filesystem on your local machine.
When you execute a Maven build, the filevault-package-maven-plugin traverses your source code and serializes JCR nodes into physical XML files (typically named .content.xml). A FileVault package is essentially a standard ZIP archive, but it requires a very specific internal structure to be understood by the AEM Package Manager.
Here is the anatomy of a compiled FileVault package (ui.apps-1.0.0.zip):
my-project.ui.apps-1.0.0.zip
├── jcr_root/
│ ├── apps/
│ │ └── acme/
│ │ ├── components/
│ │ │ └── button/
│ │ │ ├── .content.xml (Defines the jcr:primaryType and properties)
│ │ │ └── button.html (The actual HTL file)
│ └── oak:index/
│ └── acmeCustomIndex/
│ └── .content.xml
└── META-INF/
└── vault/
├── filter.xml (The absolute most important file in the package)
├── properties.xml (Package metadata: name, version, dependencies)
├── config.xml
└── nodetypes.cnd (Custom Node Type definitions)The jcr_root folder mirrors the exact repository structure where the files will be placed. However, the absolute brain of the deployment process resides in META-INF/vault/filter.xml. This file dictates exactly what actions FileVault will take when the package is extracted into the AEM repository.
The filter.xml deep dive: replace vs merge vs update
The filter.xml (Workspace Filter) dictates the exact behavior of the deployment for the paths defined within your package. A misconfigured filter is the number one cause of lost content, broken environments, and deployment rollbacks in AEM.
Every <filter> entry defines a root path. Crucially, every filter can have a mode attribute. If the mode attribute is omitted, Jackrabbit FileVault defaults to mode="replace".
The Three Modes Explained
1. mode="replace" (The Default, The Destroyer)
<workspaceFilter version="1.0">
<filter root="/apps/acme"/>
</workspaceFilter>What it does: It takes the path in the deployment package and forcibly replaces the corresponding path in the target AEM JCR.
The Critical Danger: If the production AEM JCR currently contains /apps/acme/clientlibs and /apps/acme/components, and your deployment package only contains /apps/acme/components, Jackrabbit FileVault will DELETE the entire /apps/acme/clientlibs folder from production because it does not exist in the package. The replace mode literally means "make the AEM repository look exactly like this package path, removing any extraneous nodes." For code paths like /apps, this is exactly what you want. For content paths like /content, this is disastrous.
2. mode="merge" (The Additive Approach)
<workspaceFilter version="1.0">
<filter root="/content/acme" mode="merge"/>
</workspaceFilter>What it does: It adds new nodes and properties from the package into the JCR, but it absolutely never modifies or deletes existing nodes.
Usage: This is the gold standard for deploying initial content structures or scaffolding pages. If an author has already created or modified /content/acme/us/en, the merge mode will leave it completely untouched, even if the deployment package has a different version of that page.
3. mode="update" (The Safe Modifier)
<workspaceFilter version="1.0">
<filter root="/conf/acme" mode="update"/>
</workspaceFilter>What it does: It adds new nodes and updates existing properties based on the package contents, but crucially, it does not delete nodes in the JCR that are missing from the package. Usage: This is ideal for things like Context-Aware configurations or policy updates where developers need to push updates to existing configurations without wiping out newly created Editable Templates that authors might have built in production.
Complete filter.xml Scenarios
To fully grasp these concepts, let's look at complete, production-ready filter.xml examples for different scenarios.
Scenario A: The Safe Content Deployment (ui.content)
In the ui.content module, you are deploying to mutable areas like /conf and /content. You must ensure developer configurations are updated, while author content is protected. We use surgical include/exclude rules.
<?xml version="1.0" encoding="UTF-8"?>
<workspaceFilter version="1.0">
<!--
Deploy Context Aware configs. We use 'update' so we don't delete
anything authors created, but we can push developer updates.
-->
<filter root="/conf/acme" mode="update">
<!-- CRITICAL: NEVER touch templates or policies created by authors in production -->
<exclude pattern="/conf/acme/settings/wcm/templates(/.*)?"/>
<exclude pattern="/conf/acme/settings/wcm/policies(/.*)?"/>
</filter>
<!--
Deploy initial dam structure. We use 'merge' so we never overwrite
assets uploaded by users, only create folders if they don't exist.
-->
<filter root="/content/dam/acme" mode="merge"/>
<!--
Deploy site structure. 'merge' ensures we don't wipe out author pages.
-->
<filter root="/content/acme" mode="merge"/>
<!--
Tags can be updated by developers (e.g. adding new taxonomy),
but we don't want to delete tags authors created.
-->
<filter root="/content/cq:tags/acme" mode="update"/>
</workspaceFilter>Scenario B: The Initial Content Seeding (One-Time Execution)
Sometimes, on a brand new environment, you want to aggressively push content just once. You might create a separate package, let's call it ui.content.seed. Here, you might actually want replace, but you isolate it to a package that is NOT run on every pipeline execution.
<?xml version="1.0" encoding="UTF-8"?>
<workspaceFilter version="1.0">
<!-- Aggressively replace the content tree to ensure exact parity with dev -->
<filter root="/content/acme/us/en" mode="replace"/>
<!-- Push baseline experience fragments -->
<filter root="/content/experience-fragments/acme" mode="replace"/>
</workspaceFilter>Note: Never include a package with these filters in a standard Cloud Manager pipeline without explicit approval, as it will destroy production data.
Scenario C: Code-Only Deployment (ui.apps)
For the ui.apps package, replace is exactly what we want. We want the /apps folder to be an exact mirror of the source control repository, dropping any legacy or manually created files.
<?xml version="1.0" encoding="UTF-8"?>
<workspaceFilter version="1.0">
<!-- Replace entire component, clientlib, and i18n tree -->
<filter root="/apps/acme"/>
<!-- Replace our custom index definitions -->
<filter root="/oak:index/acmeCustomIndex"/>
</workspaceFilter>The content-package-maven-plugin vs filevault-package-maven-plugin (The Migration)
If you are maintaining an older AEM codebase, you will likely see the content-package-maven-plugin provided by com.day.jcr.vault. In modern AEM development, this has been split and largely superseded by the filevault-package-maven-plugin provided by org.apache.jackrabbit. Understanding this migration is crucial for passing Cloud Manager validation.
Why the Shift?
Historically, the Day/Adobe plugin handled both the building (zipping up the files, validating the filter.xml) and the deployment (pushing the ZIP to AEM via HTTP POST) of the package.
As the Jackrabbit FileVault project matured under the Apache Software Foundation, the responsibilities were separated to adhere to the Unix philosophy of doing one thing well:
filevault-package-maven-plugin(Apache): This plugin is now the industry standard for building and validating the AEM package during thepackageMaven phase. It performs rigorous checks againstui.apps.structure, validates node types, and ensures no invalid XML exists.content-package-maven-plugin(Adobe): This plugin is now strictly relegated to the deployment phase. It is used in profiles likeautoInstallPackageto push the compiled ZIP file to your local AEM instance during theinstallphase.
How to Fix Legacy Projects
If your legacy project fails in Cloud Manager with errors about invalid structural nodes or missing properties, you must migrate your ui.apps POM to the modern architecture.
Legacy approach (Anti-pattern):
<!-- DO NOT USE THIS FOR BUILDING IN MODERN AEM -->
<plugin>
<groupId>com.day.jcr.vault</groupId>
<artifactId>content-package-maven-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<group>acme</group>
<filterSource>src/main/content/META-INF/vault/filter.xml</filterSource>
</configuration>
</plugin>Modern approach (Best Practice):
<!-- USE THIS FOR BUILDING -->
<plugin>
<groupId>org.apache.jackrabbit</groupId>
<artifactId>filevault-package-maven-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<group>acme</group>
<packageType>application</packageType>
<!-- This links to the ui.apps.structure module to validate root paths -->
<repositoryStructurePackages>
<repositoryStructurePackage>
<groupId>com.acme.aem</groupId>
<artifactId>acme.ui.apps.structure</artifactId>
</repositoryStructurePackage>
</repositoryStructurePackages>
</configuration>
</plugin>
<!-- USE THIS FOR LOCAL DEPLOYMENT ONLY via a profile -->
<plugin>
<groupId>com.day.jcr.vault</groupId>
<artifactId>content-package-maven-plugin</artifactId>
<configuration>
<targetURL>http://localhost:4502/crx/packmgr/service.jsp</targetURL>
<failOnError>true</failOnError>
</configuration>
</plugin>Embedding OSGi bundles: the 'all' package explained
In the past, developers would deploy core.jar, ui.apps.zip, and ui.content.zip individually using the AEM Package Manager or discrete Maven profiles. Cloud Manager fundamentally changed this paradigm. The deployment pipeline expects exactly one build artifact to deploy.
The all module exists solely as an aggregator. It produces a single, master container package that embeds your OSGi bundle and all content packages.
Managing Third-Party Dependencies
When you rely on third-party libraries (e.g., Apache Commons Math, AWS Java SDK, Google GSON, Snowflake JDBC) that are not provided natively by the AEM SDK, you must deploy them to the AEM instance.
The most common, and most disastrous, mistake developers make is using the maven-bundle-plugin's <Embed-Dependency> instruction inside their core bundle POM. Do not do this. Embedding raw JARs inside your custom OSGi bundle inflates the bundle size enormously, creates complex ClassLoader nightmares, and makes diagnosing dependency conflicts (the dreaded "Diamond Dependency" problem) incredibly difficult.
The correct, Staff-level approach is to deploy third-party libraries as separate, standalone OSGi bundles by embedding them within the all package.
The Maven POM Configuration
First, add the dependency to your core POM so your Java code can compile against it:
<!-- core/pom.xml -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
<scope>provided</scope>
</dependency>Notice the scope is provided. We are telling Maven, "We need this to compile, but don't package it in the JAR. The environment will provide it."
Second, add the dependency to the all module POM, and configure the filevault-package-maven-plugin to embed it:
<!-- all/pom.xml -->
<dependencies>
<!-- Add the dependency here as well -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<!-- Your project modules -->
<dependency>
<groupId>com.acme.aem</groupId>
<artifactId>acme.ui.apps</artifactId>
<version>${project.version}</version>
<type>zip</type>
</dependency>
<dependency>
<groupId>com.acme.aem</groupId>
<artifactId>acme.core</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.jackrabbit</groupId>
<artifactId>filevault-package-maven-plugin</artifactId>
<configuration>
<group>acme</group>
<packageType>container</packageType>
<embeddeds>
<!-- Embed the third-party GSON bundle -->
<embedded>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<target>/apps/acme-packages/application/install</target>
</embedded>
<!-- Embed your custom project modules -->
<embedded>
<groupId>com.acme.aem</groupId>
<artifactId>acme.ui.apps</artifactId>
<type>zip</type>
<target>/apps/acme-packages/application/install</target>
</embedded>
<embedded>
<groupId>com.acme.aem</groupId>
<artifactId>acme.core</artifactId>
<type>jar</type>
<target>/apps/acme-packages/application/install</target>
</embedded>
<embedded>
<groupId>com.acme.aem</groupId>
<artifactId>acme.ui.config</artifactId>
<type>zip</type>
<target>/apps/acme-packages/osgiconfig/install</target>
</embedded>
</embeddeds>
</configuration>
</plugin>
</plugins>
</build>Notice the target paths. When the all package is installed into AEM, Jackrabbit FileVault drops the .zip and .jar files precisely into /apps/acme-packages/application/install. The internal AEM OSGi Installer subsystem continuously monitors these /install folders. When it detects a new .jar file, it automatically registers and starts it as an OSGi bundle.
The aemanalyser-maven-plugin: Common error messages and fixes
If you've ever had a build succeed locally but fail catastrophically in Cloud Manager during the "Build" or "Code Quality" step with cryptic errors, you have encountered the AEM Analyser.
Cloud Service strictly enforces API boundaries and OSGi best practices to ensure that your custom code does not break when Adobe rolls out automated updates to the platform. The aemanalyser-maven-plugin is the enforcer.
Common Errors and Resolutions
Error 1: "Use of unexported API" or "Use of internal API"
Message: [ERROR] Bundle com.acme.aem.core:1.0.0-SNAPSHOT is importing package com.day.cq.wcm.core.impl which is not exported by any bundle.
The Cause: You are trying to use an internal AEM Java class that is not marked as part of the Public API. In AEMaaCS, you are strictly prohibited from importing packages that do not have the @ProviderType or @ConsumerType annotations, or are not explicitly exported by the AEM SDK API.
The Fix: You must refactor your code. Find the equivalent public API interface (usually in an api package rather than an impl or core package). For example, use the public PageManager interface rather than trying to instantiate a PageManagerImpl.
Error 2: OSGi Configuration Validation Failure
Message: [ERROR] OSGi configuration apps/acme/osgiconfig/config/org.apache.sling.jcr.davex.impl.servlets.SlingDavExServlet.cfg.json is not allowed.
The Cause: You are trying to configure a system-level OSGi component that Adobe has blacklisted in Cloud Service. Adobe restricts certain configurations (like WebDAV, Felix Web Console settings, or low-level repository parameters) to guarantee platform stability.
The Fix: Remove the configuration entirely. You cannot change this setting in AEM as a Cloud Service.
Error 3: Banned Imports (The Logging Trap)
Message: [ERROR] Found Banned Dependency: commons-logging:commons-logging:jar:1.2
The Cause: Your project, or a third-party dependency you included, is trying to pull in a banned logging framework. AEM uses SLF4J (Simple Logging Facade for Java). Including commons-logging, log4j-core, or java.util.logging bridges will cause classloader conflicts and break AEM's internal logging.
The Fix: You must exclude the banned dependency from the third-party library in your core POM.
<dependency>
<groupId>com.thirdparty</groupId>
<artifactId>some-library</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>To catch these analyser errors locally before pushing to Git and waiting 40 minutes for a pipeline failure, run:
mvn clean install -PautoInstallSinglePackage -Daem.analyser.skip=falseCommon Cloud Manager build failures and how to fix them
Beyond the AEM Analyser, the Cloud Manager pipeline itself is notorious for failing builds that passed on a developer's local machine. Here are the primary culprits.
1. Java Version Mismatches
Cloud Manager compiles your code using a specific Java JDK (historically Java 11, now supporting Java 17). If you write code locally utilizing Java 17 features (like Records or new Switch expressions) but your maven-compiler-plugin in the root POM is still set to target Java 11, the Cloud Manager build will fail during the compile phase.
The Fix: Ensure your maven-compiler-plugin configuration matches your target Cloud environment exactly, and set your local JAVA_HOME to match.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>2. Missing ui.apps.structure Definitions
If you add a custom root folder in your ui.apps package—for example, you create /apps/acme-vendors to store third-party scripts—and you forget to declare this new root in the ui.apps.structure module's filter.xml, the build will fail.
Message: [ERROR] ValidationViolation: "jackrabbit-packagetype: Package of type 'APPLICATION' is not allowed to contain nodes outside the allowed structural nodes."
The Fix: Add the new root path to ui.apps.structure/src/main/content/META-INF/vault/filter.xml.
<!-- ui.apps.structure filter.xml -->
<workspaceFilter version="1.0">
<filter root="/apps"/>
<filter root="/apps/acme-vendors"/> <!-- Add this line -->
<filter root="/content/dam"/>
</workspaceFilter>3. The Dispatcher Validator
Cloud Manager runs a strict linting tool against your dispatcher module. If you have syntax errors in your Apache .vhost files, or you are trying to use forbidden Apache modules (like mod_php), the build will fail.
The Fix: Always run the Adobe-provided Dispatcher Validator utility locally before committing dispatcher changes.
Custom modules: Expanding the archetype
Enterprise projects frequently outgrow the default archetype structure. You may need to introduce new modules. The most common additions are a core.testing module for shared test utilities or a shared.api module.
Adding a core.testing module
When writing JUnit tests and using AEM Mocks, you often build complex mock resource resolvers or custom assert methods. Instead of duplicating these across core, it.tests, and potentially other custom backend modules, you should create a core.testing module.
- Create the directory
core.testingat the root level. - Create a
pom.xmlin this directory. Set its packaging tojar. - Add it to the root POM: In the root
pom.xml, add<module>core.testing</module>to the<modules>list. - Depend on it: In your
coremodule'spom.xml, addcore.testingas a dependency with<scope>test</scope>.
<!-- root pom.xml -->
<modules>
<module>all</module>
<module>core.testing</module>
<module>core</module>
<module>ui.apps</module>
<!-- ... -->
</modules>
<!-- core/pom.xml -->
<dependency>
<groupId>com.acme.aem</groupId>
<artifactId>acme.core.testing</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>By keeping this scope as test, you ensure that your mock utilities are never accidentally compiled into the production OSGi bundle or deployed to AEM.
Running the archetype command with all flags explained
When bootstrapping a new project, generating it correctly from the start saves hours of refactoring. Here is the fully expanded Maven command with explanations for the critical flags.
mvn -B org.apache.maven.plugins:maven-archetype-plugin:3.2.1:generate \
-D archetypeGroupId=com.adobe.aem \
-D archetypeArtifactId=aem-project-archetype \
-D archetypeVersion=48 \
-D appTitle="Acme Corp Enterprise" \
-D appId="acme" \
-D groupId="com.acme.aem" \
-D aemVersion="cloud" \
-D includeDispatcherConfig="y" \
-D frontendModule="react" \
-D includeExamples="n" \
-D includeErrorHandler="y" \
-D singleCountry="n"aemVersion="cloud": This is the most important flag. It configures the POMs for AEMaaCS, setting up theaem-sdk-apiBOM, enforcing theaemanalyser-maven-plugin, and structuring the immutable/mutable split correctly. If you are on 6.5, you would use"6.5.0".appId="acme": This drives the naming convention. It becomes the root folder in/apps/acme,/conf/acme,/content/acme, and it prefixes your artifact names (e.g.,acme.core). Keep it short and lowercase.frontendModule="react": (Options:general,react,angular,vue,none). This determines the scaffold forui.frontend. If your enterprise uses a completely separate pipeline for frontend (a highly recommended approach for massive teams), set this tonone.includeExamples="n": Always set this to "n" for production projects. You do not want the Adobe sample React components or basic HelloWorld models polluting your codebase.includeErrorHandler="y": Generates a default 404/500 error handler script under/apps/sling/servlet/errorhandler. This is highly recommended to override the default Tomcat/Sling error pages.singleCountry="n": When set to "n", it scaffolds a multi-national site structure (/content/acme/us/en,/content/acme/fr/fr) rather than a single-language root.
Cheat Sheet
| Task | Maven Command / Configuration |
|---|---|
| Prevent Node Deletion (Content) | Set mode="merge" or mode="update" on the <filter> in filter.xml. |
| Safely Exclude Sub-trees | Use <exclude pattern="/path/to/folder(/.*)?"/> within the <filter> node. |
| Build & Deploy ALL (Local) | mvn clean install -PautoInstallSinglePackage from the project root. |
| Build & Deploy Core only | mvn clean install -PautoInstallBundle from the core directory. |
| Test Cloud Service Checks | Run with -Daem.analyser.skip=false locally to mimic Cloud Manager constraints. |
| Embed a 3rd Party JAR | Add as <scope>provided</scope> in core, then use <embedded> in the all POM. |
| Validate Dispatcher Locally | Use the AEM SDK Dispatcher Tools: bin/validator full -d out src |
Best Practices
- Never use
mode="replace"on/conf,/content, or/home. This is the golden rule. You will destroy author-generated content during the next deployment. Treat mutable paths with extreme caution. - Abstract OSGi configurations by Runmode into
ui.config. Do not hardcode configurations in Java code or embed them directly inui.apps. Utilizeconfig.authorandconfig.publishfolders. - Keep
corepure Java. Do not put content definitions, XMLs, ClientLibrary scripts, or frontend assets in thecorebundle. It violates separation of concerns and bloats the JAR. - Use Dependency Management heavily. Define all artifact versions in the root parent POM's
<dependencyManagement>section. Sub-modules should inherit versions, never declare them explicitly. This prevents dependency hell and diamond dependency conflicts across a large project. - Decouple the Frontend Pipeline (for large teams). If you have a dedicated frontend team, remove the
ui.frontendmodule from the Maven build. Have the frontend CI pipeline build their static assets and push them to an artifact registry (like NPM), or have the AEM pipeline pull the compiled CSS/JS directly into theui.appsfolder just before thepackagephase.
Do's & Don'ts
- DO validate your
filter.xmlpaths regularly, especially before major production releases. Peer review every single filter change. - DO use the
allpackage to embed third-party OSGi bundles, treating them as separate lifecycle artifacts. - DO sync your local AEM SDK version (the API BOM in the parent POM) with the Cloud Service version currently running in your environment.
- DON'T embed JARs directly inside your
coreOSGi bundle using<Embed-Dependency>unless you are fully prepared to debug complex ClassNotFoundExceptions. - DON'T mix mutable (
/content) and immutable (/apps) paths in the same Jackrabbit FileVault package. AEM as a Cloud Service will reject it outright during the pipeline build. - DON'T ignore warnings from the
aemanalyser-maven-plugin. A warning locally is often a fatal error in the Cloud Manager pipeline.
Mastering the AEM Project Archetype, Jackrabbit FileVault, and the Maven build lifecycle is what elevates a standard Java developer to a Staff-level AEM engineer. Treat your deployment configuration with the same rigor, testing, and respect as your Java code, and your production deployments will run flawlessly, every single time.
Advanced Filter configurations: Properties and Node Types
While mode is the most commonly modified attribute in filter.xml, there are more granular controls available to Staff-level engineers dealing with complex data migrations or highly specialized deployment scenarios.
Property Filters (Ignoring Specific Node Properties)
Sometimes you want to overwrite a node's structure but leave specific properties (like a dynamically generated ID or a last-modified timestamp maintained by a custom workflow) intact. Jackrabbit FileVault allows you to define property-level filtering.
This is particularly useful when migrating content from legacy systems where certain JCR properties are generated dynamically at runtime, but the structural nodes themselves must be managed by the deployment package.
However, be warned: property filters are extremely complex to debug and should be used sparingly. Cloud Manager's pipeline will often flag overly complex property filters if they violate structural node rules.
Node Type Management in Deployments
If your project introduces custom JCR Node Types (e.g., acme:ProductPage inheriting from cq:Page), the deployment package must define these node types before Jackrabbit FileVault attempts to create nodes of that type.
In the archetype, this is handled via the nodetypes.cnd (Compact Node Type Definition) file located in META-INF/vault/nodetypes.cnd.
Best Practice for Node Types:
Always declare your .cnd files explicitly in your filevault-package-maven-plugin configuration if you use custom node types. If you fail to do this, the package installation will fail with a ConstraintViolationException because the JCR repository doesn't recognize the jcr:primaryType you are trying to deploy.
<plugin>
<groupId>org.apache.jackrabbit</groupId>
<artifactId>filevault-package-maven-plugin</artifactId>
<configuration>
<cndPattern>.*\.cnd</cndPattern>
</configuration>
</plugin>The Evolution of the OSGi Installer
In AEM 6.5 and older, the JCR OSGi Installer was the primary mechanism for deploying code. When a .jar or .zip was placed in /apps/myproject/install, an internal polling thread would detect it, stop the old bundle, and start the new one. This often led to StaleReferenceExceptions or momentary system instability during deployments.
AEM as a Cloud Service fundamentally changed this. The OSGi Installer no longer runs dynamically on the production author or publish pods in the same way. Instead, the Cloud Manager pipeline uses a headless mechanism (the Sling Feature Model) during the container build phase. The all package is processed before the Docker image is finalized.
This means that by the time your code hits production, the OSGi bundles and configurations are already resolved and baked in. You will never experience a "bundle stuck in installed state" in AEMaaCS production because if a bundle cannot resolve its dependencies, the Cloud Manager build pipeline will fail the deployment before the image is even deployed to Kubernetes. This is why mastering the all package embedding strategy and the aemanalyser-maven-plugin is no longer optional—it is the only way to successfully deploy code in the modern AEM era.
Summary
The AEM deployment architecture is not a passive delivery mechanism; it is the active enforcer of your application's stability, security, and integrity. By understanding the rigorous separation of modules in the AEM Maven Project Archetype, respecting the immutable boundaries of Cloud Service, and mastering the intricate modes of Jackrabbit FileVault's filter.xml, you protect your author's content and ensure smooth, automated deployments.
Remember the 10,000-page catastrophe. Let that scenario serve as a permanent reminder: in AEM, your configuration code is just as dangerous, and just as important, as your Java code. Treat it with the respect it deserves.
Discussion
Loading discussion…
Try a related tool
Subscribe to the Newsletter
Get the latest articles, tutorials, and tech insights delivered straight to your inbox. No spam, unsubscribe anytime.