AEM Hubv1.0
General•18 min read•Oct 4, 2026

AEM Architecture: The Four-Layer Stack

#Architecture#Sling#Felix#JCR#JackRabbit Oak

Introduction

AEM isn't a single monolithic product — it's a carefully layered stack built on top of established open-source technologies, with Adobe's proprietary application logic sitting at the top. Understanding this layering is what separates developers who can follow a tutorial from developers who can actually debug a broken bundle, understand why a component isn't rendering, or reason about why a repository operation is slow. Every AEM instance you've installed (see Installing AEM Server Locally.md) and every run mode you've configured (see Run Modes in AEM.md) sits on top of this exact four-layer architecture. After reading this article, you'll understand what each layer does, how they depend on each other, and why Adobe chose to build AEM this way instead of writing a custom framework from scratch. Prerequisites: Basic understanding of Java, HTTP request/response cycles, and familiarity with the AEM installation and run mode concepts from earlier articles in this series.


The Big Picture: Four Layers, Bottom to Top

AEM's architecture is best understood as a stack, where each layer depends on the one beneath it and adds its own specialized capability:

Java
┌─────────────────────────────────────────────────────────┐
│  Layer 4: APPLICATION LAYER (AEM)                        │
│  - WCM (Web Content Management)                          │
│  - DAM (Digital Asset Management)                        │
│  - Workflows, Templates, Components, Experience Fragments│
│  - Author/Publish distinction, Personalization           │
└─────────────────────────────────────────────────────────┘
                          ▲ built on
┌─────────────────────────────────────────────────────────┐
│  Layer 3: WEB APPLICATION FRAMEWORK (Apache Sling)       │
│  - Resource-centric request processing                   │
│  - Script/Servlet resolution                              │
│  - HTL rendering, Sling Models, Adapters                 │
└─────────────────────────────────────────────────────────┘
                          ▲ built on
┌─────────────────────────────────────────────────────────┐
│  Layer 2: STORAGE LAYER (JCR / Apache Jackrabbit Oak)     │
│  - Content Repository (hierarchical node storage)         │
│  - Versioning, Access Control, Query (JCR-SQL2, XPath)    │
│  - NodeStore persistence (TarMK, Mongo, Azure)            │
└─────────────────────────────────────────────────────────┘
                          ▲ built on
┌─────────────────────────────────────────────────────────┐
│  Layer 1: OSGi / MODULE FRAMEWORK (Apache Felix)          │
│  - Dynamic module (bundle) lifecycle management           │
│  - Service registry and dependency injection              │
│  - Versioned classloading isolation                        │
└─────────────────────────────────────────────────────────┘
                          ▲ built on
                    Java Virtual Machine (JVM)

Each layer is a separate, mature open-source project that Adobe adopted rather than reinvented. This is a deliberate architectural choice: Apache Felix, Apache Jackrabbit Oak, and Apache Sling are all independently maintained Apache Software Foundation projects used outside of AEM entirely. Adobe builds the AEM-specific application layer on top of them and contributes back to these projects upstream.


Layer 1: OSGi — Apache Felix (The Module Foundation)

What OSGi Is

OSGi (Open Services Gateway initiative) is a module system and service platform for Java. It solves a problem plain Java never addressed well: how do you deploy, update, start, and stop individual pieces of an application independently, without restarting the entire JVM?

AEM uses Apache Felix as its OSGi implementation — the actual runtime container that manages everything running inside AEM.

Core OSGi Concepts

1. Bundles

A bundle is OSGi's unit of modularity — essentially a JAR file with extra metadata (in its MANIFEST.MF) describing what Java packages it exports (makes available to others) and what packages it imports (depends on from others).

Java
MANIFEST.MF (inside a bundle JAR):
Bundle-SymbolicName: com.example.aem.core
Bundle-Version: 1.0.0
Export-Package: com.example.aem.models;version="1.0.0"
Import-Package: org.apache.sling.api;version="[2.0,3.0)"

Each bundle has its own lifecycle, independent of every other bundle:

Java
INSTALLED → RESOLVED → STARTING → ACTIVE → STOPPING → UNINSTALLED

This is why, when you deploy custom code to AEM (as covered in the Maven section of Prerequisites to Learn AEM.md), you can install, update, or stop your bundle without restarting the entire AEM instance — every other bundle keeps running.

2. The Service Registry

OSGi bundles don't call each other's classes directly most of the time — instead, they publish and consume services through a central registry. A bundle registers a service interface implementation; other bundles look it up (or have it injected) without knowing which bundle actually provides it.

Java
// Bundle A registers a service
@Component(service = PricingService.class)
public class PricingServiceImpl implements PricingService {
    public double calculateDiscount(double price) {
        return price * 0.9;
    }
}

// Bundle B consumes it — doesn't need to know Bundle A exists
@Component(service = SomeOtherService.class)
public class SomeOtherService {
    @Reference
    private PricingService pricingService; // OSGi injects it
}

This is the foundation that makes Sling Models' @Inject and @OSGiService annotations work — they're ultimately asking the OSGi service registry for an implementation.

3. Versioned Classloading Isolation

Each bundle gets its own classloader. This means two bundles can depend on different versions of the same library without conflict — something plain Java classpath loading cannot do. If Bundle A needs Guava 20 and Bundle B needs Guava 30, OSGi can keep both satisfied simultaneously, as long as each bundle only exposes what it explicitly exports.

4. The OSGi Configuration Admin

OSGi also standardizes how services get configured at runtime — this is the exact mechanism that run modes (from Run Modes in AEM.md) hook into. A config.author.dev/ folder ultimately deploys OSGi configuration nodes that the Configuration Admin service applies to running components, often without requiring a restart.

Where You Interact With This Layer

  • Web Console: http://localhost:4502/system/console/bundles — lists every bundle, its state (Active/Resolved/Installed), and lets you start/stop/refresh individual bundles

  • Service listing: http://localhost:4502/system/console/services — shows every registered OSGi service

  • Configuration Manager: http://localhost:4502/system/console/configMgr — view and edit OSGi configurations directly

Why this matters for debugging: When a component "doesn't work," the very first thing to check is whether its backing bundle shows Active in the bundle console. A bundle stuck at "Installed" or "Resolved" (not Active) means a dependency is missing — and nothing above this layer will function correctly until it's fixed.


Layer 2: JCR / Storage Layer — Apache Jackrabbit Oak

What the JCR Is

JCR (Java Content Repository, standardized as JSR-170 and JSR-283) is a specification for how content should be stored, queried, versioned, and secured in a hierarchical, tree-like structure — conceptually similar to a filesystem, but far richer: nodes can have arbitrary properties, mixins (like interfaces, but for content), versioning history, and fine-grained access control lists.

Apache Jackrabbit Oak is the actual implementation of this specification that modern AEM (6.x and Cloud Service) uses under the hood. (Older AEM/CQ versions used the original "Jackrabbit Classic" implementation; Oak is the modern, horizontally-scalable successor.)

The Repository Model

Everything in AEM — pages, components, DAM assets, user accounts, workflow instances, OSGi configurations themselves — is stored as nodes and properties in this one unified tree:

Java
/
├── content/
│   └── mysite/
│       └── en/
│           └── jcr:content          ← A node, with properties like jcr:title
├── apps/
│   └── mysite/
│       └── components/
│           └── productcard/
│               └── productcard.html  ← Stored as a node too (nt:file)
├── libs/                             ← Out-of-the-box AEM/Granite code
├── etc/                              ← Workflow models, replication config
├── var/                              ← Runtime/audit data
└── home/
    └── users/                        ← User accounts, groups

Every single thing you see when browsing AEM — in CRXDE Lite, in the content tree, in the DAM — is a node in this repository. There is no separate relational database for content; the JCR is the database.

Key JCR Capabilities

1. Hierarchical Node Structure

Nodes have types (nt:unstructured, cq:Page, dam:Asset, etc.) that constrain what properties and child nodes they can have — similar to how a class defines what fields an object can have.

2. Versioning

Pages and assets support full version history — every save can create a version, letting you roll back content to a prior state. This is built into the repository layer itself, not bolted on by AEM.

3. Access Control Lists (ACLs)

Every node can have fine-grained permissions (read, write, modify, replicate) assigned per user or group. This is how AEM enforces that a content author can edit /content/mysite but not /etc/replication.

4. Query

The repository supports structured query languages:

  • JCR-SQL2 — SQL-like syntax for querying content

  • XPath — path-based querying (legacy, still supported)

  • QueryBuilder — AEM's own simplified wrapper API on top of JCR queries, commonly used in Sling Models

Java
// Example: QueryBuilder usage in a Sling Model
Map<String, String> queryParams = new HashMap<>();
queryParams.put("path", "/content/mysite/en/products");
queryParams.put("type", "cq:Page");
queryParams.put("property", "jcr:content/category");
queryParams.put("property.value", "electronics");

Query query = queryBuilder.createQuery(PredicateGroup.create(queryParams), session);
SearchResult result = query.getResult();

5. NodeStore: The Actual Persistence Backend

Oak is flexible about where the node data physically lives, through its NodeStore abstraction:

  • SegmentNodeStore (TarMK) — File-based storage on local disk, stored as .tar segment files inside crx-quickstart/repository/. This is the default for most on-premise, single-instance setups

  • DocumentNodeStore — Backed by MongoDB or an RDBMS, used for clustered/horizontally-scaled deployments where multiple AEM instances need to share one logical repository

  • Cloud-native storage — AEM as a Cloud Service uses a different, Adobe-managed storage backend entirely (not something you configure directly)

Where You Interact With This Layer

  • CRXDE Lite: http://localhost:4502/crx/de/index.jsp — browse and directly edit the raw repository tree

  • Package Manager: http://localhost:4502/crx/packmgr/index.jsp — export/import whole subtrees as content packages

  • The repository/ folder: physical files on disk (TarMK segments) — this is what you back up before any risky operation, as covered in the installation article

Why this matters for debugging: If content looks "stuck" or inconsistent, or a query returns stale results, the issue often lives at this layer — check for failed Oak index rebuilds, lock files, or repository consistency problems, all visible via the Web Console's Oak-specific diagnostic tools (/system/console/jmx-console Oak MBeans).


Layer 3: Web Application Framework — Apache Sling

What Sling Is

Apache Sling is a web framework built specifically to serve content directly out of a JCR repository, using RESTful principles. Where a traditional Java web framework (like Spring MVC) routes URLs to controller methods you write explicitly, Sling flips this: URLs map to repository paths, and Sling automatically figures out which script or servlet should render that resource.

This is the layer that actually handles incoming HTTP requests and decides how to turn repository content into an HTTP response.

The Sling Request Processing Model

When a browser requests http://localhost:4502/content/mysite/en.html, Sling does the following:

  1. Resource Resolution — Maps the URL path to a node in the JCR repository (/content/mysite/en)

  2. Resource Type Determination — Reads the sling:resourceType (or jcr:primaryType) property of that node to figure out what "kind" of resource this is (e.g., mysite/components/page)

  3. Script/Servlet Resolution — Searches /apps first, then /libs, for a script matching that resource type and the requested extension (.html → looks for page.html HTL script, or a registered Java servlet)

  4. Rendering — Executes the resolved script (typically an HTL template), which may itself include child components, each going through the same resolution process recursively

  5. Response — Returns the rendered output to the browser

Java
Request: GET /content/mysite/en/products/shoes.html
                      ↓
Resource: /content/mysite/en/products/shoes (a cq:Page node)
                      ↓
sling:resourceType = "mysite/components/page"
                      ↓
Script lookup: /apps/mysite/components/page/page.html
                      ↓
HTL renders page, which includes child components
(each child component repeats steps 2-4 independently)

Key Sling Concepts

1. The Resource API

Everything in Sling revolves around the Resource interface — a uniform abstraction over JCR nodes (and potentially other backends, though JCR is what AEM uses). This is what gets "adapted" into Sling Models:

Java
Resource resource = request.getResource();
ProductCard model = resource.adaptTo(ProductCard.class);

2. Adapters

The adaptTo() pattern is Sling's extensibility mechanism — letting any Resource (or Request) be converted into a different, more convenient type. Sling Models (covered in Sling-Models-Basics.md) are the most common adapter target you'll write, but AEM also adapts resources into Page, Asset, ValueMap, and many other built-in types.

3. Servlets and Script Resolution by Resource Type

Instead of registering servlets against URL patterns (as in traditional Java EE), Sling servlets are typically registered against a resource type:

Java
@Component(service = Servlet.class, property = {
    "sling.servlet.resourceTypes=mysite/components/productsearch",
    "sling.servlet.methods=GET"
})
public class ProductSearchServlet extends SlingSafeMethodsServlet {
    // Handles GET requests for any resource with this resourceType
}

4. HTL (HTML Template Language)

Sling's primary templating language (formerly called Sightly) for rendering resources into HTML — designed to be secure by default (auto-escaping output) and to cleanly separate markup from logic, which is why business logic belongs in Sling Models, not HTL expressions.

5. The /apps vs. /libs Overlay Pattern

Sling's script resolution always checks /apps before /libs. This is the mechanism behind AEM's customization model: Adobe ships default behavior in /libs, and you "overlay" it by placing your own script at the equivalent path under /apps — without ever modifying /libs directly (which would be overwritten on the next update anyway).

Where You Interact With This Layer

  • Sling Resource Resolution testing: Appending .json to almost any content URL dumps the raw resource as JSON, which is invaluable for debugging what Sling actually sees

  • Script debugging: http://localhost:4502/system/console/status-slingscripts shows registered scripts and resource type mappings

  • Request logs: crx-quickstart/logs/request.log shows exactly which resource/script resolved each incoming request

Why this matters for debugging: "My component isn't rendering" is almost always a Sling resolution problem — wrong sling:resourceType, a missing script at the expected /apps path, or a resourceType typo. Understanding the resolution algorithm above lets you trace exactly where the chain broke.


Layer 4: Application Layer — AEM Itself

What AEM Adds on Top

Everything below this layer (Felix, Oak, Sling) is generic — none of it knows anything about "pages," "components," "assets," or "workflows" as business concepts. AEM is the layer that takes this generic, resource-oriented, OSGi-modular foundation and builds the actual content management product on top of it.

Core AEM Application Concepts

1. WCM (Web Content Management)

AEM defines the concept of a Page (cq:Page node type) with structured authoring: templates, policies, and the Editable Templates system that lets authors (not developers) define page layout and allowed components through the UI.

2. Components

While Sling provides the generic resource-type-to-script resolution mechanism, AEM defines the component model on top of it: dialogs (_cq_dialog) for authoring, the component registration conventions, the Core Components library, and the drag-and-drop authoring experience (the "Touch UI" / Granite UI) that lets non-developers build pages from pre-built components.

3. DAM (Digital Asset Management)

AEM adds the dam:Asset node type and an entire processing pipeline on top of raw JCR binary storage: automatic rendition generation (thumbnails, responsive image sizes), metadata extraction (EXIF, IPTC), asset workflows, and smart tagging — none of which exist in plain JCR or Sling.

4. Workflows

AEM's workflow engine (built using OSGi services and JCR-stored workflow models) orchestrates multi-step business processes — content approval chains, DAM asset processing, scheduled publishing — none of which are concepts the lower layers know about.

5. Replication

The mechanism by which content moves from an author-run-mode instance to publish-run-mode instances (see Run Modes in AEM.md) is entirely an AEM application-layer concept, implemented using lower-layer primitives (HTTP calls between instances, JCR event listeners detecting content changes, OSGi services managing replication agents).

6. Personalization and Multi-Site Management

Features like audience targeting, A/B testing (ClientContext/Target integration), and Multi-Site Manager (Live Copies, Blueprints) are pure AEM business logic — built as OSGi services and components, but representing concepts entirely specific to AEM's purpose as a marketing/content platform.

How the Layers Connect: A Full Request Example

Tracing a real request through all four layers makes the architecture concrete. Request: GET /content/mysite/en/products.html

  1. OSGi (Felix): The HTTP Service bundle (part of the OSGi/Felix layer) receives the raw HTTP request and hands it to the registered Sling engine servlet

  2. Sling: Resolves /content/mysite/en/products to a Resource, reads its sling:resourceType, finds the matching HTL script under /apps/mysite/components/page

  3. JCR/Oak: Every property read during this process (jcr:title, sling:resourceType, child node iteration for components on the page) is a read operation against the Oak repository

  4. AEM: The resolved script renders an AEM Page component, which internally uses AEM's Core Components (an application-layer library) for the layout container, which in turn renders child components like a Product Carousel — each of which may invoke a Sling Model (adapting the Resource, per the Sling adapter mechanism) that calls an OSGi service (@Reference) to fetch live pricing data, which itself might query the JCR via QueryBuilder

Every request you make to AEM passes through all four layers, even though you'll spend most of your development time working at the AEM application layer (writing components and Sling Models) while only occasionally dropping down to raw Sling (custom servlets) or OSGi (custom services) concerns.


Why This Layered Architecture Matters

1. Independent Technology Evolution

Because each layer is a separate Apache project, improvements happen independently. Oak can get performance improvements or new NodeStore backends without AEM's application layer needing a rewrite. This is also why AEM as a Cloud Service could swap out storage internals while keeping the Sling/OSGi programming model developers already know.

2. Debugging Follows the Stack

When something breaks, experienced AEM developers mentally walk down the stack:

  • Is my component rendering wrong? → Check AEM/Sling script resolution

  • Is a service not available? → Check OSGi bundle status

  • Is data missing or wrong? → Check JCR/Oak content directly in CRXDE

  • Is the whole instance behaving strangely? → Check Felix bundle states and Oak repository health

3. Extensibility at Every Layer

You can extend AEM at any of these four layers depending on your need:

  • OSGi layer: Write a custom service for a cross-cutting concern (logging, caching)

  • JCR layer: Define custom node types for specialized content structures

  • Sling layer: Write a custom servlet for a REST API endpoint that doesn't map naturally to a page component

  • AEM layer: Build components, templates, and workflows — where most day-to-day development happens

4. It's Why AEM "Feels" Different from Other Java Web Apps

If you've worked with Spring Boot or plain Java EE before, AEM's resource-centric, path-based resolution (rather than explicit route registration) can feel unfamiliar at first. That unfamiliarity is entirely explained by the Sling layer's design philosophy: content and its location in the repository drive behavior, rather than a central routing table.


Common Pitfalls

Pitfall 1: Not Knowing Which Layer a Problem Lives In

The Mistake: Spending hours debugging "why a component won't render" by rewriting the HTL template, when the actual problem is that the backing bundle failed to start (OSGi layer issue).

Fix: Always check the bundle console (/system/console/bundles) first for any custom-code rendering issue — rule out the OSGi layer before assuming the problem is in your Sling/HTL code.

Pitfall 2: Editing /libs Directly

The Mistake: Modifying out-of-the-box AEM scripts or components directly under /libs because "it's faster than setting up an overlay."

Why It Happens: Misunderstanding the /apps-over-/libs resolution order central to the Sling layer.

Fix: Always create your customization under /apps, mirroring the /libs path — this is the entire point of the overlay pattern, and /libs changes are routinely wiped out by Service Pack updates.

Pitfall 3: Treating JCR Queries Like SQL Queries on a Relational Database

The Mistake: Writing JCR queries assuming relational-style joins and performance characteristics.

Why It Happens: JCR-SQL2's syntax looks similar to SQL, but the underlying engine is a hierarchical document/node store, not a relational database — query performance depends heavily on Oak indexes, which must be explicitly defined for custom property queries.

Fix: Always check that a custom Oak index exists for any property you query frequently; unindexed queries can silently traverse the entire repository and severely degrade performance.

Pitfall 4: Forgetting That Everything Is Ultimately One Repository

The Mistake: Assuming DAM assets, pages, and user accounts are stored in logically separate "systems" the way they might be in a traditional multi-database application.

Fix: Remember it's all one JCR tree — permission, versioning, and replication behavior are consistent across content types because they all go through the same storage layer.


Key Takeaways

The Four Layers

  1. OSGi (Apache Felix) — Dynamic module system: bundles, service registry, versioned classloading, configuration management. The foundation everything else runs inside

  2. JCR/Storage (Apache Jackrabbit Oak) — Hierarchical content repository: all AEM content (pages, assets, users, configs) is nodes and properties in one unified tree, persisted via pluggable NodeStores (TarMK, MongoDB, cloud-native)

  3. Web Application Framework (Apache Sling) — Resource-centric request processing: URLs map to repository paths, resource type drives script/servlet resolution, HTL renders output, adapters (like Sling Models) convert resources into usable Java objects

  4. Application Layer (AEM) — Business-specific functionality built on the three layers below: WCM, DAM, Workflows, Replication, Personalization, Multi-Site Management, Components, and the authoring UI itself

Why It's Built This Way

  • Each layer is an independently maintained Apache Software Foundation project, allowing focused innovation and reuse outside AEM

  • Clean separation lets you debug systematically — work down the stack from "is my AEM component wrong" to "is my resource type wrong" to "is my bundle active" to "is my repository healthy"

  • Extensibility is available at every layer, not just the top

Practical Implications

  • /apps overrides /libs — a Sling-layer convention that's central to safe AEM customization

  • Bundle status (OSGi layer) is the first thing to check when custom code doesn't behave

  • JCR queries need proper Oak indexes — treating them like relational SQL queries leads to performance problems

  • Run modes (covered in the previous article) hook directly into the OSGi Configuration Admin — tying back to Layer 1

Connected Flow Pathways

View Full Knowledge Graph

Read First

Where to Go Next

End of this branch. Explore the Flow Graph to discover other paths.