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");
}
}

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.
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"),
QueryObjectModelFactory.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 SlingScheduler or OSGi schedulers.
  • 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 AEM’s XSSAPI to validate and encode user input before it reaches templates or logs.

@Reference
private XSSAPI xssApi;

public String safeText(String input) {
// Strictly validate: returns null if invalid
String valid = xssApi.getValidString(input, 200, true);
if (valid == null) {
return "";
}
// Encode for HTML output
return xssApi.encodeForHTML(valid);
}

public String safeUrl(String url) {
// Validates and normalizes URLs
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