A Technical Deep-Dive for Developers & Architects
Adobe Experience Manager is one of the most powerful — and most misunderstood — platforms in
enterprise web development. This article gives you that map. By the time you reach the end, you’ll
be able to trace any request from a browser URL all the way through the stack to a rendered page,
and you’ll know exactly where things can go wrong at each layer.
| Stack Layers | Key Frameworks | Min Read Time | Skill Level |
|---|---|---|---|
| 6 core layers | OSGi, JCR, Sling, HTL | ~20 minutes | Intermediate–Expert |
1. The AEM Technology Stack
AEM doesn’t run on magic. It runs on a carefully layered set of open standards. Understanding
those layers is the difference between being reactive (“something is broken”) and proactive (“I know
exactly what to check”).
The full stack, from bottom to top:
| Layer | Technology | What It Does |
|---|---|---|
| 6 (Top) | AEM Applications | Sites, Assets, Forms — the products you work with |
| 5 | Adobe Granite | AEM's custom layer: workflows, tagging, replication, security |
| 4 | Apache Sling | Maps URLs to content; drives the request pipeline |
| 3 | JCR / Apache Oak | Content repository — everything is a node |
| 2 | OSGi (Apache Felix) | Modular runtime — bundles, services, dynamic wiring |
| 1 (Base) | JVM (Java) | The foundation — all of the above runs on the JVM |
2. OSGi — The Modular Heart
OSGi is beautiful, until it isn’t. When it works, you get a hot-reloadable, loosely-coupled runtime
where services wire themselves together at startup. When it breaks, you get a cascade of bundle
failures that look completely unrelated to your change.
What Is OSGi?
OSGi (Open Services Gateway initiative) is a Java-based component system. In AEM, it’s
implemented by Apache Felix. The entire AEM application — including your custom code — runs
as a set of OSGi bundles inside this container.
The Four Building Blocks
Bundles
A bundle is a standard JAR file with extra metadata in its MANIFEST.MF. That metadata tells OSGi
what packages the bundle exports (makes available) and what packages it imports (depends on).
Services
A service is a Java interface implemented by one or more bundles and registered in a service
registry. Other bundles discover services through the registry — they never depend on a specific
implementation class.
Components
A component is a Java class annotated with @Component (from the OSGi DS annotations). When
the bundle activates, OSGi DS scans these annotations and instantiates/wires components
automatically.
Configurations
Every component can have a corresponding factory configuration. Configurations are stored in the
JCR repository (under /apps/config) or as .cfg.json/.config files in your project. They are
environment-aware through run modes.
Production Gotcha — The Silent Run-Mode Miss
Your OSGi configuration for prod is under /apps/config.prod but your run modes are set to
author,publish — not prod. Everything deploys without an error. Your production config is silently
ignored. Always validate run-mode-specific configs with the Felix console after deployment.
OSGi Bundle Lifecycle
- Installed — Bundle JAR is in the OSGi container
- Resolved — All package imports are satisfied
- Starting — Bundle's Activator.start() is called
- Active — Bundle is running, services are registered
- Stopping — Bundle's Activator.stop() is called
- Uninstalled — Bundle is removed from the container
Production Gotcha — The OSGi Cascade Failure
You update Bundle A, which exports a package that Bundle B imports. Bundle B suddenly goes from
Active to Installed state (its import is no longer satisfied during the update window). Bundle C
depends on a service from Bundle B — it also goes down. One bundle update causes three services
to disappear simultaneously. The Felix console’s Bundles view is your first diagnostic tool.
3. JCR & Apache Oak — Everything Is a Node
The Java Content Repository (JCR) is the storage backbone of AEM. Unlike a relational database
with rows and columns, JCR stores everything — content, configuration, templates, component
code, user data — as a hierarchy of nodes and properties.
The Repository Structure
The JCR tree has several standard root paths you’ll work with every day:
| Path | Purpose | Example |
|---|---|---|
| /content | All authored content — pages, assets, experience fragments | /content/we-retail/en/home |
| /apps | Custom application code — components, templates, OSGi configs | /apps/my-project/components/page |
| /libs | AEM product code — do not modify directly | /libs/granite/ui/components/coral/foundation |
| /conf | Editable template definitions and context-aware configs | /conf/my-project/settings/wcm/templates |
| /var | Variable data — workflow instances, audit logs, replication queues | /var/workflow/instances |
| /home | Users, groups, and their profile data | /home/users/admin |
Node Types
Every node has a primary type that defines what properties and child nodes it can have. Common
node types in AEM include:
- nt:unstructured — Unconstrained node; accepts any property
- nt:file / nt:resource — Binary file storage
- cq:Page — A content page (has a jcr:content child)
- cq:PageContent — The content node of a page
- dam:Asset — A Digital Asset Manager asset
- cq:Component — An AEM component definition
Apache Oak — The JCR Implementation
Apache Oak is the JCR 2.0 implementation powering AEM 6.x and AEM as a Cloud Service. Oak
supports two storage backends:
- TarMK (Tar Micro Kernel) — File-based; default for author instances. Good for single-node setups.
- MongoMK (Mongo Micro Kernel) — MongoDB-backed; used for clustered author environments to support multiple active author nodes.
Sling Resource Resolution and the JCR
Sling maps incoming URLs to JCR nodes. When a request arrives for /content/my-site/home.html,
Sling walks the JCR tree to find a node at /content/my-site/home, then looks for the component
referenced by that node’s sling:resourceType property to render it.
4. Apache Sling — The Request Pipeline
Apache Sling is the web framework that bridges the JCR and your component code. Its core insight
is deceptively simple: every URL maps to a resource in the JCR, and every resource has a type
that determines how it’s rendered.
URL Anatomy in Sling
A Sling URL follows a specific pattern that experienced AEM developers read like code:
| Path | Selectors | Extension | Suffix |
|---|---|---|---|
| /content/we-retail/home | model.json | .html | /some/extra/path |
The selectors (.model.json in the example above) are what drive multi-format rendering. The same
resource at /content/page can render as HTML with no selector, as JSON with .model, and as an
image thumbnail with .thumb.80.80 — all served by different scripts without any routing
configuration.
Security Gotcha — Selector Injection
Sling’s open selector model is powerful but dangerous. An attacker requesting
/content/page.infinity.json can dump an entire JCR subtree as JSON. Always configure your Sling
URL mappings and Dispatcher rules to whitelist only the selectors your application exposes. The
/system/console/slinglog viewer will show you every selector hit in real time.
The Sling Request Pipeline
When a request arrives, Sling processes it through a fixed pipeline:
- URL Decomposition — Parse path, selectors, extension, suffix
- Resource Resolution — Find the JCR node matching the path
- Servlet/Script Resolution — Find the best script to render the resource based on resourceType, selectors, and extension
- Request Filters — Pre-processing filters run (authentication, access control)
- Script Execution — HTL/JSP/Servlet renders the response
- Response Filters — Post-processing filters run (header manipulation, etc.)
Sling Models
Sling Models are Plain Old Java Objects (POJOs) annotated to automatically inject resource
properties and OSGi services. They’re the recommended way to write business logic in AEM.
5. HTL (HTML Template Language)
HTL replaced JSP as the recommended server-side templating language for AEM. If you’re still
writing Java directly in your template files, you’re doing it wrong — and you’re creating a security
risk.
Why HTL Over JSP?
- XSS protection is built-in — HTL escapes output context-appropriately by default
- Separation of concerns — Logic lives in Sling Models; HTL is pure presentation
- No scriptlets — Java code cannot be embedded inline in HTL files
- Security by design — Server-side-only execution; client never sees HTL source
HTL Syntax Essentials
data-sly-use — Load a Sling Model
data-sly-list — Iterate a Collection
data-sly-test — Conditional Rendering
data-sly-include / data-sly-resource — Composition
Best Practice — HTL Context Options
Always be explicit about output context. ${variable @ context=’html’} for HTML content, ${variable @
context=’uri’} for URLs, ${variable @ context=’attribute’} for HTML attributes. The default ‘text’ context
strips all HTML — use ‘unsafe’ sparingly and only when you trust the source.
6. Client-Side Libraries (ClientLibs)
ClientLibs are AEM’s asset management system for CSS and JavaScript. They aggregate, minify,
and version-stamp your front-end assets for performance and cache control.
ClientLib Structure
A ClientLib is a JCR node of type cq:ClientLibraryFolder located typically under /apps/my-
project/clientlibs/. Key properties include:
| Property | Type | Purpose |
|---|---|---|
| categories | String[] | Logical name(s) used to include this library in pages |
| dependencies | String[] | Other ClientLib categories that must load first |
| embed | String[] | Other ClientLib categories to inline into this one |
| js.txt / css.txt | Text file | Ordered list of JS/CSS files to aggregate |
To include a ClientLib in a page template:
7. Run Modes — Environment Awareness
Run modes are how AEM differentiates between environments (author vs. publish) and deployment
contexts (dev, staging, prod). They drive OSGi configurations, Sling content loading, and even
which bundles are active.
Standard Run Modes
- author — The authoring environment (content creation, workflow, administration)
- publish — The delivery environment (public-facing; cached by Dispatcher)
- dev — Development-specific configurations
- stage — Staging/QA environment configurations
- prod — Production configurations
Run modes are set at startup via the sling.run.modes system property or in the quickstart JAR
filename. You can have multiple active run modes simultaneously — e.g., author,prod.
Run-Mode-Specific Configurations
OSGi configuration files are placed in path-scoped folders under /apps/[project]/config:
- /apps/my-project/config — Applied to all run modes
- /apps/my-project/config.author — Author only
- /apps/my-project/config.publish — Publish only
- /apps/my-project/config.author.dev — Author + dev (both must match)
Real-World Issue — Missing Publish Config
A common mistake is writing the correct OSGi configuration for the author instance but forgetting to
create the publish-specific equivalent. Cache TTLs, replication agent settings, and authentication
handlers often have different requirements per tier. Always peer-review config changes against both
/config.author and /config.publish.
8. Replication — Author to Publish
When a content author clicks “Activate” in the AEM Sites console, AEM doesn’t just flip a flag. It
triggers a replication workflow that serializes the JCR content from the author instance and
transmits it to one or more publish agents.
The Replication Workflow
- Author triggers Activation (manual, workflow, or API)
- Replication Agent serializes the content package
- Package is transmitted to the publish instance via HTTP POST to /bin/receive
- Publish instance deserializes and imports the content into its JCR
- Dispatcher Flush Agent sends cache invalidation requests to the Dispatcher
Replication Agents
Replication agents are configured under /etc/replication/agents.author/ and
/etc/replication/agents.publish/. The most important ones are:
- Default Agent — Replicates to the publish instance
- Reverse Replication Agent — Pulls user-generated content (forms data, etc.) back to author
- Dispatcher Flush Agent — Sends cache invalidation (FLUSH) requests to the Dispatcher
9. Dispatcher — The Performance & Security Layer
The Dispatcher is the last line of defense and the first line of performance. It sits in front of your
publish tier, caches HTML and assets, load balances requests, and filters malicious traffic before it
ever reaches AEM.
What the Dispatcher Does
- Caches static HTML pages and assets to serve requests without hitting AEM
- Invalidates cached pages when content is activated (via the Flush agent)
- Blocks requests to sensitive URL patterns (/crx, /system, /bin) from the public
- Load balances requests across multiple publish instances
- Can handle SSL termination and custom header manipulation
Cache Invalidation
Dispatcher cache invalidation works through a stat file mechanism. When the Dispatcher Flush
Agent triggers, the Dispatcher creates a .stat file in the cache directory. Any cached file older than
the .stat file is considered stale and will be fetched fresh from AEM on the next request.
Production Gotcha — Cache Stampede
When a high-traffic page cache expires (or is intentionally invalidated after a publish), dozens of
simultaneous requests can hit AEM before the first request has finished rendering and warming the
cache. This “stampede” can bring down an AEM publish instance. Solutions include request
coalescing in the Dispatcher, grace periods, and staggered activation schedules.
Key Dispatcher Configuration Files
| File | Purpose |
|---|---|
| dispatcher.any | Main configuration: farms, filters, cache rules, load balancing |
| /cache/rules | Which URLs to cache and for how long |
| /filter | Whitelist/blacklist URL patterns; blocks /crx, /system, /apps, etc. |
| /renders | Publish instance hostnames and ports to route requests to |
| /vanityUrls | Resolves AEM vanity URL redirects before forwarding to publish |
10. Tracing a Request End-to-End
This is where everything comes together. Let’s follow a real HTTP GET request from a browser all
the way to a rendered AEM page.
| Step | Component | What Happens |
|---|---|---|
| 1 | Browser | Sends GET /content/we-retail/us/en/home.html |
| 2 | Load Balancer / CDN | Routes to the Dispatcher (Apache + mod_dispatcher) |
| 3 | Dispatcher — Filter | Checks /filter rules. If denied → 403. If allowed → continue. |
| 4 | Dispatcher — Cache | Cache HIT → Serve file from disk, done. Cache MISS → Forward to publish. |
| 5 | AEM Publish — Sling | URL decomposed. Path=/content/.../home, extension=html |
| 6 | Sling — Resource | JCR node at path is resolved. sling:resourceType retrieved. |
| 7 | Sling — Script | Finds /apps/my-project/components/page/page.html (HTL script) |
| 8 | HTL Engine | Evaluates HTL; adapts resource to Sling Model; merges data into template |
| 9 | Sling Model | @ValueMapValue and @OSGiService injections resolved; business logic runs |
| 10 | Response | HTML returned → Dispatcher caches it → Browser renders page |
Every step in this chain has a corresponding diagnostic tool. OSGi problems → Felix console. JCR
problems → CRXDE Lite. Sling resolution problems → Sling Resource Resolver console.
Dispatcher problems → mod_dispatcher logs. Knowing the chain means knowing exactly where to
look.
11. Quick Reference — Diagnostic Tools
| Problem Type | Diagnostic Tool | URL |
|---|---|---|
| OSGi bundle state | Felix Web Console — Bundles | /system/console/bundles |
| OSGi service wiring | Felix Web Console — Components | /system/console/components |
| OSGi configurations | Felix Web Console — Configuration | /system/console/configMgr |
| JCR content & structure | CRXDE Lite | /crx/de/index.jsp |
| Sling URL resolution | Sling Resource Resolver | /system/console/jcrresolver |
| ClientLib issues | HTML Library Manager | /libs/granite/ui/content/dumplibs.html |
| Replication status | Replication Agents | /etc/replication/agents.author.html |
| Request logs | Log Support | /system/console/slinglog |
Conclusion
AEM is not a black box. It’s six well-documented open-source technologies stacked on top of each
other, each with a specific responsibility, a standard API, and a console for introspection.
The developers who excel in AEM are not the ones who memorize the most APIs. They’re the ones
who understand the shape of the system — who know that a missing OSGi configuration in a run-
mode folder is why prod behaves differently than dev, that a Dispatcher filter rule is why a perfectly
valid URL returns 403, and that a Sling resourceType miss is why a component renders nothing.
You now have that map. The next step is to open the Felix console on a real AEM instance and
start exploring. The architecture is all there, visible and interactive, waiting for you to poke at it.