> For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt.

# AEM Backend Development Complete Guide

"AEM backend development" is a broad phrase. On any given day it can mean building a component, wiring an editable template, writing a Sling Model, reacting to a content change, running a nightly job, or kicking off a publish from code. This guide is a single map of that whole surface — the everyday building blocks first, then the advanced server-side pieces that turn a content site into a real application.

The foundational topics here each have a dedicated deep-dive elsewhere on this blog, so I'll explain _what each is and how the pieces fit together_ and link you onward for the full treatment — that keeps this guide cohesive without repeating thousands of words. The **advanced** half — custom workflow steps, event handlers, listeners, scheduled jobs, and replication APIs — gets full, original depth, because that's where most "how do I actually do this?" questions live. As always, there's a cheat sheet, best practices, and do's & don'ts at the end.

If you're new to the platform, start with the [AEM Developer Cheat Sheet](https://www.kumaraman.in/blog/aem-developer-cheat-sheet); the building blocks below assume that vocabulary.

## The building blocks

These are the pieces you assemble into a typical authored experience. They're introduced here in the order content actually flows — from the template that defines a page, to the component an author drops onto it, to the Java and markup that render it.

### Components

A **component** is the unit authors place on a page — a node of type `cq:Component` whose `sling:resourceType` links content to the script that renders it. The modern convention is to create a thin **proxy** that inherits an Adobe Core Component via `sling:resourceSuperType`, then customize only what you need, so you inherit accessibility, the data layer, and JSON export for free.

```
<jcr:root jcr:primaryType="cq:Component"
          jcr:title="Teaser" componentGroup="My Site - Content"
          sling:resourceSuperType="core/wcm/components/teaser/v2/teaser"/>
```

The full anatomy — edit config, design dialog, the render flow — is covered in the [Component Development guide](https://www.kumaraman.in/blog/aem-component-development-complete-guide).

### Templates, editable templates & policies

A **template** defines the structure a new page starts with. Modern projects use **editable templates** (stored in `/conf`, authored in the Template Editor) rather than legacy static templates, because they stay _dynamically linked_ to their pages — update the template's structure and existing pages reflect it. A template's per-component configuration lives in **policies** (also in `/conf`), which are reusable settings applied _in the context of a template_. The same component can behave differently on different templates purely through policies, with no code change.

The distinction and the `/conf` layout are explained in detail in the [AEM Developer Cheat Sheet](https://www.kumaraman.in/blog/aem-developer-cheat-sheet#editable-templates-vs-static-templates-and-policies).

### Client libraries

CSS and JavaScript are delivered through **client libraries** — `cq:ClientLibraryFolder` nodes with `categories`, `dependencies`, and `embed` metadata. AEM bundles, minifies, and versions them, and you serve them via the `/etc.clientlibs` proxy so the `/apps` tree is never exposed. Include one from HTL with the built-in clientlib template. (See the [Developer Cheat Sheet](https://www.kumaraman.in/blog/aem-developer-cheat-sheet#client-libraries-clientlibs) for the full breakdown of categories, dependencies, and embeds.)

### Dialogs & multifields

A **dialog** (`_cq_dialog`) is the authoring form, built from Granite UI fields organized into tabs; each field's `name` attribute decides which JCR property the value is saved to. A **multifield** is the special field for repeating groups — a composite multifield stores each entry as a child node, which you later read as a `List<Resource>` in your model. Both are covered, with full XML, in the [Component Development guide](https://www.kumaraman.in/blog/aem-component-development-complete-guide#touch-ui-dialogs).

### Sling Models & HTL

The logic and markup layers. A **Sling Model** adapts a `Resource` or request into a typed Java object — all your component logic belongs here, exposed through getters. **HTL** then binds that model and renders escaped, XSS-safe markup, with _no_ logic of its own.

```
@Model(adaptables = Resource.class,
       defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class TeaserModel {
    @ValueMapValue private String heading;
    public String getHeading() { return heading; }
}
```

```
<sly data-sly-use.teaser="com.mysite.core.models.TeaserModel"/>
<h2>${teaser.heading}</h2>
```

Go deep on Models in the [Annotations reference](https://www.kumaraman.in/blog/aem-annotations-complete-reference) and the [Sling guide](https://www.kumaraman.in/blog/apache-sling-framework-complete-guide), and on templating in the [HTL cheat sheet](https://www.kumaraman.in/blog/htl-cheat-sheet-and-reference).

### Context-Aware Configuration

When configuration needs to vary **by content path** — a different API key per brand, a feature flag per region — a single global OSGi config can't express it. **Context-Aware Configuration (CA Config)** resolves values based on a content tree's `sling:configRef`, so the same code returns the right value for each site. (Editable-template policies are themselves built on it.) Full example in the [Sling guide](https://www.kumaraman.in/blog/apache-sling-framework-complete-guide#sling-context-aware-configuration).

## Advanced backend

This is where AEM becomes an application platform rather than a content renderer. Each of the following lets your code participate in AEM's runtime — reacting to content, automating processes, and moving content between tiers.

### Custom workflow steps

**Workflows** automate content processes — review-and-publish, asset processing, translation. A workflow is a series of steps, and the most common custom step is a **process step**: a piece of Java that runs when the workflow reaches it. You implement the Granite `WorkflowProcess` interface and register it as an OSGi service with a `process.label`, which is how authors select it in the workflow model.

```
@Component(
    service = WorkflowProcess.class,
    property = { "process.label=My Site - Publish Approver" })
public class PublishApproverStep implements WorkflowProcess {

    @Override
    public void execute(WorkItem workItem, WorkflowSession wfSession, MetaDataMap args)
            throws WorkflowException {
        // The content the workflow is acting on (e.g. a page path)
        String payloadPath = workItem.getWorkflowData().getPayload().toString();

        // Step arguments configured on the model (PROCESS_ARGS)
        String config = args.get("PROCESS_ARGS", String.class);

        // ... do the work: validate, transform, replicate, notify ...
    }
}
```

The key objects: the **`WorkItem`** carries the **payload** (the path the workflow is operating on), the **`WorkflowSession`** lets you advance, route, or complete the workflow, and the **`MetaDataMap`** delivers the step's configured arguments. Once deployed, the step shows up in the workflow model's _Process Step_ component, selected by its `process.label`.

> **Tip:** Keep workflow steps fast and resilient. They run inside the workflow engine, so a step that blocks on a slow external call holds up the whole workflow. For heavy or unreliable work, have the step _enqueue a Sling Job_ (below) and complete quickly.

### Event handlers

An **event handler** lets your code react to things happening inside AEM — a resource added, a page modified, content replicated. AEM publishes these as OSGi events, and you subscribe by implementing `org.osgi.service.event.EventHandler` and declaring which topics you care about.

```
@Component(
    service = EventHandler.class,
    property = {
        EventConstants.EVENT_TOPIC + "=" + SlingConstants.TOPIC_RESOURCE_ADDED
    })
public class ResourceAddedHandler implements EventHandler {
    @Override
    public void handleEvent(Event event) {
        String path = (String) event.getProperty(SlingConstants.PROPERTY_PATH);
        // react to the newly added resource
    }
}
```

Common topics include the Sling resource events (`TOPIC_RESOURCE_ADDED`, `_CHANGED`, `_REMOVED`), replication events, and page events. Two caveats matter: event handlers run **on the instance where the event fires** (not cluster-wide), and they're invoked by the OSGi EventAdmin, so a slow handler can back up event delivery — keep them lightweight, and offload real work to a job.

### Listeners

When you specifically need to observe **content changes** under a path, the modern, recommended tool is Sling's **`ResourceChangeListener`** — it's path-scoped, type-filtered, and cleaner than the low-level JCR `ObservationManager` / `EventListener` API it replaces.

```
@Component(
    service = ResourceChangeListener.class,
    property = {
        ResourceChangeListener.PATHS + "=/content/mysite",
        ResourceChangeListener.CHANGES + "=ADDED",
        ResourceChangeListener.CHANGES + "=CHANGED",
        ResourceChangeListener.CHANGES + "=REMOVED"
    })
public class ContentChangeListener implements ResourceChangeListener {
    @Override
    public void onChange(List<ResourceChange> changes) {
        for (ResourceChange change : changes) {
            // change.getType(), change.getPath()
        }
    }
}
```

Prefer `ResourceChangeListener` over a raw JCR listener for almost everything — you declare the paths and change types as service properties, and Sling handles the wiring. As with event handlers, observation happens per-instance, so if a content change must trigger _exactly one_ downstream action across a cluster, observe the change and then enqueue a **Sling Job** rather than acting directly in the callback.

> **Note:** Event handlers and resource listeners overlap. Rule of thumb: use a **`ResourceChangeListener`** when you're watching content changes under known paths; use an **`EventHandler`** for non-resource events (replication, workflow, custom OSGi events) identified by topic.

### Scheduled jobs

Server-side work that runs on a timetable uses the **Sling Scheduler**; work that must run _reliably and exactly once_ across a cluster uses **Sling Jobs**. The distinction is critical on AEM as a Cloud Service, where instances scale horizontally.

A scheduled task is a `Runnable` configured with a cron expression:

```
@Component(service = Runnable.class, immediate = true)
@Designate(ocd = NightlyTask.Config.class)
public class NightlyTask implements Runnable {
    @ObjectClassDefinition(name = "Nightly Task")
    public @interface Config {
        @AttributeDefinition(name = "Cron") String scheduler_expression() default "0 0 2 * * ?";
    }
    @Override public void run() { /* periodic work */ }
}
```

The catch is that a scheduled task fires on **every** instance it's deployed to. When you need a job to run only once cluster-wide — sending an email, calling an API — hand it to the `JobManager` and process it in a `JobConsumer`, which Sling dispatches to a single instance and retries on failure. Both patterns, and exactly when to choose each, are detailed in the [Apache Sling guide](https://www.kumaraman.in/blog/apache-sling-framework-complete-guide#sling-jobs).

### Replication APIs

Publishing content from code — after a workflow approves it, or in a custom import — uses the **`Replicator`** service. You call it with a session, an action, and a path:

```
@Reference
private Replicator replicator;

public void publish(ResourceResolver resolver, String path) throws ReplicationException {
    Session session = resolver.adaptTo(Session.class);
    replicator.replicate(session, ReplicationActionType.ACTIVATE, path);
}
```

The `ReplicationActionType` selects the operation — **ACTIVATE** (publish), **DEACTIVATE** (unpublish), or **DELETE** — and a `ReplicationOptions` argument can make the call synchronous or apply a filter. On classic AEM this drives the replication agents; on AEM as a Cloud Service the same API runs over Sling Content Distribution under the hood, so your code stays the same. A common pattern is to call the Replicator from a custom **workflow step** so that approval and publishing are one automated flow.

> **Important:** Replicate with a properly-permissioned **service user**, never an admin session, and remember that activation also drives **Dispatcher cache invalidation** — see the [Dispatcher guide](https://www.kumaraman.in/blog/aem-dispatcher-the-complete-guide) for how the flush propagates.

## Cheat sheet

| Need                    | Use                                 | Registered as                                       |
| ----------------------- | ----------------------------------- | --------------------------------------------------- |
| Render authored content | Component + Sling Model + HTL       | `cq:Component`, `@Model`                            |
| Page structure & config | Editable template + policies        | `/conf`                                             |
| Ship CSS/JS             | Client library                      | `cq:ClientLibraryFolder`                            |
| Author input            | Dialog + (multifield)               | `_cq_dialog`                                        |
| Per-path config         | Context-Aware Configuration         | `@Configuration`                                    |
| Custom workflow logic   | `WorkflowProcess`                   | `service = WorkflowProcess.class` + `process.label` |
| React to OSGi events    | `EventHandler`                      | `event.topics` property                             |
| Watch content changes   | `ResourceChangeListener`            | `resource.paths` + `resource.change.types`          |
| Periodic task           | `Runnable` + `scheduler.expression` | runs per instance                                   |
| Once-only task          | `JobManager` + `JobConsumer`        | runs once across cluster                            |
| Publish from code       | `Replicator`                        | `replicate(session, ACTIVATE, path)`                |

## Best practices

- ✅ Build components as **proxies of Core Components**; keep logic in **Sling Models**, markup in **HTL**.
- ✅ Configure with **editable templates, policies, and CA Config** rather than code branches.
- ✅ Keep **workflow steps, event handlers, and listeners fast** — offload real work to **Sling Jobs**.
- ✅ Prefer **`ResourceChangeListener`** over raw JCR observation.
- ✅ Use the **Scheduler** for periodic work, **Jobs** for once-only work.
- ✅ Replicate via the **`Replicator`** with a **service user**, and account for cache invalidation.

## Do's and Don'ts

**Do**

- ✅ Enqueue a Sling Job from a workflow step / listener when work is heavy or must run once.
- ✅ Scope listeners and event handlers tightly (paths, topics, change types).
- ✅ Call the Replicator from a workflow step to automate approve-and-publish.

**Don't**

- ❌ Don't block inside a workflow step, event handler, or listener with slow external calls.
- ❌ Don't use the Scheduler for cluster-wide once-only work — it runs on every instance.
- ❌ Don't use raw JCR `EventListener` when `ResourceChangeListener` will do.
- ❌ Don't replicate with an admin session — use a service user.
- ❌ Don't put business logic in HTL or duplicate config in code that belongs in policies / CA Config.

## Wrapping up

AEM backend development is a layered craft: the **building blocks** (components, templates, policies, client libraries, dialogs, Sling Models, HTL, CA Config) compose the authored experience, and the **advanced pieces** (workflow steps, event handlers, listeners, scheduled jobs, and replication) let your code drive AEM's runtime. The thread connecting the advanced half is discipline about _where work runs_: keep callbacks fast, push heavy or once-only work to Sling Jobs, and always act with a scoped service user.

From here, go deep where you need it — the [Component Development guide](https://www.kumaraman.in/blog/aem-component-development-complete-guide), the [Apache Sling guide](https://www.kumaraman.in/blog/apache-sling-framework-complete-guide), the [OSGi guide](https://www.kumaraman.in/blog/osgi-in-aem-complete-guide), and the [Annotations reference](https://www.kumaraman.in/blog/aem-annotations-complete-reference) — and scaffold the boilerplate with my [AEM Component Generator](https://www.kumaraman.in/tools/aem-component).
