---
name: aem-backend-development
description: Write production-grade Adobe Experience Manager back-end Java for AEM as a Cloud Service (with 6.5 notes) — OSGi Declarative Services, runmode configs, Sling servlets and filters, service users, queries and Oak indexes, jobs, schedulers, workflows and replication. Use this whenever the user writes or debugs an OSGi @Component, @Designate/@ObjectClassDefinition config, .cfg.json in ui.config, $[env:] or $[secret:] values, a SlingSafeMethodsServlet or SlingAllMethodsServlet, @SlingServletResourceTypes, a Sling filter, ResourceResolverFactory or service user and repoinit, QueryBuilder or JCR-SQL2, a custom oak:index, JobConsumer, scheduler cron, ResourceChangeListener, EventHandler, WorkflowProcess or launcher, Replicator or content distribution, or asks why code fails on Cloud Service or leaks sessions. Also apply it when reviewing AEM core bundle code or migrating 6.5 code to Cloud Service.
metadata:
  technology: AEM
  type: development
---

# AEM Back-end Development

AEM back-end code runs as long-lived OSGi singletons inside a horizontally scaled, auto-restarted cluster. Write every service as if it serves concurrent requests on several pods at once, with an immutable `/apps`, no admin access, and a restart that can happen at any moment — then it works on 6.5 too.

## 1. Cloud Service vs 6.5 — know which rules apply

| Topic | AEM as a Cloud Service | AEM 6.5 (on-prem / AMS) |
|---|---|---|
| `/apps`, `/libs` | Immutable at runtime; only deployable via `ui.apps` | Writable (but treat as immutable anyway) |
| Runmodes | Fixed: `author`/`publish` × `dev`/`stage`/`prod` (+ `rde`) | Arbitrary custom runmodes |
| Config values | `$[env:NAME;default=x]`, `$[secret:NAME]` via Cloud Manager | Per-environment files or `sling:OsgiConfig` nodes |
| Publish tier | Independent, disposable instances; no cluster, no writes that must survive | Persistent publish instances |
| Replication | Sling Content Distribution via an Adobe pipeline; agents not configurable | Replication agents you configure |
| Logs | `logs/error.log` only; levels per environment | Custom log files allowed |
| Indexes | Named `<name>-custom-<n>`, deployed in code | Free naming; reindex manually |

## 2. OSGi Declarative Services

```java
@Component(service = PriceService.class)
@Designate(ocd = PriceServiceImpl.Config.class)
public class PriceServiceImpl implements PriceService {
    @ObjectClassDefinition(name = "Acme – Price Service")
    public @interface Config {
        @AttributeDefinition(name = "Endpoint") String endpoint() default "https://api.example.com";
        @AttributeDefinition(name = "Timeout (ms)") int timeout() default 3000;
    }
    private volatile Config config;                    // swapped atomically on @Modified

    @Reference private HttpClientFactory httpClientFactory;     // static, mandatory (default)

    @Reference(cardinality = ReferenceCardinality.MULTIPLE,
               policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY)
    private volatile List<PriceRule> rules;            // replaced as a whole when services come/go

    @Activate @Modified
    protected void activate(Config config) { this.config = config; }
}
```

- **Program to interfaces**, `service = Interface.class`; keep the impl in an `impl` package that is not exported.
- **Static reluctant** references (default) restart your component when the dependency changes — simple and safe. Use **dynamic + volatile** only for optional or multiple references that come and go.
- `@Modified` lets config change without deactivation; without it, a config change restarts the component and all dependents.
- **Service ranking** (`Constants.SERVICE_RANKING`) decides which impl wins for a unary reference; use it to override, never rely on bundle start order.
- **Thread safety:** components are singletons. No request state in fields; `ResourceResolver`, `Session` and `SimpleDateFormat` are not thread-safe; immutable config objects in `volatile` fields.

## 3. OSGi configuration per runmode (`ui.config`)

```
ui.config/src/main/content/jcr_root/apps/acme/osgiconfig/
  config/                com.acme.core.impl.PriceServiceImpl.cfg.json
  config.author/         …
  config.publish.prod/   …
  config/                org.apache.sling.commons.log.LogManager.factory.config~acme.cfg.json
```

```json
{ "endpoint": "$[env:PRICE_ENDPOINT;default=https://api.example.com]",
  "apiKey": "$[secret:PRICE_API_KEY]",
  "timeout:Integer": 3000 }
```

- Factory configs use `PID~name.cfg.json`; typed values use `"key:Integer"` style only where the type isn't inferable from JSON.
- Most specific runmode folder wins; don't duplicate a full config into five folders — put defaults in `config/`, override only what differs, or use one config with env vars.
- Secrets never in Git: `$[secret:]` on Cloud Service, a vault-fed mechanism on 6.5.

## 4. Sling servlets and filters

| Choice | Default | Change when… |
|---|---|---|
| Binding | `@SlingServletResourceTypes` (+ selectors/extension) | Almost never path-bound: `@SlingServletPaths` bypasses repository ACLs, can't be secured per resource, must be allow-listed in the servlet resolver's execution paths, and is unreachable through Dispatcher without extra rules |
| Base class | `SlingSafeMethodsServlet` (GET/HEAD) | `SlingAllMethodsServlet` only for state-changing POST/PUT/DELETE |
| Output | JSON with a specific selector + extension (`.prices.json`) | Selectors make responses cacheable at Dispatcher/CDN |

```java
@Component(service = Servlet.class)
@SlingServletResourceTypes(resourceTypes = "acme/components/product",
        methods = HttpConstants.METHOD_GET, selectors = "prices", extensions = "json")
public class ProductPricesServlet extends SlingSafeMethodsServlet { … }
```

- **CSRF:** mutating requests go through the Granite CSRF filter; front-end sends the token from `/libs/granite/csrf/token.json`. Never add your path to CSRF exclusions to "fix" a 403 — send the token.
- Validate selectors/suffix/parameters; return proper status codes (400/404/405), never a 200 with an error body.
- **Filters:** `@SlingServletFilter(scope = SlingServletFilterScope.REQUEST, resourceTypes = …, pattern = …)` with an explicit `service.ranking`. Keep them cheap and narrowly scoped — they run on every matching request, including cache-miss floods.

## 5. ResourceResolver lifecycle and service users

- Request code uses `request.getResourceResolver()` — never close it.
- Background code uses a **service user**: repoinit creates the user and ACLs, `ServiceUserMapperImpl.amended~acme.cfg.json` maps `com.acme.core:price-sync=[acme-price-sync]`.

```
create service user acme-price-sync with path system/acme
set ACL for acme-price-sync
    allow jcr:read,rep:write on /content/acme
end
```

```java
try (ResourceResolver rr = resolverFactory.getServiceResourceResolver(
        Map.of(ResourceResolverFactory.SUBSERVICE, "price-sync"))) {
    // work, then rr.commit()
} catch (LoginException e) { LOG.error("Service user mapping missing for price-sync", e); }
```

- Least privilege per sub-service; one sub-service per concern, not one "acme-admin" for everything.
- `loginAdministrative` / `getAdministrativeResourceResolver` are deprecated and blocked on Cloud Service. Never store a resolver in a field.
- Prefer the **Sling Resource API** (`Resource`, `ValueMap`, `ModifiableValueMap`, `adaptTo`); drop to JCR (`Node`, `Session`) only for versioning, locking, node types or ACL work.

## 6. Queries and Oak indexes

- First ask: **can I navigate instead of query?** Known paths → `getChild`/`listChildren`. Queries are for unbounded search.
- JCR-SQL2 with `ISDESCENDANTNODE`, a node type, and property constraints is explicit and easy to index; QueryBuilder is fine for author UIs and predicates but set `p.limit`, `p.guessTotal=true` and avoid `p.hits=full`.
- Every production query must hit an index — check with the Explain Query tool; a **traversal warning is a bug**, not a log nuisance.
- Cloud Service indexes live in code (`ui.apps`, `/oak:index`): new ones as `acme.productLucene-1-custom-1`, extensions of product indexes as e.g. `damAssetLucene-<v>-custom-1`; bump the suffix to change it (indexes are immutable once deployed). Include only the properties you query/sort on, with `tags`/`selectionPolicy` where supported so your index isn't picked up by unrelated queries.

## 7. Background work: Jobs, schedulers, events, workflows

| Need | Use | Why |
|---|---|---|
| Guaranteed, retryable, once-per-cluster work | **Sling Job** (`JobManager.addJob`, `JobConsumer`) | Persisted, retried, distributed; survives restarts |
| Periodic task | **Scheduled Sling Job** (`createJob(topic).schedule().cron(…).add()`) | Runs once per cluster; plain `Scheduler` with `scheduler.runOn=LEADER` depends on topology and runs on *every* publish pod |
| React to content changes | `ResourceChangeListener` (paths + change types) → enqueue a Job | Listener must return in milliseconds |
| React to OSGi events (e.g. replication) | `EventHandler` on specific topics → enqueue a Job | Same; never do I/O in the handler |
| Human steps, approvals, audit trail, author-visible progress | **AEM Workflow** (`WorkflowProcess`, launcher) | Heavyweight: persisted instances, purge needed; use transient workflows when no history is needed |

- Job consumers return `JobResult.OK` / `FAILED` (retry) / `CANCEL` (give up) — make them **idempotent**.
- Cloud Service: no long-running or stateful work on publish (instances are recycled); asset renditions belong to asset microservices, post-processing workflows only for custom steps.
- Scheduler jobs: `scheduler.concurrent=false`; never hold a resolver across runs.

## 8. Replication and content mutability

- Activate via `Replicator.replicate(session, ReplicationActionType.ACTIVATE, path, options)` or the Sling Distribution API; on Cloud Service this rides Sling Content Distribution and supports the preview tier via an agent filter.
- Mutable areas: `/content`, `/conf`, `/var`, `/home`; `/oak:index` is special — deployed from code, never edited at runtime. Code never writes to `/apps` at runtime; content packages for `ui.content` must not contain `/apps`.
- Writes on Cloud Service publish are local to one pod and lost on recycle — user data goes to an external store or back to author via a sanctioned flow.

## 9. Logging and error handling

- SLF4J with parameterized messages (`LOG.debug("Synced {} products", count)`); no string concatenation, no `e.printStackTrace()`.
- Log config via `LogManager.factory.config~acme.cfg.json` targeting `logs/error.log`; DEBUG only in dev.
- Catch specific exceptions (`PersistenceException`, `LoginException`, `RepositoryException`); log once at the boundary with context (path, job id), not at every layer.
- `rr.revert()` on failure before rethrowing; commit in batches (e.g. every 500 changes) for bulk writes to keep the session small.

## Deliverable format

1. **Target**: Cloud Service or 6.5, runmodes affected.
2. **Design**: components, their interfaces and references, and which background mechanism was chosen (with the reason).
3. **Code**: Java classes, `.cfg.json` per runmode folder, repoinit and service-user mapping, index definition if a query was added.
4. **Security**: service user privileges, servlet binding and CSRF handling, Dispatcher filter/cache rules needed.
5. **Tests**: AEM Mocks unit tests for services/servlets; note what needs an integration test.
6. **Ops notes**: env vars/secrets to create in Cloud Manager, log categories, reindex impact.

## Anti-patterns to reject

- Admin sessions or `loginAdministrative`; resolvers stored in fields or never closed.
- Path-bound servlets under `/bin` for new features; CSRF exclusions to silence errors.
- Request or user state in OSGi component fields; non-volatile dynamic references.
- Queries without an index, `p.limit=-1` on user-facing paths, traversal warnings ignored.
- Long work inside `EventHandler`, `ResourceChangeListener` or filters; `Scheduler` jobs that must run exactly once.
- Workflows used as a job queue; unpurged workflow instances.
- Writing to `/apps` at runtime; relying on publish-side persisted writes on Cloud Service.
- Hardcoded endpoints, credentials or environment names in Java; secrets in `.cfg.json`.
- Custom log files on Cloud Service; `System.out`; swallowed exceptions.
