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, and this page is available as Markdown at https://nhanbe.id.vn/others/posts/best-practices-for-aem-developer.md.

Best practices for AEM 6.5 LTS developers covering architecture, security, performance, and coding standards.

Best Practices For Aem Developer

Architecture & Component Design

Use Sling Models Over WCMUsePojo

Prefer @Model-annotated Sling Models with @Inject / @ValueMapValue / @ChildResource. Avoid WCMUsePojo — it is a legacy approach and does not support proper unit testing with AEM Mocks.

@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class MyComponentModel {

    @ValueMapValue
    private String title;

    @ChildResource
    private Resource items;
}

Write Logic in HTL, Not JSP

AEM 6.5 uses HTL (HTML Template Language) as the primary templating engine. JSP is deprecated for new development. Keep HTL free of business logic — delegate to Sling Models via data-sly-use.

<sly data-sly-use.model="com.example.models.MyComponentModel">
    <h2>${model.title @ context='text'}</h2>
</sly>

Follow the Proxy Component Pattern

Always proxy Core Components instead of copying them. Extend only what differs:

/apps/my-project/components/content/mybutton
  sling:resourceSuperType = core/wcm/components/button/v2/button

This ensures you inherit Core Component updates and security patches without overriding base code.


OSGi & Configuration

Use DS Annotations (Not Felix SCR)

AEM 6.5 uses OSGi R6/R7 Declarative Services. Use @Component, @Activate, @Deactivate from org.osgi.service.component.annotations — not the deprecated Felix @org.apache.felix.scr.annotations.*.

@Component(service = MyService.class, immediate = true)
public class MyServiceImpl implements MyService {

    @Activate
    protected void activate(ComponentContext ctx) { ... }
}

Externalise Configuration via OSGi Config Files

Never hardcode environment-specific values. Use OSGi configuration files per run mode:

/apps/my-project/config.author/com.example.MyServiceImpl.cfg.json
/apps/my-project/config.publish/com.example.MyServiceImpl.cfg.json

Cloud Services vs. OSGi Config

Use Cloud Services (via /conf) for integration credentials (e.g., reCAPTCHA site keys, analytics IDs) so authors can manage them per site. Reserve OSGi configs for infrastructure-level settings developers control.


Sling & JCR

Prefer Resource API Over JCR API

Use Resource, ValueMap, and ResourceResolver instead of raw JCR Node/Session wherever possible. The Sling abstraction is mock-friendly and decoupled from the repository implementation.

Always Close ResourceResolver

Resource resolvers opened programmatically must be closed in a finally block or try-with-resources to prevent repository session leaks:

try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(params)) {
    // use resolver
}

Use Service Users, Not Admin Sessions

ResourceResolverFactory.getAdministrativeResourceResolver() is disabled by default in AEM 6.5. Create a system user with minimum required ACLs and map it via org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended.

Node Naming

Use lowercase, hyphenated names for JCR nodes (e.g., my-component, not MyComponent). Avoid spaces and special characters.


Servlets

Prefer Resource-Type & Selector Servlets Over Path-Based

Path-based servlets bypass Dispatcher caching and require explicit allow rules. Bind servlets by resourceType + selector + extension:

@Component(service = Servlet.class)
@SlingServletResourceTypes(
    resourceTypes = "my-project/components/content/search",
    selectors = "results",
    extensions = "json",
    methods = HttpConstants.METHOD_GET
)
public class SearchResultsServlet extends SlingSafeMethodsServlet { ... }

Security

Sanitise Output in HTL

HTL applies automatic XSS escaping based on context. Always specify the correct context explicitly when in doubt:

${user.bio @ context='text'}       <!-- plain text, strips HTML -->
${page.url @ context='uri'}        <!-- URL encoding -->
${component.html @ context='html'} <!-- trusted HTML only -->

Never use @ context='unsafe' unless the content is fully controlled and sanitised server-side.

Validate on the Server Side

Client-side validation is UX only. Always re-validate inputs on the server (Sling servlet or service layer) before persisting to JCR.

Restrict Servlet Access

Use Dispatcher allowedSelectors / allowedExtensions filters to block unexpected selector or extension combinations that could expose unintended servlet endpoints.


Performance & Caching

Design for Dispatcher Cache

Dispatcher caches full HTML pages. Keep pages cacheable by default:

  • Avoid session-dependent or user-specific content in page HTML.
  • Use Sling Dynamic Include (SDI) for personalised or time-sensitive fragments within otherwise cacheable pages.
  • Use AEM selectors (e.g., .model.json) instead of query parameters — query strings invalidate Dispatcher cache by default.

JCR Query Optimisation

  • Always create Oak indexes for properties used in queries (/oak:index).
  • Prefer property-based indexes (lucene or property type) over nodetype traversal.
  • Use QueryBuilder or Query APIs — never string-concatenate JCR-SQL2/XPath with user input (SQL injection risk).
  • Monitor slow queries via /system/console/queryDebugLog.

Avoid Large Result Sets

Paginate JCR queries. Never iterate unbounded result sets in request threads — use background jobs (Sling Jobs) for bulk operations.


Content & Editable Templates

Use Editable Templates, Not Static

AEM 6.5 fully supports Editable Templates stored under /conf. Avoid static templates in /apps for new projects — they do not support policy-driven component control or initial content per template.

Lock Down Component Policies

Use the Template Editor to whitelist allowed components per parsys. This prevents authors from inserting incompatible or unstyled components.


Logging & Error Handling

Use SLF4J, Not System.out

private static final Logger log = LoggerFactory.getLogger(MyClass.class);
log.debug("Processing resource: {}", resource.getPath());

Set package-level log levels in OSGi (/system/console/slinglog) without redeploying code.

Never Swallow Exceptions

Always log the full exception, not just the message:

} catch (RepositoryException e) {
    log.error("Failed to read node at {}", path, e); // pass Throwable as last arg
}

Testing

Use AEM Mocks

Unit-test Sling Models and servlets with AemContext from io.wcm.testing.mock.aem:

@ExtendWith(AemContextExtension.class)
class MyModelTest {

    private final AemContext ctx = new AemContext();

    @Test
    void testTitleIsReturned() {
        ctx.currentResource(ctx.create().resource("/content/test",
            "sling:resourceType", "my-project/components/content/mycomp",
            "title", "Hello"));
        MyComponentModel model = ctx.request().adaptTo(MyComponentModel.class);
        assertEquals("Hello", model.getTitle());
    }
}

Summary

AreaDoAvoid
TemplatingHTL + Sling ModelsJSP, WCMUsePojo
OSGiDS Annotations (org.osgi.*)Felix SCR annotations
JCR accessService users, ResourceResolverAdmin sessions, raw Node API
ServletsResource-type + selector bindingPath-based servlets
CachingDispatcher-friendly selectors, SDIQuery parameters, session-scoped pages
TestingAEM Mocks, JUnit 5Manual CRXDE testing only
ConfigOSGi .cfg.json per run modeHardcoded environment values