Skip to main content

Java Best Practices in AEM

This page collects practical Java guidelines for AEM projects. The goal is consistent, safe code that is easy to test and maintain.

Core principles​

  • Keep Sling Models small and focused.
  • Prefer services for reusable business logic.
  • Fail safely and log only what you need.
  • Avoid direct JCR APIs unless necessary.

Prefer Resource API over JCR when possible​

Resource resource = resolver.resolve("/content/my-site/en");
if (ResourceUtil.isNonExistingResource(resource)) {
return;
}
ValueMap props = resource.getValueMap();
String title = props.get("jcr:title", String.class);

Prefer early returns​

Early returns make code easier to read and reduce deep nesting, especially around error handling.

public String getTitle(ResourceResolver resolver, String path) {
Resource resource = resolver.resolve(path);
if (ResourceUtil.isNonExistingResource(resource)) {
return "";
}

String title = resource.getValueMap().get("jcr:title", String.class);
return StringUtils.defaultIfBlank(title, "Untitled");
}

Sling Models​

  • Use DefaultInjectionStrategy.OPTIONAL and handle missing values.
  • Keep logic out of HTL; expose computed values from the model.
  • Avoid heavy repository queries in models.
@Model(
adaptables = SlingHttpServletRequest.class,
adapters = {ComponentExporter.class},
resourceType = MyComponent.RESOURCE_TYPE,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class MyComponent implements ComponentExporter {
public static final String RESOURCE_TYPE = "myproject/components/mycomponent";

@ValueMapValue
private String title;

public String getTitle() {
return StringUtils.defaultIfBlank(title, "Default title");
}

@Override
public String getExportedType() {
return RESOURCE_TYPE;
}
}

Services and OSGi​

  • Keep service interfaces small and explicit.
  • Use OSGi configs for endpoints and flags.
  • Avoid static singletons; rely on OSGi lifecycle.
@ObjectClassDefinition(
name = "My Service",
description = "Example configuration for MyService"
)
public @interface MyServiceConfig {
@AttributeDefinition(name = "Enabled")
boolean enabled() default true;

@AttributeDefinition(name = "API Base URL")
String apiBaseUrl() default "https://api.example.com";
}

@Component(service = MyService.class)
@Designate(ocd = MyServiceConfig.class)
public class MyServiceImpl implements MyService {
private boolean enabled;
private String apiBaseUrl;

@Activate
protected void activate(MyServiceConfig config) {
enabled = config.enabled();
apiBaseUrl = config.apiBaseUrl();
}
}

Example config file:

ui.config/.../config.author/com.myproject.MyServiceImpl.cfg.json
{
"enabled": true,
"apiBaseUrl": "https://api.example.com"
}

ResourceResolver usage​

  • Use a service user for background tasks.
  • Always close resolvers you open.
  • Avoid leaking resolver instances across threads.
Map<String, Object> authInfo = Map.of(
ResourceResolverFactory.SUBSERVICE, "my-subservice"
);

try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(authInfo)) {
Resource resource = resolver.getResource(path);
// work with resource
}

Querying content​

  • Prefer QueryBuilder with explicit predicates.
  • Make sure queries are indexed and scoped by path.
  • Avoid building SQL2 with raw user input.

SQL2 example with safe parameter handling​

JCR SQL2 does not support prepared statements. For user-controlled values, prefer the Query Object Model (QOM) API, which lets you bind values safely.

public List<Resource> findPagesByTitle(ResourceResolver resolver, String rootPath, String title) throws RepositoryException {
Session session = resolver.adaptTo(Session.class);
QueryManager qm = session.getWorkspace().getQueryManager();
QueryObjectModelFactory qom = qm.getQOMFactory();
ValueFactory vf = session.getValueFactory();

Constraint pathConstraint = qom.descendantNode("p", rootPath);
Constraint titleConstraint = qom.comparison(
qom.propertyValue("p", "jcr:title"),
QueryObjectModelConstants.JCR_OPERATOR_LIKE,
qom.literal(vf.createValue(title))
);

QueryObjectModel query = qom.createQuery(
qom.selector("cq:PageContent", "p"),
qom.and(pathConstraint, titleConstraint),
null,
null
);

QueryResult result = query.execute();
List<Resource> resources = new ArrayList<>();
NodeIterator nodes = result.getNodes();
while (nodes.hasNext()) {
Node node = nodes.nextNode();
Resource resource = resolver.getResource(node.getPath());
if (resource != null) {
resources.add(resource);
}
}
return resources;
}

If you must build a raw SQL2 string, allowlist the input and escape it before interpolation. Avoid concatenating unsanitized values.

Error handling and logging​

  • Log with intent; avoid noisy info logs in hot paths.
  • Include context in errors, but avoid sensitive data.
  • Use warn for recoverable problems and error for failures.
try {
doWork();
} catch (Exception e) {
LOG.error("Failed to process content for path {}", path, e);
}

Threading and schedulers​

  • Prefer Sling Jobs for work that must run once in a cluster; use the Sling Scheduler only as the trigger.
  • Do not spawn your own threads without a clear reason.
  • Keep scheduled jobs idempotent.
@Component(service = Runnable.class,
property = {
"scheduler.expression=0 0 * * * ?",
"scheduler.concurrent=false"
})
public class HourlyJob implements Runnable {
@Override
public void run() {
// work must be idempotent
}
}

Security basics​

Validate all user input before use​

Treat anything that comes from requests, content properties, or external services as untrusted. Validate early and close to the boundary.

Backend validation + encoding (AEM/Sling)​

Use Sling's org.apache.sling.xss.XSSAPI to validate and encode user input before it reaches templates or logs.

@Reference
private XSSAPI xssApi;

public String safeText(String input) {
// Encode for HTML element text. Use a context-specific encoder for attributes or JS.
return xssApi.encodeForHTML(input == null ? "" : input);
}

public String safeUrl(String url) {
// Sanitizes href/src values; invalid URLs return an empty string.
return xssApi.getValidHref(url);
}

Other backend techniques:

  • Use HTL contexts (@ context='uri', @ context='html') when rendering values.
  • Use Text.escape() or Encode.forHtml() for non-HTL contexts.
  • Prefer allowlists over regex for values like hostnames, paths, or enums.

Frontend pitfalls and sanitization​

Even if backend is safe, frontend code can reintroduce XSS:

  • Injecting raw HTML into the DOM (innerHTML) without sanitization.
  • Using template helpers that output raw HTML by default.
  • Binding untrusted values to href or src without validation.

Use DOMPurify when you must render HTML from user-controlled sources:

import DOMPurify from 'dompurify';

const clean = DOMPurify.sanitize(untrustedHtml, {USE_PROFILES: {html: true}});
element.innerHTML = clean;

Handlebars auto-escapes, but it does not sanitize URLs. Add a helper for href values:

Handlebars.registerHelper('safeHref', (value) => {
try {
const url = new URL(value, window.location.origin);
return ['http:', 'https:', 'mailto:'].includes(url.protocol) ? url.toString() : '#';
} catch (e) {
return '#';
}
});

Additional security notes​

  • Do not hardcode credentials.
  • Use allowlists for any external hostnames.
private static final Set<String> ALLOWED_HOSTS =
Set.of("api.example.com", "services.example.com");

public boolean isAllowedHost(URI uri) {
return ALLOWED_HOSTS.contains(uri.getHost());
}

See also​