The core philosophy is everything is content. Pages, components, templates, configurations, code-resolved scripts - they all live as nodes in the JCR.
AEM Core Architecture
- Apache Sling - a REST-based web framework that maps URLs to content resources and resolves rendering scripts
- Apache Jackrabbit Oak - the JCR (Java Content Repository) implementation that stores all content as a tree of nodes and properties
- OSGi (Apache Felix) - a modular runtime where every piece of functionality is deployed as a bundle with explicit dependencies
The core philosophy is everything is content. Pages, components, templates, configurations, code-resolved scripts - they all live as nodes in the JCR. This single content tree drives URL resolution, rendering, and access control.
High-Level Architecture
A typical AEM deployment consists of three tiers:
Content flow
- An author creates or edits content on the Author instance.
- When the content is approved, the author activates (replicates) it.
- The replication agent serializes the content and pushes it to one or more Publish instances.
- A flush agent invalidates the Dispatcher cache for the affected paths.
- The next request from a browser triggers a cache miss, Dispatcher fetches the fresh page from Publish, caches it, and serves it.
The Sling Request Processing Pipeline
This is the heart of AEM. Every HTTP request - whether it renders a page, returns JSON, or serves an asset - follows the same pipeline.
Step 1: URL decomposition
Sling breaks every URL into structured parts:
Step 2: Resource resolution
The ResourceResolver maps the URL path to a JCR node. Resolution respects several mapping layers:
/etc/mapmappings - rewrite rules stored in the repository (e.g. strip/content/my-sitefrom external URLs)sling:alias- a property on a node that provides an alternative URL segment (e.g.sling:alias=homeon a node namedhome-page)- Vanity URLs - a
sling:vanityPathproperty that creates a shortcut (e.g./promomaps to/content/my-site/en/campaigns/summer) - Direct path - if no mapping matches, the path is resolved directly against the JCR tree
The result is a Resource object that wraps the JCR node and exposes its properties.
Use /etc/map for environment-specific URL shortening (e.g. stripping /content/my-site/en on publish) rather than hardcoding shortened URLs in your code. This keeps author and publish configurations independent.
Step 3: Script and servlet resolution
Once the resource is found, Sling reads its sling:resourceType property to locate the rendering script or servlet. The resolution follows a well-defined search order:
Key rules:
/appsis always searched before/libs- this is how overlays and customisations work- Selectors narrow the script selection:
nav.htmlis preferred overpage.htmlwhen the.navselector is present - The
sling:resourceSuperTypechain enables component inheritance - if no script is found on the current resource type, Sling walks up the super-type chain
Step 4: Rendering
The located HTL template is executed:
data-sly-usestatements instantiate Sling Models (or other Use-objects)- Sling Models are adapted from the request or resource, injecting properties via annotations
- The HTL expression language renders the model data into HTML
data-sly-resourceincludes trigger nested resource resolution for child components- The final HTML response is returned to the client
Content Model (JCR Repository)
All content in AEM is stored as a tree of nodes and properties in the JCR.
Common node types
Page anatomy
Every cq:Page node has the same structure:
- The
cq:Pagenode itself holds very little data - just the page structure - All authored content lives on the
jcr:contentchild and its descendants - Each component instance is a child node under the layout container, with its own
sling:resourceTypeand authored properties
Property value types
Content hierarchy as URL structure
The JCR tree directly maps to URLs:
This means content architecture decisions (site structure, language copies, page hierarchy) directly impact your URL scheme. Plan your content tree carefully - restructuring later requires redirects and can break SEO.
Component Architecture
AEM components follow an MVC-like pattern where the roles are split across four artifacts:

A minimal component example
Dialog (defines what the author can configure):
Sling Model (reads the data, applies logic):
HTL template (renders the output):
When to use which Use-object
Always prefer Sling Models. They are the recommended approach by Adobe and the community. See the Sling Models page for detailed annotation reference and patterns.
Older codebases may still use WCMUsePojo (extend the class, override activate(), look up properties manually). It predates Sling Models, has no dependency injection, is harder to unit test, and should not be used in new code. If you inherit a project that uses it, migrate to Sling Models as components are touched.
Data Input
Content enters AEM through several channels:
Author UI (primary)
- Page Editor - drag-and-drop components, inline editing, Touch UI dialogs
- Content Fragment Editor - structured content authoring with predefined models
- Experience Fragment Editor - reusable experience building blocks (header, footer, promo)
- DAM - asset upload, metadata editing, processing profiles, Smart Tags
Programmatic input
SlingPostServlet- AEM's built-in POST handler. A simple form POST to a resource path creates or updates properties:
- Assets HTTP API - REST API for CRUD operations on DAM assets
- Content Fragment API - create, update, and manage Content Fragments programmatically
- Package Manager - import
.zipcontent packages (useful for migrations) - Groovy Console - run scripts for bulk content operations (see Groovy Console)
SlingPostServlet dangerous on Publish. Hammer nguy hiểm nếu expose trên Publish. Ensure it is restricted via OSGi configuration and blocked by the Dispatcher on all public-facing instances.
Data Retrieval
Sling Resource API (recommended)
The highest-level API. Works with Resource objects and ValueMap for type-safe property access.
JCR API (lower level)
Direct JCR node and property access. Use when you need transactions, observation, or node-type-specific operations.
QueryBuilder API
AEM's predicate-based query API. Returns Resource -based results and supports pagination.
Common predicates:
JCR-SQL2
SQL-like query language for complex queries, especially joins. Prefer QueryBuilder for simple lookups and SQL2 for advanced scenarios.
Every query must be backed by an Oak index. Queries that trigger an index traversal are extremely slow and will log WARN messages like Traversed 10000 nodes. Check the query plan with EXPLAIN and create custom Oak indexes where needed.
GraphQL (headless)
AEM's Content Fragment GraphQL API enables headless delivery:
Use persisted queries for production to enable CDN caching and avoid exposing arbitrary queries.
Sling Model Exporter
Export component data as JSON without writing a custom servlet:
Now requesting /content/my-site/en/home.model.json returns the page's component data as JSON.
OSGi Services and Dependency Injection
Business logic that is shared across components, servlets, or workflows should live in OSGi services.
Service basics
Inject into a Sling Model:
OSGi configuration and run modes
Configuration files live in ui.config and are scoped by run mode:
Service users (avoid admin sessions)
Never use loginAdministrative(). Define a service user with minimal permissions:
Always use try-with-resources for service resource resolvers. A leaked resolver means a leaked JCR session, which eventually exhausts the session pool and brings down the instance.
Caching and Delivery
Dispatcher caching
The Dispatcher caches rendered HTML pages as static files on the Apache httpd filesystem. Key configuration areas:
Sling Dynamic Include (SDI)
For pages with a mix of static and dynamic content (e.g. personalised header on an otherwise static page), SDI replaces component output with an SSI (Server Side Include) or ESI tag at the Dispatcher level:
This lets you cache 90% of the page while keeping dynamic fragments fresh.
CDN integration
For AEM as a Cloud Service, Adobe provides a built-in CDN (Fastly). For AMS or on-premise, place a CDN (CloudFront, Akamai, Fastly) in front of the Dispatcher and configure:
- Cache-Control headers from AEM/Dispatcher
- Cache invalidation API triggered by replication events
- Origin shielding to reduce load on Publish
Common Pitfalls
A practical reference of issues that regularly trip up AEM developers:
Dialog and authoring
- Missing
./prefix in field names - properties won't be persisted to the JCR. Always usename="./myProperty" - Mixing Coral 2 and Coral 3 - components from
/components/foundation(Coral 2) and/components/coral/foundation(Coral 3) can cause visual and functional inconsistencies - Not handling
nullin Sling Models - always usedefaultInjectionStrategy = OPTIONALor null-safe getters. Missing dialog fields returnnull, not empty strings
Resource resolvers and sessions
- Closing a request-scoped resolver - resolvers injected via
@SlingObjectorrequest.getResourceResolver()are managed by Sling and closed automatically at the end of the request. Do not close them yourself - doing so breaks other components that still need the resolver during the same request - Using a request-scoped resolver in async threads - the resolver is closed when the request ends, so background threads will get
IllegalStateException. Open a dedicated service resolver for async work - Not closing manually opened resolvers - service resolvers obtained via
getServiceResourceResolver()are your responsibility. Always use try-with-resources. A leaked resolver means a leaked JCR session
Queries and performance
- Queries without Oak indexes - triggers traversal (full scan). Always check with
EXPLAIN SELECT ...and create indexes for production queries - Using deprecated
xpathqueries - use JCR-SQL2 or QueryBuilder instead p.limit=-1in QueryBuilder - returns all results without pagination. Fine for small datasets, dangerous for large ones. Always paginate in production code
Content and URLs
- Hardcoding
/content/dam/...paths - use authored references (pathfield dialogs) instead. Hardcoded paths break when content is moved - Relying on JCR node ordering - not all Oak persistence stores guarantee child node order. Use explicit sort properties if order matters
- Ignoring
/etc/mapon publish - URLs that work on author (with/content/my-site/en/...) will 404 on publish if URL mappings are not configured
Dispatcher and caching
- "Works on author, broken on publish" - the Dispatcher strips query parameters, blocks certain paths, and caches aggressively. Always test with Dispatcher enabled during development
- Not invalidating on publish - ensure flush agents are correctly configured for every Publish instance
- Caching personalised content - if the page varies by user, either use SDI for the dynamic fragments or set
Dispatcher: no-cacheheaders on that path