25 – Maven, Packages and the AEM Project Structure
Target: 3–4 years experienced AEM Developer Covers from your additional list: Maven · Package Manager · Deployment process · project structure Project domain: a global energy technology company's marketing site.
Before we start — the question underneath this topic
This topic has been referenced from almost every earlier file without ever being explained. ui.apps, ui.content, ui.config, all, filter.xml, mode="merge" — they've appeared in files 01, 03, 04, 06, 13 and 14 as things you should know, and this is where they get explained.
But the interview question is rarely "list the Maven modules." It's:
"How does your code actually get from your laptop onto a production AEM instance?"
That's a question about the whole chain — modules, packages, filters, build, deploy. And the part that separates a good answer is understanding why the modules are split the way they are, because it isn't arbitrary. It follows directly from the immutable/mutable distinction in file 01:
/appsis code./contentand/confare data. They have different lifecycles, so they ship in different packages.
Get that, and the module structure stops being a list to memorise and becomes something you can derive.
1. Introduction
1.1 What a package actually is
Before Maven, the simpler concept:
An AEM package is a ZIP file containing repository content, plus a
filter.xmlthat declares exactly which paths are included.
That's it. It's built by a tool called FileVault (often abbreviated vlt), which serialises repository nodes into files on disk and back again.
Two things follow from that definition, and both matter:
The filter.xml is what defines the package, not the folder structure. You can have files sitting in your project that aren't in any filter, and they simply won't be deployed. That surprises people — "it's in my project, why isn't it on the server?"
Installing a package writes to the repository. It's a content operation. Which means it can overwrite things, and by default it does — section 2.4.
1.2 Why the project is split into modules
Here's the reasoning, and it's the answer to "why so many modules?"
Your project contains fundamentally different kinds of thing:
| Kind of thing | Example | Lifecycle |
|---|---|---|
| Java code | A Sling Model, an OSGi service | Compiled, becomes a bundle |
| Immutable content | Components, clientlibs, template types | Deployed, never changed at runtime |
| Mutable content | Pages, editable templates, policies | Changed by authors at runtime |
| Configuration | OSGi configs | Deployed, per run mode |
| Front-end source | SCSS, TypeScript | Compiled by webpack |
| Dispatcher config | Filter and cache rules | Deployed to the web tier |
Those have genuinely different lifecycles, and mixing them causes real problems. The worst one: if pages and components ship in the same package, redeploying your components would overwrite author content.
So the modules exist to keep them apart. Once you see it that way, the structure is obvious rather than arbitrary.
1.3 A real project example to adapt
"We use the standard AEM archetype structure —
corefor the Java bundle,ui.appsfor components and clientlibs,ui.contentfor our templates and sample content,ui.configfor OSGi configs,ui.frontendas a webpack build,dispatcherfor the web tier, andallas the aggregate that Cloud Manager actually deploys.The split that matters most is
ui.appsversusui.content.ui.appsis immutable — on Cloud Service it's baked into the container image and read-only at runtime.ui.contentis mutable content, and every one of its filters usesmode="merge"so a deployment doesn't wipe the templates and policies our content leads have adjusted.Day to day I mostly deploy just the module I've changed —
-pl corefor a Java change is much faster than rebuilding everything — and only build the fullallpackage when I need to verify the whole thing."
That covers the structure, the reason for the key split, and the practical workflow — three follow-ups pre-empted.
2. Core Concepts
2.1 The modules, and what each is for
The AEM Project Archetype generates this structure. Learn what each module is for rather than just its name.
my-project/
├── core/ Java code → an OSGi bundle
├── ui.apps/ Components, clientlibs, template types (IMMUTABLE)
├── ui.content/ Pages, templates, policies (MUTABLE)
├── ui.config/ OSGi configurations
├── ui.frontend/ SCSS / TypeScript, built by webpack
├── ui.tests/ UI / end-to-end tests
├── it.tests/ Integration tests
├── dispatcher/ Dispatcher configuration
├── all/ The AGGREGATE package that gets deployed
└── pom.xml The parent POMGoing through them properly:
core — all your Java. Sling Models, OSGi services, servlets, filters, workflow process steps, job consumers. It compiles into an OSGi bundle, which is a JAR with the manifest headers from file 06.
ui.apps — everything under /apps. Components, their dialogs and HTL, clientlibs, template types. This is immutable content — on Cloud Service it's baked into the container image and read-only at runtime (file 14).
ui.content — everything under /content and /conf. Editable templates, policies, sample or initial content, Content Fragment Models. This is mutable content, which authors change at runtime.
ui.config — OSGi configurations, organised into run-mode folders (file 06). Technically these live under /apps, but the archetype separates them because configuration changes for different reasons than code does.
ui.frontend — an npm and webpack project (file 04). The front-end team works here with SCSS and TypeScript, and the build output is copied into a clientlib in ui.apps.
dispatcher — the dispatcher configuration (file 19). On Cloud Service this is deployed by the web-tier pipeline, which is much faster than a full-stack one.
it.tests and ui.tests — integration and UI tests, run by the Cloud Manager pipeline (file 14).
all — and this one needs explaining, because its purpose isn't obvious.
2.2 Why the all package exists
The all module contains no content of its own. It's an aggregate — a package whose only job is to embed the other packages.
Why that's necessary: Cloud Manager deploys exactly one package. It doesn't take a list. So something has to bundle core, ui.apps, ui.content and ui.config into a single deployable artefact, and that's all.
And it does one more thing that matters: it declares the order things install in, and it separates the mutable and immutable parts so the Cloud Service build validation can check them.
The interview answer:
"The
allpackage is an aggregate — it contains nothing itself, it just embeds the other packages. It exists because Cloud Manager deploys exactly one package rather than taking a list, so something has to bundle the bundle, the apps content, the mutable content and the configs into one artefact. It also declares the install order, and keeps the mutable and immutable parts distinguishable so the Cloud Service build validation can check that we haven't mixed them."
2.3 filter.xml — what actually defines a package
This is the most important file in the packaging story, and the most commonly misunderstood.
<?xml version="1.0" encoding="UTF-8"?>
<workspaceFilter version="1.0">
<filter root="/apps/energy"/>
<filter root="/apps/energy-vendor-packages"/>
</workspaceFilter>The filter declares which repository paths this package covers. Not which files are in your project folder — which paths the package claims.
Two consequences that catch people out:
A file in your project that isn't under a filter root won't be deployed. It builds fine, it's in the JAR, and it never reaches the repository. "It works locally but not on the server" is often exactly this — locally you'd copied it manually at some point.
A filter root claims that entire path — which brings us to the next section, and the single most dangerous property in AEM packaging.
2.4 mode="merge" — the property that stops you destroying content
By default, installing a package REPLACES everything under its filter root.
Read that again, because the consequence is severe. If ui.content has:
<filter root="/content/energy"/>then every deployment deletes everything under /content/energy and replaces it with whatever is in the package. Every page an author has created since the last release, gone.
mode="merge" changes that:
<filter root="/content/energy" mode="merge"/>With merge, existing content is left alone and only nodes present in the package are added or updated.
The filter modes:
| Mode | Behaviour |
|---|---|
replace (default) |
Delete everything under the root, then install the package content |
merge |
Add and update; leave existing nodes alone |
update |
Update existing nodes, don't remove |
The rule to state:
Anything under
/contentor/confmust usemode="merge". Those are paths authors change. Anything under/appscan use the defaultreplace, because that's your code and a clean replace is what you want.
This is a genuinely good thing to raise unprompted, because getting it wrong destroys production content and it's a mistake people only make once.
2.5 Filter rules — include and exclude
Filters can be more precise than a root path:
<filter root="/apps/energy">
<exclude pattern="/apps/energy/install(/.*)?"/>
</filter>
<filter root="/content/dam/energy" mode="merge">
<include pattern="/content/dam/energy/logos(/.*)?"/>
</filter>exclude removes paths from a filter that would otherwise be covered. include narrows a filter to only matching paths.
A practical use: excluding an install folder so a nested package isn't accidentally shipped, or including only a specific asset folder rather than the whole DAM.
2.6 The Maven build
Maven is the build tool. The parent pom.xml declares the modules and the shared configuration; each module has its own POM.
The commands you'll actually use — and knowing the targeted ones is a small signal that you develop day to day rather than only building everything:
# Build and deploy EVERYTHING to a local author on 4502
mvn clean install -PautoInstallSinglePackage
# Same, but to a local publish on 4503
mvn clean install -PautoInstallSinglePackagePublish
# Deploy ONLY the Java bundle — much faster while iterating on backend code
mvn clean install -PautoInstallBundle -pl core
# Deploy ONLY ui.apps — faster while iterating on components and HTL
mvn clean install -PautoInstallPackage -pl ui.apps
# Just build, don't deploy
mvn clean installThe -pl flag means "project list" — build only that module. That's the difference between a fifteen-second deploy and a two-minute one, and during active development it matters a lot.
The profiles (-P) are defined in the POM and control what happens after the build — autoInstallSinglePackage uploads and installs the all package; autoInstallBundle uploads just the bundle.
2.7 Dependency scopes — the one that breaks bundles
This is the Maven detail most likely to come up, because getting it wrong produces a confusing OSGi failure.
AEM's APIs come from a dependency — the uber-jar on 6.5, or the aem-sdk-api on Cloud Service. And the scope you give it matters:
<dependency>
<groupId>com.adobe.aem</groupId>
<artifactId>aem-sdk-api</artifactId>
<version>${aem.sdk.api}</version>
<scope>provided</scope> <!-- ← THIS -->
</dependency>provided means "this will be available at runtime; don't package it."
If you use compile instead — the default — Maven embeds a copy of the AEM API into your bundle. Now there are two copies of the same packages in the OSGi container at potentially different versions, and your bundle's imports can't be satisfied.
The symptom is the file 06 problem: the bundle sits in INSTALLED state, never activates, and your code silently never runs.
So the chain to be able to state: wrong Maven scope → duplicate packages → unsatisfied Import-Package → bundle stuck in INSTALLED → your code never runs, with no error. That's a genuinely good answer because it connects a build-tool detail to a runtime symptom.
The scopes worth knowing:
| Scope | Meaning | Use for |
|---|---|---|
provided |
Available at runtime, not packaged | AEM APIs, OSGi APIs, Servlet API |
compile (default) |
Packaged into your bundle | Third-party libraries AEM doesn't have |
test |
Only on the test classpath | JUnit, Mockito, AEM Mocks |
2.8 Embedding third-party libraries
If your code needs a library AEM doesn't provide, you have to get it into the OSGi container.
Two approaches:
Embed it in your bundle — the library is packaged inside your JAR and its packages are private to you. Simple, and the right answer for a small utility library.
Deploy it as its own bundle — if the library is already an OSGi bundle, or several of your bundles need it. Cleaner, but more moving parts.
The thing to watch: embedding a library that AEM already provides at a different version causes exactly the duplicate-package problem from 2.7. Before embedding anything, it's worth checking whether AEM already exports it — /system/console/depfinder tells you which bundle exports a given package (file 06).
2.9 Package Manager
Package Manager at /crx/packmgr is the UI for uploading, installing, building and downloading packages.
What it's genuinely useful for:
Building a content package to move content between environments — export from one, install on another.
Inspecting a package — what filters does it have, what's actually inside it.
Downloading what's currently installed.
And the important caveat: you can't use it to deploy on Cloud Service production. Deployment is Cloud Manager pipelines only (file 14). Package Manager still exists on development environments, but the deployment path is closed deliberately — that's the discipline benefit from file 14, and it means production can't drift from Git.
On 6.5 you can deploy via Package Manager, and plenty of teams did. It's also exactly how production drifts from source control, which is the argument against it.
2.10 Content packages for moving content
A practical use worth knowing, and it comes up as a scenario question.
Moving content from production to a lower environment for debugging:
- Build a package on production with a filter covering just the paths you need
- Download it
- Install it on the lower environment
Three things to be careful about:
Filter precisely. A filter of /content on a large site produces an enormous package.
Exclude what you shouldn't copy — /home (users and their data), anything containing credentials, and personal data. This is a genuine data-protection concern, not just tidiness.
Watch for mode="replace" — installing a content package on the target will replace what's there unless you set merge.
On Cloud Service there's a Content Copy feature in Cloud Manager for exactly this, which is safer than hand-built packages.
2.11 The mutable/immutable split on Cloud Service
This is where the module structure becomes mandatory rather than good practice (file 14).
On Cloud Service, the build validates that packages don't mix mutable and immutable content. If ui.apps contains anything under /content, or ui.content contains anything under /apps, the build fails.
Which is why the split exists as a hard rule:
| Package | Contains | Paths |
|---|---|---|
ui.apps |
Immutable — code | /apps |
ui.content |
Mutable — data | /content, /conf |
ui.config |
OSGi configs | /apps/<project>/osgiconfig |
A common migration finding is a project where components and sample content shipped together in one package, which worked fine on 6.5 and fails validation on Cloud Service. That's what the Repository Modernizer tool restructures (file 14).
3. Internal Working
3.1 From your laptop to the repository
Two things worth drawing out:
ui.frontend builds first, and its output lands in ui.apps before that package is assembled. That's why editing the generated css.txt by hand gets overwritten (file 04) — the build regenerates it every time.
The bundle is embedded in the package. Your Java doesn't deploy separately; it's inside ui.apps (or all) in an install folder, and the Sling Installer picks it up.
3.2 How a package installs
The left branch is the dangerous one, and it's why mode="merge" matters so much on content paths. A default-mode filter on /content/energy deletes every page authors created since the last release, and the package installs successfully — no error, because it did exactly what it was told.
3.3 The Sling Installer
Worth knowing because it explains how a bundle inside a content package becomes a running bundle.
The Sling Installer watches /apps/**/install and /libs/**/install. When a JAR or a .cfg.json appears there, it picks it up — installing bundles and applying configurations.
Which is why: your compiled bundle ends up in an install folder inside the package, and OSGi configurations end up in config.<runmode> folders. The installer handles both, and everything from file 06's bundle lifecycle takes over from there.
4. Important Interview Questions
4.1 Basic
Q1. What is an AEM package?
An AEM package is a ZIP file, built by FileVault, that contains repository content together with a filter.xml declaring which repository paths it covers. Installing the package writes that content into the repository. What the package actually affects is defined by its filter, not by the folder structure inside the ZIP, and a package can also carry a compiled bundle in an install folder.
Cross: What defines what's in it? (the filter, not the folder structure) · What's FileVault? · Can a package contain a bundle? (yes — in an install folder)
Q2. Describe the AEM Maven project structure.
The archetype splits the project by kind of content. core holds the Java code and builds an OSGi bundle; ui.apps holds immutable content under /apps; ui.content holds mutable content under /content and /conf; ui.config holds OSGi configurations; ui.frontend is the webpack build; dispatcher holds the web-tier configuration; it.tests and ui.tests hold the tests; and all is the aggregate package that is actually deployed. Each module exists because it has a different lifecycle from the others.
Cross: Why so many? (different kinds of thing with different lifecycles) · What's in all? (nothing of its own — it embeds the others) · Why does all exist?
Q3. Why does the all package exist?
The all package exists because Cloud Manager deploys exactly one package rather than taking a list, so something has to bundle the Java bundle, the apps content, the mutable content and the configurations into a single artefact. It contains nothing of its own; it only embeds the other packages. It also declares the order in which they install and keeps the mutable and immutable parts separate, so that the Cloud Service build validation can check them.
Cross: Does it contain content? (no — it's an aggregate) · What order do things install in? · What happens on a local build?
Q4. What is the difference between ui.apps and ui.content?
ui.apps holds immutable content: code under /apps, such as components, clientlibs and template types, which is read-only at runtime on Cloud Service. ui.content holds mutable content: pages, editable templates and policies under /content and /conf, which authors change at runtime. On Cloud Service the build validates this split and fails if the two are mixed, and the ui.content filters are the ones that need mode="merge".
Cross: What happens if you mix them? (the Cloud Service build fails validation) · Which needs mode="merge"? (ui.content) · Why does the distinction exist?
Q5. What is filter.xml and why does it matter?
filter.xml declares which repository paths a package covers, and it is what actually defines the package. A file that sits in your project but is not under any filter root is simply never included, so it builds without error and never reaches the server. Filters also control how existing content under each root is treated on install, which is why the mode attribute matters so much.
Cross: What's the symptom of a missing filter? ("works locally, not on the server") · What are include and exclude patterns for? · What's the default mode?
Q6. What does mode="merge" do?
By default, installing a package replaces everything under each filter root with what is in the package. With mode="merge", existing content is left in place and only the nodes in the package are added. Content filters for /content and /conf need it, because without it every page authors created since the last release would be deleted. Code under /apps does not, because a clean replace is exactly what you want for code.
Cross: What happens without it on /content? (every page authors created since the last release is deleted) · Which paths need it? (/content, /conf) · Which don't? (/apps — a clean replace is what you want for code)
Q7. Which Maven commands do you use to build and deploy locally?
mvn clean install -PautoInstallSinglePackage builds everything and deploys it to a local author instance, and -PautoInstallSinglePackagePublish does the same for a local publish instance. While iterating on Java, mvn clean install -PautoInstallBundle -pl core builds and deploys only the bundle, which is much faster. -pl restricts the build to the listed module, and -P activates a Maven profile.
Cross: What does -pl do? (project list — build one module) · What's a profile? · How do you deploy to publish? (-PautoInstallSinglePackagePublish)
Q8. What scope should the AEM API dependency have?
The AEM API dependency should have provided scope, because AEM already supplies those packages at runtime and they must not be packaged into your bundle. The dependency is uber-jar on AEM 6.5 and aem-sdk-api on Cloud Service. With compile scope, a duplicate copy of the API is embedded, its imports cannot be satisfied, and the bundle stays stuck in the INSTALLED state. Test libraries such as JUnit use test scope.
Cross: What breaks with compile? (a duplicate copy of the API leaves imports unsatisfied, so the bundle is stuck in INSTALLED) · What is the dependency called? (uber-jar on 6.5, aem-sdk-api on Cloud Service) · What scope does JUnit use? (test)
Q9. What is Package Manager and what can't you do with it?
Package Manager is the UI at /crx/packmgr for uploading, building, installing and downloading packages. You cannot use it to deploy to Cloud Service production; that goes only through Cloud Manager pipelines. The restriction is deliberate, because it means production cannot drift from what is in Git. Package Manager is still available on development environments and remains useful for moving content and inspecting packages.
Cross: Why is that restriction good? (production can't drift from Git) · Is it available at all on Cloud Service? (on development environments) · What is it useful for? (moving content, inspecting packages)
Q10. How do you move content between environments?
You build a content package with a precise filter, download it, and install it on the target environment. Set mode="merge" so that you do not replace content already on the target, and exclude /home and anything containing personal data, which should not be copied between environments. Keep the filter narrow, because a filter on the whole of /content produces an enormous package. On Cloud Service, the equivalent tool is Content Copy in Cloud Manager.
Cross: What's the Cloud Service equivalent? (Content Copy in Cloud Manager) · What goes wrong with a /content filter? (an enormous package) · What's the data-protection concern?
4.2 Intermediate
Q11. Your bundle is stuck in INSTALLED after a deployment. What's the Maven cause?
The most common Maven cause is a dependency with compile scope instead of provided, which embeds a duplicate copy of an API that AEM already exports, so the bundle's imports cannot be satisfied. /system/console/bundles shows which import is unsatisfied, and /system/console/depfinder shows who exports a package. Other causes are a version-range mismatch after an upgrade, or a third-party library that was never embedded. The failure is silent because the component simply never activates.
Cross: What else causes it? (a version range mismatch after an upgrade, or a third-party library that was never embedded) · How do you find who exports a package? (/system/console/depfinder) · Why is it silent? (the component never activates, so nothing throws)
Q12. A file is in your project but isn't on the server. Why?
The file is not under any filter root in filter.xml, so it was never included in the package. The project builds without error and the file simply does not deploy. You can confirm this by inspecting the built package or checking the filter. It often appears to work locally because someone copied the file onto their instance by hand at some point, so the fix is to review the filter whenever a new path is added.
Cross: How would you confirm? (inspect the built package, or check filter.xml) · Why does it work locally? (someone copied it manually at some point) · How do you avoid it? (review the filter when adding a path)
Q13. Why can't ui.apps contain /content?
ui.apps cannot contain /content because code and content have different lifecycles: code is immutable and content is mutable. On Cloud Service the build validates the separation and fails if they are mixed, so it is enforced rather than advisory. On AEM 6.5 mixing them worked, which is why migrations often uncover it. Sample or initial content belongs in ui.content, and the Repository Modernizer tool can restructure an older project.
Cross: What was the 6.5 situation? (it worked, which is why migrations find this) · What tool restructures it? (Repository Modernizer) · Where does sample content go? (ui.content)
Q14. How does your compiled Java get into AEM?
The core bundle is embedded inside a content package, in an install folder. When the package is installed, the Sling Installer watches those folders, picks up the JAR and hands it to the OSGi framework, which then installs, resolves and starts the bundle. OSGi configurations are delivered the same way, from config.<runmode> folders, so the bundle is never deployed separately from the package.
Cross: Where do OSGi configs go? (config.<runmode> folders, also picked up by the installer) · Does the bundle deploy separately? (no — it's inside the package) · What's the run-mode folder naming?
Q15. How do you include a third-party library in an AEM project?
You either embed it in your own bundle, so that its packages are private to your code, or deploy it as a separate bundle if it is already OSGi-ready or several bundles need it. For a small utility used by one bundle, embedding is usually simpler. The risk is embedding something AEM already provides at a different version, which creates duplicate packages and unsatisfied imports; /system/console/depfinder shows what is already exported.
Cross: What is the risk? (embedding something AEM already provides at a different version creates duplicate packages and unsatisfied imports) · How do you check? (/system/console/depfinder) · Which would you prefer for a small utility? (embed it)
Q16. What are the include and exclude patterns in a filter for?
Include and exclude patterns narrow or carve out paths within a filter root. For example, you can exclude an install folder, or include only one specific asset folder instead of the whole DAM. They matter because a broad filter can produce a huge package or, without mode="merge", replace far more content than you intended.
Cross: Give a real use. · Does an exclude affect the merge mode? · What is the risk of a broad filter? (a huge package, or replacing more than you intended)
Q17. What's the practical difference between deploying all and deploying one module?
Deploying all rebuilds and redeploys everything, which is slower but complete. Deploying one module, such as -pl core, updates only the Java bundle, which during backend development can turn a two-minute cycle into a fifteen-second one. Use targeted deploys while iterating and a full all build to verify, because relying only on partial deploys lets your local instance drift away from what a clean install would produce.
Cross: When would you use each? (targeted while iterating, all to verify) · What's the risk of always deploying one module? (your local state diverges from a clean install) · How often would you do a clean full build?
Q18. How does ui.frontend fit into the project?
ui.frontend is an npm and webpack project where the front-end code, such as SCSS and TypeScript, actually lives. The Maven build compiles it and copies the output into a clientlib in ui.apps, which is why the clientlib's css.txt and js.txt are generated files that are overwritten if you edit them by hand. Keeping it separate gives the front-end team modern tooling without needing a running AEM instance.
Cross: Why separate it? (the front-end team gets modern tooling and doesn't need a running AEM instance) · What is aem-clientlib-generator? · Which pipeline deploys it on Cloud Service? (the front-end pipeline, which builds only ui.frontend)
4.3 Advanced
Q19. Walk me through how a change gets from your laptop to production.
A change moves from a local build, through Git, into a Cloud Manager pipeline:
mvn clean installbuilds each module:corebecomes an OSGi bundle,ui.frontendruns webpack into a clientlib inui.apps, the content modules become FileVault packages, andallembeds them into one artefact.- During development you deploy targeted modules, such as
-PautoInstallBundle -pl core, then run a fullallbuild before pushing to catch anything that only works because of leftover state. - The commit is pushed to the Cloud Manager Git repository, and the pipeline builds, runs the code quality gates (SonarQube and OakPAL) and security testing, deploys to Stage, runs functional and UI tests, and finally performs a rolling deployment to production without publish downtime.
There is no route around the pipeline: Package Manager cannot deploy to Cloud Service production, so production cannot drift from Git.
Cross: How long does the pipeline take? · What blocks it? · What would you do in an emergency? (use feature flags and the config pipeline, because there is no hotfix route around the pipeline)
Q20. How would you design the package structure for a multi-brand site?
Keep the split by kind of content — code, immutable content, mutable content and configuration — and share the code. If the brands share one codebase, core and ui.apps stay shared, so there is one set of components; what differs per brand lives under /conf and /content, in ui.content. Split ui.content per brand, such as ui.content.brandA and ui.content.brandB, when brands release on different schedules or are owned by different teams. Avoid splitting ui.apps per brand, because that produces copies of components that drift apart. Every content filter should use mode="merge".
Cross: What if one brand needs a component the others don't? (put it in the shared ui.apps and allow it only through that brand's template policy) · How do you handle brand-specific config? (run modes don't distinguish brands, so use context-aware configuration under /conf) · When would you use separate AEM instances?
Q21. What are the main risks in an AEM deployment process, and how do you mitigate them?
There are five main risks:
- A content filter without
mode="merge"installs successfully and silently deletes everything authors created under that root. Require merge on every/contentand/conffilter and check it in code review. - Mixing mutable and immutable content breaks the package structure; on Cloud Service the build catches it.
compilescope on an AEM API leaves the bundle stuck in INSTALLED, so its code never runs. Useprovided.- Partial deploys can hide problems, so run a clean full build before pushing.
- Anything created by hand — a user, a Felix console config, a CRXDE node — is not in Git and exists on only one environment. Everything must go through code.
Cross: How would you catch the merge problem before production? (deploy to a lower environment that holds real author content) · What is your rollback? (another pipeline run, which is also slow) · Have you seen content lost this way?
5. Cross Questions — how this topic gets drilled
Thread A — from "describe the project structure"
Why so many modules? → What's in each? → Why does all exist? → What's the difference between ui.apps and ui.content? → What happens if you mix them? → Which needs mode="merge"?
Thread B — from "what's filter.xml"
What does it define? → What if a file isn't under a filter? → What's the default mode? → What does that do to /content? → What's merge? → Which paths need it?
Thread C — from "your bundle is stuck in INSTALLED"
What does that mean? → What's the Maven cause? → What scope should AEM APIs have? → What does compile do? → How do you find who exports a package? → Why is the failure silent?
Thread D — from "how do you deploy" Locally? → To production? → Can you use Package Manager? → Why not? → What if it's an emergency? → What does the pipeline do?
6. Best Interview Answers
6.1 "Describe the AEM project structure" — about 90 seconds
"The archetype generates a multi-module Maven project, and the reason for the split is that the project contains genuinely different kinds of thing with different lifecycles.
coreis all the Java — Sling Models, OSGi services, servlets — and it compiles into an OSGi bundle.ui.appsis everything under/apps: components, dialogs, HTL, clientlibs, template types. That's immutable content — on Cloud Service it's baked into the container image and read-only at runtime.ui.contentis everything under/contentand/conf: editable templates, policies, sample content. That's mutable — authors change it.ui.configholds OSGi configurations in run-mode folders.ui.frontendis a webpack project where the front-end code lives, and its output gets copied into a clientlib inui.apps. Thendispatcherfor the web tier,it.testsandui.tests, andall.
allis worth explaining because its purpose isn't obvious — it contains nothing of its own. It's an aggregate that embeds the other packages, and it exists because Cloud Manager deploys exactly one package rather than taking a list.The split that matters most is
ui.appsversusui.content, because on Cloud Service the build actually validates it — mix mutable and immutable content in one package and the build fails. That's enforced rather than advisory, and it's a common migration finding, because on 6.5 shipping components and sample content together worked fine."
6.2 "What does mode="merge" do, and why does it matter?" — about 60 seconds
"By default, installing a package replaces everything under its filter root. So if
ui.contenthas a filter on/content/energywithout a mode, then every deployment deletes everything under that path and replaces it with what's in the package — every page authors have created since the last release, gone.
mode="merge"changes that: existing nodes are left alone, and only nodes in the package are added or updated.The rule I'd apply is that anything under
/contentor/confneeds merge, without exception, because those are paths authors change. Anything under/appscan use the default replace, because that's our code and a clean replace is exactly what we want.What makes it dangerous is that the failure looks like success. The package installs cleanly, there's no error, and the deployment reports as fine — because it did exactly what it was told. You find out when someone asks where their pages went. So it's worth checking explicitly in review rather than assuming."
7. Real Project Examples
Story 1 — The deployment that deleted a week of content
What happened. A release went out on a Friday afternoon. On Monday, the content team reported that roughly a week of new product pages had vanished from the author instance.
The cause. A new filter had been added to ui.content to ship some sample content, and it covered /content/energy — without mode="merge". So the deployment did exactly what a default-mode filter does: deleted everything under that root and replaced it with the handful of sample pages in the package.
Why nobody caught it. The deployment succeeded. There was no error, no warning, nothing in any log suggesting a problem — because nothing had gone wrong from the package's point of view. It performed a correct replace.
And it hadn't shown up in testing because the lower environments had almost no author-created content. A replace of nearly-empty content looks identical to a successful install.
The recovery. Restoring from a backup, and losing some work that had happened after the backup.
What we changed. A review rule that every /content and /conf filter has mode="merge", checked explicitly rather than assumed. And we made sure at least one lower environment carries realistic author-created content, so a destructive filter has something to destroy where it's safe.
The lesson to state: "A successful deployment that quietly deleted content is worse than a failed one, because nothing tells you. mode="merge" on content paths is the single most important line in a filter file."
Story 2 — The bundle that never activated
What happened. A new integration was deployed. The build passed, the package installed, and the feature did nothing. No errors anywhere.
The investigation. /system/console/bundles showed the bundle in INSTALLED state rather than ACTIVE, with an unsatisfied import highlighted.
The cause. A developer had added a dependency for an HTTP client library and given it the default compile scope. AEM already provided that library at a different version, so the bundle contained a duplicate copy of those packages, and the version ranges couldn't be satisfied.
The chain worth being able to explain: wrong Maven scope → duplicate packages in the container → unsatisfied Import-Package → bundle stuck in INSTALLED → the component never activates → code silently never runs, with no exception anywhere.
The fix. Removing the dependency, since AEM already provided it. /system/console/depfinder confirmed which bundle exported those packages.
What we changed. Before adding any dependency, check whether AEM already provides it. And AEM APIs always get provided scope — that one's a review checklist item now.
The lesson: "A Maven scope is a build-tool detail with a runtime consequence, and the runtime consequence is completely silent. It's worth knowing the whole chain, because 'my feature doesn't work and there are no errors' points straight at it."
Story 3 — The file that was in the project and not on the server
What happened. A component worked perfectly locally and rendered unstyled on stage. The clientlib was in the project, the category was right, and allowProxy was set correctly (file 04).
The cause. The clientlib folder was under a path that no filter in ui.apps covered. So it built fine, it just wasn't in the package.
Why it worked locally. The developer had copied it into their local instance manually while first setting it up, weeks earlier, and forgotten. From then on their local instance had it and no build ever needed to deliver it.
How we found it. Downloading the built package and looking inside — which is the check worth knowing: inspect the artefact, not the project folder. The project folder tells you what you wrote; the package tells you what will actually deploy.
The lesson to state: "'Works locally' can mean 'my local instance has something the build never put there.' When something is missing on a server, I check the built package before I check the code."
8. Configuration Examples
8.1 ui.apps filter — immutable content
ui.apps/src/main/content/META-INF/vault/filter.xml
<?xml version="1.0" encoding="UTF-8"?>
<workspaceFilter version="1.0">
<!-- IMMUTABLE content: our code.
No mode attribute, so the default 'replace' applies -- and
that is exactly what we want here. A clean replace means the
deployed state matches the build exactly, with no leftovers
from a previous release.
NOTE: nothing under /content or /conf may appear in this
package. On Cloud Service the build VALIDATES that and fails
if mutable and immutable content are mixed. -->
<filter root="/apps/energy"/>
<!-- Third-party packages embedded in the build -->
<filter root="/apps/energy-vendor-packages"/>
<!-- Oak index definitions are immutable -->
<filter root="/oak:index/energy-products-custom-1"/>
</workspaceFilter>8.2 ui.content filter — mutable content
ui.content/src/main/content/META-INF/vault/filter.xml
<?xml version="1.0" encoding="UTF-8"?>
<workspaceFilter version="1.0">
<!-- MUTABLE content: things AUTHORS change.
mode="merge" IS NOT OPTIONAL HERE.
Without it, every deployment DELETES everything under this
root and replaces it with what is in the package -- which
means every page authored since the last release.
The package installs SUCCESSFULLY while doing it, so there
is no error and nothing in any log. -->
<filter root="/content/energy" mode="merge"/>
<!-- Editable templates, policies, Content Fragment Models.
Template authors adjust these at runtime (file 03), so the
same rule applies. -->
<filter root="/conf/energy" mode="merge"/>
<!-- Only the brand asset folder, not the whole DAM -->
<filter root="/content/dam/energy" mode="merge">
<include pattern="/content/dam/energy/brand(/.*)?"/>
</filter>
<!-- Experience Fragments are pages under their own root (file 16) -->
<filter root="/content/experience-fragments/energy" mode="merge"/>
</workspaceFilter>The mode="merge" comment is the thing to be able to explain, and it's story 1.
8.3 The AEM API dependency
core/pom.xml
<dependencies>
<!-- The AEM API.
SCOPE MUST BE 'provided'. It is available at runtime and must
NOT be packaged into our bundle.
With the default 'compile' scope, Maven embeds a copy of the
AEM API into our JAR. There are then two copies of the same
packages in the OSGi container at potentially different
versions, our Import-Package cannot be satisfied, and the
bundle sits in INSTALLED state and never activates -- with
no exception anywhere (file 06). -->
<dependency>
<groupId>com.adobe.aem</groupId>
<artifactId>aem-sdk-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- Test-only: never packaged -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.wcm</groupId>
<artifactId>io.wcm.testing.aem-mock.junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>8.4 The commands worth memorising
# ---------- FULL BUILD AND DEPLOY ----------
# Everything → local AUTHOR on 4502
mvn clean install -PautoInstallSinglePackage
# Everything → local PUBLISH on 4503
mvn clean install -PautoInstallSinglePackagePublish
# ---------- TARGETED (what you actually use all day) ----------
# Only the Java bundle. Seconds rather than minutes.
mvn clean install -PautoInstallBundle -pl core
# Only ui.apps -- components, HTL, clientlibs
mvn clean install -PautoInstallPackage -pl ui.apps
# Only ui.content -- templates, policies
mvn clean install -PautoInstallPackage -pl ui.content
# ---------- BUILD WITHOUT DEPLOYING ----------
mvn clean install
# Skip tests (for a quick local check only -- never in CI)
mvn clean install -DskipTests
# ---------- CREATE A NEW PROJECT ----------
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="Energy Sites" \
-D appId="energy" \
-D groupId="com.energy" \
-D aemVersion="cloud"-pl is the flag worth knowing — it means "project list", building only that module. During active development that's the difference between a fifteen-second cycle and a two-minute one, and knowing it is a small signal that you develop day to day.
8.5 A content package filter for moving content between environments
<?xml version="1.0" encoding="UTF-8"?>
<workspaceFilter version="1.0">
<!-- Precise. A filter of /content on a large site produces an
enormous package that may not even install. -->
<filter root="/content/energy/de/de/products" mode="merge"/>
<!-- Only the assets these pages actually reference -->
<filter root="/content/dam/energy/products/transformers" mode="merge"/>
<!-- Content Fragments are ASSETS in the DAM (file 15), so they
need their own filter -- they are NOT included by publishing
or packaging the pages that reference them. -->
<filter root="/content/dam/energy/fragments/products" mode="merge"/>
<!-- DELIBERATELY NOT INCLUDED:
/home users, groups, and personal data
/conf unless the config genuinely needs moving
anything with credentials
This is a data-protection concern, not just tidiness. -->
</workspaceFilter>The Content Fragment filter is the detail worth pointing at — it's the same trap as file 15 and file 18. Fragments are separate assets, so packaging the pages that reference them doesn't include them, exactly as publishing the pages doesn't publish them.
9. Common Mistakes
| The mistake | What actually happens | The fix |
|---|---|---|
No mode="merge" on a content filter |
Deployment silently DELETES author content and reports success | mode="merge" on every /content and /conf filter |
compile scope on an AEM API |
Duplicate packages → bundle stuck in INSTALLED, code silently never runs | provided |
| A file not under any filter root | Builds fine, never deploys — "works locally" | Check the filter when adding a path |
Mixing /apps and /content in one package |
Cloud Service build validation fails | Split into ui.apps and ui.content |
| Embedding a library AEM already provides | Same duplicate-package problem | Check /system/console/depfinder first |
A /content filter that's too broad |
An enormous package that may not install | Filter precisely |
Including /home in a content package |
Copies users and personal data between environments | Exclude it, always |
| Only ever deploying one module | Local state diverges from a clean install | Full build before pushing |
Editing generated css.txt / js.txt |
Overwritten by the next ui.frontend build |
Change the webpack config (file 04) |
| Deploying via Package Manager on 6.5 production | Production drifts from Git | Pipeline, even where it's technically possible |
| Assuming packaging pages includes their fragments | Fragments are separate assets | Their own filter |
| Forgetting Oak index definitions in the filter | Indexes never deploy; queries traverse | Include the index path |
-DskipTests in CI |
Tests stop protecting you | Local quick checks only |
10. Best Practices
On filters. mode="merge" on every /content and /conf filter, without exception, checked in review rather than assumed. Filter precisely. Never include /home.
On modules. Keep the mutable/immutable split clean — it's enforced on Cloud Service anyway, and it's right on 6.5 too. Don't split ui.apps per brand; share the code and differentiate through policies and configuration.
On dependencies. provided for AEM APIs, test for test libraries, compile only for third-party libraries AEM genuinely doesn't have. Check depfinder before embedding anything.
On the build. Deploy targeted while iterating, full build before pushing. Never skip tests in CI.
On deployment. Pipeline only, even where a manual route technically exists. Anything created by hand doesn't exist on the next environment — that's the file 13 and 14 lesson, and packaging is where it bites.
11. Debugging Tips
"It works locally but not on the server." This is the signature packaging problem, and the check is: inspect the built package, not the project folder. Download the artefact from Package Manager, open it, and see whether the file is actually there. The project folder tells you what you wrote; the package tells you what deploys.
"My bundle isn't active." /system/console/bundles — INSTALLED means an unsatisfied import, which very often traces back to a Maven scope. /system/console/depfinder tells you which bundle exports a given package.
"Content disappeared after a deployment." A filter without mode="merge". Check the filter file for the affected path, and check whether the deployment reported success — it will have.
"The configuration isn't applying." Check the run-mode folder name and the file name against the component PID (file 06), and confirm the file is actually under a filter root.
| Tool | Answers |
|---|---|
/crx/packmgr |
What's installed; download and inspect a package |
| Package Manager → the package's filters | What paths does it actually claim |
/system/console/bundles |
Is the bundle ACTIVE, and which import is unsatisfied |
/system/console/depfinder |
Which bundle exports this package |
/system/console/configMgr |
What configuration values are actually in effect |
The built artefact in all/target/ |
What the build actually produced |
12. Performance Notes
Build time is developer time. Targeted deploys with -pl are the single biggest saving during active development.
Package size matters for install time. A content package covering a large subtree takes real time to install and can time out. Filter precisely.
A large content package on production is an operation, not a background task — it writes a lot of nodes and competes with authoring.
The Cloud Manager pipeline duration is dominated by the quality gates and stage tests, not the build. Which is why the faster pipeline types exist (file 14) — a dispatcher rule change doesn't need the full cycle.
13. Real Production Scenarios
1. Author content deleted after a deployment. A content filter without mode="merge". The deployment reported success.
2. Bundle stuck in INSTALLED. A Maven scope problem — compile where provided was needed.
3. A file is in the project and not on the server. Not under any filter root.
4. Works locally only. The local instance has something copied by hand that the build never delivered.
5. Cloud Service build fails on package structure. Mutable and immutable content mixed.
6. Configuration deployed but never applied. Wrong run-mode folder, wrong PID filename, or not under a filter.
7. Clientlib missing after deployment. Not covered by a filter root.
8. Content package won't install — too large. Filter too broad.
9. Users appeared on a lower environment. /home included in a content package.
10. Generated js.txt change keeps reverting. ui.frontend regenerates it on every build.
11. Content Fragments missing after a content copy. They're separate assets and need their own filter.
12. Search broken after a content migration. Oak index definitions weren't in the package, so they never deployed.
13. Production has something not in Git. Deployed by hand via Package Manager on 6.5.
14. A dependency upgrade broke the bundle. Version ranges no longer satisfied after an AEM upgrade.
15. Local instance behaves differently from a colleague's. Both have accumulated different manual state from partial deploys.
14. Follow-up Questions
- What does your project structure look like?
- How do you deploy locally?
- How does a change reach production?
- Have you ever lost content in a deployment?
- What scope do you use for the AEM API, and why?
- How do you move content between environments?
- Do you split packages per brand or per feature?
- What would you change about your build or deployment process?
For the last: "Our lower environments have almost no realistic author content, so a destructive filter has nothing to destroy there and reaches production before anyone notices. I'd want one environment carrying a real content copy specifically so that class of mistake fails somewhere safe."
15. Comparison Tables
The modules
| Module | Contains | Mutable? | Notes |
|---|---|---|---|
core |
Java | — | Compiles to an OSGi bundle |
ui.apps |
/apps — components, clientlibs |
Immutable | Read-only at runtime on cloud |
ui.content |
/content, /conf |
Mutable | Needs mode="merge" |
ui.config |
OSGi configs | — | Run-mode folders |
ui.frontend |
SCSS, TS | — | Webpack; output → ui.apps |
dispatcher |
Web tier config | — | Web-tier pipeline |
it.tests / ui.tests |
Tests | — | Run by the pipeline |
all |
Nothing | — | Aggregate — embeds the rest |
Filter modes
| Mode | Behaviour | Use for |
|---|---|---|
replace (default) |
Deletes everything under the root first | /apps — your code |
merge |
Adds and updates; leaves existing alone | /content, /conf |
update |
Updates existing, doesn't remove | Rare |
Maven scopes
| Scope | Packaged? | Use for |
|---|---|---|
provided |
No | AEM APIs, OSGi APIs |
compile (default) |
Yes | Libraries AEM doesn't have |
test |
No | JUnit, Mockito, AEM Mocks |
Deployment routes
| Route | 6.5 | Cloud Service |
|---|---|---|
| Maven profile (local) | Yes | Yes (local SDK) |
| Package Manager | Yes — and that's how drift happens | Dev environments only |
| Cloud Manager pipeline | — | The only route to production |
| RDE | — | Seconds, dev iteration (file 14) |
16. Memory Tricks
The split: "Code is immutable, content is mutable — different packages."
The dangerous default: "Replace is the default, and replace means delete."
The rule: "Merge on content, always."
Maven scope: "Provided means AEM already has it."
The scope consequence: "Compile scope, stuck bundle."
all: "The aggregate contains nothing — it just carries the others."
Filters: "The filter defines the package, not the folder."
Debugging: "Inspect the artefact, not the project."
Fast iteration: "-pl core while you work; full build before you push."
17. Revision Notes
- An AEM package is a FileVault ZIP of repository content plus a
filter.xmldeclaring which paths it covers. The filter defines the package, not the folder structure — a file not under a filter root builds fine and never deploys. - Modules:
core(Java → OSGi bundle) ·ui.apps(/apps— immutable) ·ui.content(/content,/conf— mutable) ·ui.config(OSGi configs, run-mode folders) ·ui.frontend(webpack → clientlib inui.apps) ·dispatcher·it.tests/ui.tests·all. allcontains nothing of its own — it's an aggregate embedding the others, because Cloud Manager deploys exactly one package.mode="merge"is mandatory on/contentand/conf. The default is replace, which DELETES everything under the root and installs successfully while doing it — no error, nothing in any log./appsfilters can use the default replace, because a clean replace of code is what you want.- Maven scope:
providedfor AEM APIs (uber-jaron 6.5,aem-sdk-apion cloud).compileembeds a duplicate → unsatisfiedImport-Package→ bundle stuck in INSTALLED → code silently never runs (file 06).testfor test libraries. - Before embedding a third-party library, check
/system/console/depfinderfor whether AEM already exports it. - Commands:
-PautoInstallSinglePackage(everything → 4502) ·-PautoInstallSinglePackagePublish(→ 4503) ·-PautoInstallBundle -pl core(just the Java, seconds not minutes) ·-PautoInstallPackage -pl ui.apps. - Cloud Service validates the mutable/immutable split — mixing them fails the build. Repository Modernizer restructures legacy projects (file 14).
- The Sling Installer watches
/apps/**/install— that's how a bundle inside a content package becomes a running bundle. - Package Manager cannot deploy to Cloud Service production. Deliberate — production can't drift from Git.
- Moving content: filter precisely,
mode="merge", exclude/homeand anything personal. Content Fragments need their own filter — packaging pages doesn't include them. - Debug "works locally": inspect the built package, not the project folder.
18. Cheat Sheet
Structure
core/ Java → OSGi bundle
ui.apps/ /apps IMMUTABLE (default replace is fine)
ui.content/ /content /conf MUTABLE (mode="merge" REQUIRED)
ui.config/ OSGi configs, config.<runmode>/
ui.frontend/ webpack → clientlib in ui.apps
dispatcher/ web tier
it.tests/ ui.tests/
all/ AGGREGATE — embeds the rest, deployed as ONE packagefilter.xml
<!-- code: replace is fine -->
<filter root="/apps/energy"/>
<!-- content: MERGE OR YOU DELETE AUTHOR CONTENT -->
<filter root="/content/energy" mode="merge"/>
<filter root="/conf/energy" mode="merge"/>
<!-- narrowing -->
<filter root="/content/dam/energy" mode="merge">
<include pattern="/content/dam/energy/brand(/.*)?"/>
</filter>
<filter root="/apps/energy">
<exclude pattern="/apps/energy/install(/.*)?"/>
</filter>
modes: replace (DEFAULT — deletes first) | merge | updateMaven scopes
<scope>provided</scope> AEM APIs — NOT packaged
<scope>compile</scope> default — PACKAGED (duplicate risk)
<scope>test</scope> JUnit, Mockito, AEM MocksCommands
mvn clean install -PautoInstallSinglePackage # all → 4502
mvn clean install -PautoInstallSinglePackagePublish # all → 4503
mvn clean install -PautoInstallBundle -pl core # JAVA ONLY, fast
mvn clean install -PautoInstallPackage -pl ui.apps # ui.apps only
mvn clean install # build, don't deployDebug
Works locally, not on the server → inspect the BUILT PACKAGE
Bundle INSTALLED → Maven scope; /system/console/bundles
Who exports this package? → /system/console/depfinder
Content vanished → a filter without mode="merge"
Config not applied → run-mode folder, PID filename, filter19. Frequently Forgotten Things
- The default filter mode is
replace, and replace means DELETE first. mode="merge"on every/contentand/conffilter.- That failure looks like a successful deployment — no error anywhere.
- The filter defines the package, not your folder structure.
providedscope for AEM APIs —compilegives you a bundle stuck in INSTALLED.- That failure is silent too — the component never activates, so nothing throws.
allcontains nothing — it's an aggregate.- Cloud Manager deploys exactly one package, which is why
allexists. - Cloud Service validates the mutable/immutable split and fails the build.
-plbuilds one module — seconds instead of minutes.ui.frontendregeneratescss.txtandjs.txt— hand edits are overwritten.- The Sling Installer watches
installfolders — that's how the bundle gets in. - Package Manager can't deploy to Cloud Service production.
- Content Fragments need their own filter — packaging pages doesn't include them.
- Never include
/homein a content package. - Oak index definitions need to be in a filter, or searches break after migration.
20. Final Interview Summary
1. A package. A FileVault ZIP plus a filter.xml that defines what it covers.
2. The modules. core, ui.apps, ui.content, ui.config, ui.frontend, dispatcher, tests, all.
3. Why the split. Different kinds of thing with different lifecycles — code versus data.
4. all. An aggregate, because Cloud Manager deploys exactly one package.
5. mode="merge". Mandatory on content paths. The default deletes, and it looks like success.
6. Maven scope. provided for AEM APIs, or the bundle sticks in INSTALLED and your code silently never runs.
7. The Sling Installer. Watches install folders — that's how a bundle inside a package becomes a running bundle.
8. Cloud Service enforcement. The mutable/immutable split is validated, not advised.
9. Deployment. Targeted while iterating, full build before pushing, pipeline to production.
10. The debugging move. Inspect the built artefact, not the project folder.
21. Mock Interview
How to use this: cover the answers, 20-minute timer, speak every answer out loud.
The interviewer asks:
- Describe the AEM Maven project structure.
- Why are there so many modules?
- Why does the
allpackage exist? - What's the difference between
ui.appsandui.content? - What is
filter.xml? - What does
mode="merge"do, and what happens without it? - Which paths need merge and which don't?
- What is an AEM package?
- What scope should the AEM API dependency have, and what breaks otherwise?
- How does your compiled Java actually get into AEM?
- What Maven commands do you use day to day?
- A file is in your project but not on the server. Why?
- How do you move content between environments?
- Can you deploy with Package Manager on Cloud Service?
- Walk me through how a change gets from your laptop to production.
Model answers
1. (The 6.1 answer — the modules, what each is for, the lifecycle reasoning, and the all aggregate.)
2. Because the project contains genuinely different kinds of thing with different lifecycles — Java that compiles to a bundle, immutable content that's deployed and never changed at runtime, mutable content that authors change, configuration that varies per environment, and front-end source that's compiled by a completely different toolchain. Mixing them causes real problems, and the worst one is that if pages and components shipped together, redeploying components would overwrite author content.
3. Because Cloud Manager deploys exactly one package — it doesn't take a list. So something has to bundle the OSGi bundle, the apps content, the mutable content and the configs into a single artefact, and that's all. It contains nothing of its own; it's purely an aggregate that embeds the others. It also declares install order, and keeps the mutable and immutable parts distinguishable so the Cloud Service build validation can check them.
4. ui.apps is everything under /apps — components, dialogs, HTL, clientlibs, template types. That's immutable content: on Cloud Service it's baked into the container image and read-only at runtime. ui.content is /content and /conf — editable templates, policies, sample content. That's mutable: authors change it. The distinction matters practically because ui.content needs mode="merge" on its filters and ui.apps doesn't, and because on Cloud Service mixing them fails the build.
5. The file that declares which repository paths the package covers. And the important point is that it's what actually defines the package — not the folder structure in your project. A file sitting in your project that isn't under any filter root builds perfectly happily and never deploys, which is the classic "works locally but not on the server."
6. (The 6.2 answer — the default replaces, which means deleting everything under the root; merge leaves existing content alone; the rule about which paths; and the point that the failure looks like a successful deployment.)
7. Merge on everything under /content and /conf, without exception, because those are paths authors change — pages, editable templates, policies. /apps doesn't need it, and arguably shouldn't have it: that's our code, and a clean replace means the deployed state matches the build exactly with no leftovers from a previous release.
8. A ZIP file built by FileVault containing serialised repository content, plus a filter.xml declaring which paths it covers. Installing it writes to the repository, which is why it can overwrite things. It can also contain an OSGi bundle in an install folder, which is how compiled Java gets deployed.
9. provided — the AEM API is available at runtime and must not be packaged into your bundle. With the default compile scope, Maven embeds a copy of the API into your JAR, so there are two copies of the same packages in the OSGi container at potentially different versions, your Import-Package can't be satisfied, and the bundle sits in INSTALLED state and never activates. And the failure is completely silent — the component never activates so nothing throws, and you get a feature that just does nothing. We had exactly that with an HTTP client library someone added at default scope that AEM already provided at a different version.
10. The compiled bundle is embedded inside a content package, in an install folder. The Sling Installer watches /apps/**/install and /libs/**/install, picks up the JAR, and hands it to OSGi — and from there the bundle lifecycle takes over: installed, resolved, active. The same mechanism picks up .cfg.json files from config.<runmode> folders, which is how OSGi configuration gets applied.
11. mvn clean install -PautoInstallSinglePackage for everything to a local author on 4502. But day to day I mostly deploy targeted — -PautoInstallBundle -pl core for a Java change, because rebuilding everything for a one-line change wastes a couple of minutes each time, and -pl ui.apps when I'm working on components or HTL. The -pl flag means "project list" and builds only that module. I do a full all build before pushing though, to make sure nothing works only because of leftover state from partial deploys.
12. Because it isn't under any filter root, so it was never in the package. It builds fine and simply doesn't deploy. And it typically "works locally" because someone copied it into their local instance by hand at some point — often weeks earlier — and forgot. The check I'd do is download the built package and look inside it. The project folder tells you what you wrote; the package tells you what will actually deploy, and those aren't the same thing.
13. Build a content package with a precise filter, download it, install it on the target with mode="merge". Three cautions. Filter precisely, because a /content filter on a large site produces a package that may not even install. Never include /home, because that copies users and personal data between environments, which is a data-protection concern rather than just untidiness. And remember that Content Fragments need their own filter — they're assets in the DAM, so packaging the pages that reference them doesn't include them, exactly as publishing those pages doesn't publish them. On Cloud Service there's a Content Copy feature in Cloud Manager, which is safer than hand-built packages.
14. Not to production — deployment there is Cloud Manager pipelines only. Package Manager still exists on development environments, but the production route is closed deliberately. And that's a benefit rather than an inconvenience: on 6.5 you can deploy a package by hand, and that's precisely how production ends up containing something nobody can reproduce from Git. The cost is that there's no emergency route, which you mitigate with feature flags and the faster config and web-tier pipelines.
15. (The Q19 answer — local build with each module's output, targeted deploys while iterating, a full build before pushing, then the Cloud Manager pipeline: build, quality gates, security testing, images, stage, tests, rolling production deployment. Plus the point that there's deliberately no route around it, and why that's a benefit.)
Next topic
26-Cloud-Manager-Pipelines-and-CI-CD — Cloud Manager in depth, the pipeline types and quality gates, Git strategy and branching, environments, and what "CI/CD" actually looks like on an AEM project.
Topic 25 of the AEM Interview Preparation repository. Teaching style, energy-sector project domain.