N
Naveenr.dev
Chapter 18
28 min read2026-07-19

Servlets in AEM — Request Resolution, Security, and Production Architecture

Understand how Sling Servlets fit into AEM architecture, how Sling resolves servlet requests, resource type vs path registration, secure repository access, Dispatcher considerations, and production troubleshooting.

Introduction

A frontend application needs to fetch data from AEM.

A form needs to submit information.

An internal application needs to trigger an operation.

Or a business team asks for an endpoint that creates or updates content in the repository.

A common developer reaction is:

"Let's create a servlet."

Sometimes that is the right solution.

But before creating one, there are more important questions to answer.

Who will call this endpoint?

Is the operation read-only, or does it modify data?

Should the endpoint be publicly accessible?

Does it require authentication?

Should the response be cached?

Does the operation need access to the AEM repository?

And most importantly:

Should this operation be exposed through an AEM servlet at all?

Creating a Sling Servlet is straightforward.

Designing one correctly for a production AEM environment requires understanding how Sling resolves requests, how authentication and permissions work, how Dispatcher controls access, and where business logic should live.

In this chapter, we will focus on those architectural decisions.

What You'll Learn

By the end of this chapter, you will understand:

  • Where Sling Servlets fit in AEM's backend architecture
  • How Sling resolves a request to the correct servlet
  • When to use resource-type-based and path-based servlets
  • Why HTTP methods such as GET and POST matter when designing endpoints
  • How to separate servlet responsibilities from business logic using OSGi services
  • When to use a request ResourceResolver and when a service user is appropriate
  • How authentication, authorization, input validation, and CSRF considerations affect servlet design
  • How Dispatcher and caching influence servlet accessibility in production
  • How to troubleshoot common servlet issues across Author, Publish, and Dispatcher
  • How to evaluate servlet design decisions from an AEM architect's perspective

Part 1: Where Servlets Fit in AEM Architecture

A Sling Servlet is an HTTP entry point into server-side logic running inside AEM.

For a simple requirement, the flow may be straightforward. A client sends an HTTP request, the servlet processes it, and AEM returns an HTTP response.

In a production implementation, however, the request usually passes through multiple layers before reaching the servlet.

Where a Servlet Fits in AEM Architecture.
Where a Servlet Fits in AEM Architecture.

The servlet is usually responsible for:

  • Receiving the request
  • Reading and validating input
  • Verifying that the requested operation is allowed
  • Calling the appropriate service
  • Converting the result into an HTTP response
  • Returning the appropriate HTTP status code

The servlet should not automatically become the place where all business logic is implemented.

Consider a requirement to create a page. The first implementation might be simple: receive the request, validate the parameters, create the page, and return a response.

That may work initially.

But requirements rarely remain that simple.

Over time, the same operation may need to handle page creation, metadata updates, business rules, repository operations, or even calls to external systems. If all of that logic is implemented directly inside the servlet, the servlet quickly becomes difficult to test and maintain.

A better design is to keep the servlet focused on the HTTP request and delegate the actual business operation to an OSGi service.

The responsibility is then clearly separated:

Servlet

  • Handles the HTTP request and response
  • Validates request-specific input
  • Calls the appropriate service
  • Maps the result to the correct HTTP response

OSGi Service

  • Implements the business logic
  • Performs repository operations where required
  • Integrates with external systems where required
  • Can be reused outside the servlet layer

For example, if page creation is implemented in an OSGi service, the servlet only needs to validate the incoming request and call that service. The same page creation logic can later be reused by a workflow, scheduler, event handler, job, or another service without duplicating the implementation.

This separation also makes the code easier to test. Servlet tests can focus on request handling and HTTP responses, while service tests can focus on the actual business logic.

The key design principle is simple:

  • A servlet should be the HTTP entry point to an operation, not the entire implementation of that operation.

Part 2: How Sling Resolves a Servlet Request

One of the most important concepts to understand is that Sling does not treat every request as a direct servlet URL.

Sling is resource oriented.

When a request reaches AEM, Sling first resolves the request to a resource. Once the resource is identified, Sling determines which script or servlet can handle the request.

These are two related but different steps:

  • Resource resolution determines which resource the request represents.
  • Servlet resolution determines which script or servlet should process the request.

For resource-type-based servlets, the resolution process can consider information such as:

  • Resource type
  • Selector
  • Extension
  • HTTP method

Consider this request:

GET /content/mycompany/us/en/products.list.json

The request gives Sling several pieces of information:

  • Resource path: /content/mycompany/us/en/products
  • Selector: list
  • Extension: json
  • HTTP method: GET

Suppose the resolved resource has the following resource type:

myproject/components/page

A servlet registered with matching properties can become a candidate to handle the request:

Resource Type: myproject/components/page
Selector:      list
Extension:     json
Method:        GET
Sling Servlet Resolution Flow — how Sling resolves a resource and uses its resource type together with selectors, extension, and HTTP method during servlet resolution.
Sling Servlet Resolution Flow — how Sling resolves a resource and uses its resource type together with selectors, extension, and HTTP method during servlet resolution.

This is an important distinction when debugging servlet issues.

A servlet can be deployed correctly and active in OSGi but still never execute because:

  • The requested resource does not exist
  • The resolved resource type does not match the servlet registration
  • The selector does not match
  • The extension does not match
  • The HTTP method does not match

In these cases, the problem may not be inside the servlet code at all.

The request may simply not resolve to that servlet.

This is why checking only whether the servlet component is active is not enough. When debugging a resource-type-based servlet, you also need to verify the resource being requested and the request properties Sling uses during servlet resolution.

Part 3: Resource-Type vs Path-Based Servlets

A Sling Servlet can commonly be registered in two ways:

  • By resource type
  • By path

Both approaches are supported, but they represent different ways of designing an endpoint.

Resource-Type-Based Servlet

A resource-type-based servlet is associated with the resource type of the requested resource.

For example:

@Component(service = Servlet.class)
@SlingServletResourceTypes(
    resourceTypes = "myproject/components/page",
    selectors = "list",
    extensions = "json",
    methods = HttpConstants.METHOD_GET
)
public class PageListServlet extends SlingSafeMethodsServlet {
}

A request might look like:

GET /content/mycompany/us/en/products.list.json

Sling resolves /content/mycompany/us/en/products to a resource and uses information such as its resource type, the list selector, the json extension, and the GET method during servlet resolution.

This approach fits naturally with Sling's resource-oriented architecture and is useful when the operation belongs to a specific type of resource.

For example, a page resource might expose a list.json representation that returns information related to that page or its child resources.

The important point is that the endpoint is connected to a resource that already exists in Sling's resource tree.

Path-Based Servlet

A path-based servlet is registered directly against a servlet path.

For example:

@Component(service = Servlet.class)
@SlingServletPaths(
    value = "/bin/myproject/process"
)
public class ProcessServlet extends SlingAllMethodsServlet {
}

The request directly targets:

/bin/myproject/process

Unlike a resource-type-based servlet, the endpoint is registered against the path itself rather than being selected based on the resource type of a requested content resource.

This can be useful for operations that do not naturally belong to a specific content resource.

However, using a path-based servlet should be an intentional design decision. The endpoint still needs to be evaluated in terms of authentication, authorization, request filtering, CSRF protection for applicable state-changing requests, input validation, and external exposure.

How Do You Choose?

RequirementRegistration Approach
Operation naturally belongs to an existing Sling resourceResource type
Behavior depends on the resource type being requestedResource type
Selector-based representation of a resourceResource type
Operation has no natural content resourcePath may be considered
Dedicated operational endpointPath may be considered with appropriate security and exposure controls

Before choosing either approach, ask:

  • Does this operation naturally belong to a Sling resource?
  • Is the endpoint representing or operating on that resource?
  • Does the operation need a dedicated URL that is independent of content structure?
  • Who should be allowed to access the endpoint?
  • Which HTTP methods should be exposed?
  • Will the request pass through Dispatcher?
  • Does the operation modify repository content?

The choice between resource-type and path-based registration should come from the nature of the operation and how the endpoint fits into the application architecture, not simply from which annotation is easier to use.

This table is a decision guide, not a strict rule for every implementation.

Part 4: GET vs POST — HTTP Semantics Matter

One of the easiest servlet mistakes to make is using GET for an operation that changes repository content.

Consider this endpoint:

GET /bin/createPage

If calling this endpoint creates a page in AEM, the request is changing server state.

That should not be modeled as a read operation.

A better design would use a state-changing HTTP method:

POST /bin/myproject/pages

The servlet can then validate the request, verify that the operation is allowed, delegate the page creation to the appropriate service, and return the result.

As a general rule:

HTTP MethodTypical Purpose
GETRetrieve data without changing server state
POSTCreate a resource or trigger a state-changing operation
PUTCreate or replace a resource at a known URI, where the API design supports it
DELETERemove a resource, where the API design supports it

The exact method depends on the API contract, but the important principle is that a read request should not unexpectedly modify content.

Using the correct HTTP method matters beyond code style.

GET is defined as a safe method and is commonly treated by clients and infrastructure as a read operation. Depending on the environment, GET responses may also be cached, prefetched, or requested automatically.

For example, this is a reasonable read operation:

GET /content/mycompany/us/en/products.list.json

The client is requesting information associated with the resource.

But an endpoint like this should not create repository content:

GET /bin/createPage?parentPath=/content/mycompany/us/en

Imagine someone bookmarks that URL, a monitoring tool calls it repeatedly, or another layer treats it as a normal read request. An operation that creates content should not be triggered simply by retrieving a URL.

A better API design would be:

POST /bin/myproject/pages

with the page information provided as part of the request.

The server can then:

  • Validate the incoming data
  • Verify authentication and authorization
  • Apply the required CSRF protection where applicable
  • Delegate the page creation to the service layer
  • Return an appropriate HTTP status code and response

For example, a successful page creation might return 201 Created, while an invalid request might return 400 Bad Request.

The main principle is simple:

The HTTP method should reflect what the operation actually does.

If an endpoint only reads data, GET is appropriate.

If an endpoint changes repository state, use an HTTP method that represents that operation and design the required security controls around it.

Part 5: Building a Production-Ready Servlet

Consider a requirement where an authenticated internal application needs to create a page in AEM.

A quick implementation might put everything inside the servlet: request validation, page creation, repository access, and response handling.

That may work initially, but it tightly couples the HTTP layer with the business logic.

A better design separates these responsibilities.

The servlet should handle the HTTP interaction:

  • Read the incoming request
  • Validate request-specific input
  • Verify that the request is allowed
  • Call the appropriate OSGi service
  • Map the result to an HTTP response
  • Return the appropriate status code

The OSGi service should handle the actual page creation logic and any repository operations required by that process.

For example:

@Component(service = Servlet.class)
@SlingServletPaths("/bin/myproject/pages")
public class CreatePageServlet extends SlingAllMethodsServlet {

    private static final Logger LOG =
            LoggerFactory.getLogger(CreatePageServlet.class);

    @Reference
    private PageCreationService pageCreationService;

    @Override
    protected void doPost(
            SlingHttpServletRequest request,
            SlingHttpServletResponse response) throws IOException {

        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        String parentPath = request.getParameter("parentPath");
        String pageName = request.getParameter("pageName");
        String pageTitle = request.getParameter("pageTitle");

        if (StringUtils.isAnyBlank(parentPath, pageName, pageTitle)) {
            response.setStatus(SlingHttpServletResponse.SC_BAD_REQUEST);
            response.getWriter().write(
                "{\"error\":\"Missing required parameters\"}"
            );
            return;
        }

        try {
            String pagePath = pageCreationService.createPage(
                parentPath,
                pageName,
                pageTitle
            );

            response.setStatus(SlingHttpServletResponse.SC_CREATED);
            response.getWriter().write(
                "{\"path\":\"" + pagePath + "\"}"
            );

        } catch (PageCreationException e) {
            LOG.error(
                "Unable to create page under parent path: {}",
                parentPath,
                e
            );

            response.setStatus(
                SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR
            );

            response.getWriter().write(
                "{\"error\":\"Unable to create page\"}"
            );
        }
    }
}

The important part of this example is not the page creation implementation itself.

Notice that the servlet does not use PageManager or perform repository writes directly. It delegates the operation to PageCreationService.

The implementation behind that service can handle:

  • Obtaining the appropriate ResourceResolver
  • Adapting to PageManager
  • Applying page creation rules
  • Performing repository operations
  • Handling implementation-specific failures

The servlet remains focused on the HTTP contract.

Why Keep the Business Logic in a Service?

Imagine that page creation is initially triggered only through the servlet.

Later, the same operation may also be required by:

  • A workflow
  • A scheduled process
  • An asynchronous job
  • An event-driven process
  • Another OSGi service

If the page creation logic lives directly inside the servlet, each new entry point either has to duplicate that logic or depend on the servlet implementation.

When the logic lives in PageCreationService, each entry point can reuse the same operation.

The caller changes, but the business logic remains in one place.

This also makes testing easier.

The servlet can be tested for request validation, service interaction, and HTTP responses. The service can be tested separately for page creation and repository behavior.

Where Should Repository Access Happen?

Moving repository logic into an OSGi service introduces another important architecture decision.

Should the page be created using the permissions of the authenticated user who made the request?

Or should the operation run using a dedicated service identity with controlled repository permissions?

These are two different security models, and the choice affects how the ResourceResolver should be obtained.

We will look at that distinction next.

Part 6: Repository Access and ResourceResolvers

Servlets often need to read or modify content in the AEM repository.

Once repository access is required, an important architecture decision appears:

Which security context should perform the repository operation?

In a servlet-based implementation, there are two common approaches:

  • Use the ResourceResolver associated with the incoming request
  • Obtain a service ResourceResolver using a configured service user

These approaches represent different security models and should not be used interchangeably.

Request ResourceResolver

Every Sling request provides access to a ResourceResolver:

ResourceResolver resolver = request.getResourceResolver();

This resolver is associated with the security context of the incoming request.

If the request is made by an authenticated user, repository operations performed through this resolver are subject to that user's permissions.

This approach can be appropriate when the operation is expected to respect the permissions of the caller.

For example, if an authenticated author triggers an operation and the action should only succeed when that author already has permission to modify the target content, using the request resolver may fit the requirement.

One important rule is:

Do not close a ResourceResolver obtained from request.getResourceResolver().

The resolver is managed as part of the Sling request lifecycle. The servlet uses it, but does not own its lifecycle.

Service ResourceResolver

Some backend operations should not depend on the repository permissions of the user who initiated the request.

Instead, the operation may need to run using a controlled application identity with a predefined set of permissions.

In that case, the business service can obtain a service ResourceResolver through ResourceResolverFactory using a configured service-user mapping.

For example:

Map<String, Object> authInfo = Collections.singletonMap(
    ResourceResolverFactory.SUBSERVICE,
    "page-writer"
);

try (ResourceResolver resolver =
        resourceResolverFactory.getServiceResourceResolver(authInfo)) {

    // Perform the repository operation
}

In this case, the application code obtains the resolver and therefore owns its lifecycle. The resolver should be closed after the operation is complete, which is why a try-with-resources block is commonly used.

The service user should receive only the repository permissions required for the operation.

For example, a page creation service might require access to specific project paths rather than unrestricted access across the repository.

The exact permissions depend on what the service needs to do.

The important principle is least privilege:

Give the service identity only the permissions required to perform its responsibility.

A service user should not be given broad administrative permissions simply to avoid permission-related implementation issues.

Request Resolver vs Service Resolver

QuestionRequest ResourceResolverService ResourceResolver
Security contextIncoming request or authenticated userConfigured service identity
Obtained fromrequest.getResourceResolver()ResourceResolverFactory
PermissionsBased on the request's security contextBased on service-user permissions
Lifecycle ownerSling request lifecycleApplication code
Should application code close it?NoYes
Typical useOperation should respect caller permissionsControlled backend repository operation

The choice between the two is not simply a coding preference.

Ask:

Should this operation execute using the caller's permissions?

If yes, the request resolver may be appropriate.

Or:

Should this operation execute as a controlled backend service identity with explicitly assigned permissions?

If yes, a service resolver may be appropriate.

This distinction becomes especially important when designing state-changing servlet operations.

Using a service resolver does not replace authentication or authorization at the HTTP layer. A servlet should not allow any caller to trigger a privileged backend operation simply because the repository write itself is performed by a service user.

The servlet still needs to determine whether the caller is allowed to trigger the operation.

The service user determines what the backend operation is allowed to do in the repository.

These are separate security boundaries, and both need to be designed correctly.

Part 7: Servlet Security

A servlet is an HTTP entry point into AEM.

That means every servlet should be treated as part of the application's security boundary.

Before exposing an endpoint, consider:

  • Who can call it?
  • What input can the caller provide?
  • What can the servlet access?
  • What can the operation modify?
  • Should the request be allowed through Dispatcher?

These questions should be answered as part of the endpoint design, not after the servlet has already been implemented.

Authentication

Authentication determines who is making the request.

Before exposing a servlet, decide whether the endpoint should be:

  • Publicly accessible
  • Available only to authenticated users
  • Restricted to specific internal users or systems

Do not assume that registering a servlet automatically makes the endpoint safe.

The accessibility of an endpoint depends on how authentication, authorization, Dispatcher rules, and the surrounding AEM security configuration are designed.

Authorization

Authentication answers:

Who is the caller?

Authorization answers:

Is this caller allowed to perform this operation?

The distinction becomes especially important for state-changing operations.

For example, an authenticated user may be able to access AEM but still should not automatically be allowed to trigger an endpoint that creates or modifies content.

The servlet or the underlying application logic must ensure that the caller is authorized to perform the requested operation.

If a service user performs the repository operation, the two security boundaries remain separate:

  • The caller must be authorized to trigger the operation
  • The service user must have only the repository permissions required to perform it

Using a privileged service identity should never become a way to bypass authorization at the HTTP layer.

Input Validation

Never trust incoming request parameters simply because the endpoint requires authentication.

Consider an endpoint that accepts a repository path:

parentPath=/content/mycompany/us/en

If the caller can provide any repository path, the servlet may unintentionally allow operations outside the content tree it was designed to manage.

Input validation should consider:

  • Required parameters
  • Allowed repository paths
  • Expected data formats
  • Allowed values
  • Request size where relevant

For example, if an endpoint is intended to create pages only under a specific project, the implementation should enforce that boundary.

Allowed:
/content/mycompany/...

Rejected:
/content/other-project/...
/apps/...
/libs/...

The validation should happen before the request reaches the repository operation.

Do not rely only on frontend validation. A caller can invoke the endpoint directly without using the intended user interface.

CSRF Considerations

State-changing requests require additional security considerations.

Operations using methods such as POST, PUT, or DELETE should be designed with AEM's authentication and applicable CSRF protections in mind.

A request that works when called directly during local development may behave differently when the same endpoint is accessed through a production environment.

Depending on the implementation and request path, the request may pass through multiple controls, including:

  • Authentication
  • Authorization
  • CSRF protection
  • Dispatcher filtering
  • Repository permissions

When a request is rejected, the right approach is to identify which security layer is responsible rather than disabling protections simply to make the endpoint work.

Security should be part of the servlet design from the beginning, especially when an endpoint can modify repository content or trigger a privileged backend operation.

Part 8: Dispatcher and Caching

A servlet may work correctly when called directly on AEM Publish and still fail when accessed through the public endpoint.

The reason is that production traffic usually passes through additional infrastructure before reaching AEM.

Dispatcher Request Flow — showing how client requests pass through the CDN and Dispatcher, where they may be served from cache, blocked, or forwarded to AEM Publish and the Sling Servlet.
Dispatcher Request Flow — showing how client requests pass through the CDN and Dispatcher, where they may be served from cache, blocked, or forwarded to AEM Publish and the Sling Servlet.

Dispatcher acts as an important boundary between incoming requests and AEM Publish.

Depending on the configuration, a request may be allowed, denied, or handled through the caching layer before it reaches Sling.

This means a servlet can be registered correctly and work when called directly on Publish, while still being inaccessible through the public URL.

When that happens, the servlet itself may not be the problem. The request may be blocked before it reaches AEM.

Designing Servlet Endpoints for Dispatcher

When designing an endpoint, consider:

  • Should this URL be reachable through Dispatcher?
  • Which HTTP methods should be allowed?
  • Is the endpoint public or authenticated?
  • Should the response be cacheable?
  • How do selectors and extensions affect the URL?
  • Are query parameters part of the request?
  • Does the endpoint expose a state-changing operation?

These decisions should be made together with the endpoint design.

For example, consider a read-only endpoint:

GET /content/mycompany/us/en/products.list.json

Because this is a read operation, its response may be a candidate for caching depending on the Dispatcher and CDN configuration, URL pattern, cache rules, and application requirements.

That does not mean every GET servlet response is automatically cached.

Cache behavior depends on how the surrounding infrastructure is configured.

Now consider a state-changing endpoint:

POST /bin/myproject/pages

This request performs an operation rather than retrieving a cacheable representation of content. It should be allowed only when required and protected by the appropriate authentication, authorization, request filtering, and security controls.

The endpoint design, Dispatcher configuration, and caching strategy should agree with each other.

A servlet should not be designed in isolation and then exposed through Dispatcher as an afterthought.

For production troubleshooting, this leads to an important distinction:

Works directly on AEM Publish does not necessarily mean the endpoint is correctly exposed through Dispatcher.

When an endpoint behaves differently between direct Publish access and the public URL, check the complete request path before changing the servlet implementation.

Part 9: Real Project Use Cases

The servlet design should come from the requirement.

The following examples show how the decisions discussed earlier apply to common AEM project scenarios.

Use Case 1: Read-Only JSON Endpoint

Requirement:

A frontend needs a list of child pages under a specific page.

Because the operation is read-only and belongs to an existing content resource, a resource-type-based servlet may fit naturally.

For example:

GET /content/mycompany/us/en/products.list.json

The servlet can use the requested resource as the context, read the required child resources, and return a JSON response.

In this scenario:

  • The operation is read-only
  • The request is associated with an existing Sling resource
  • GET represents the operation correctly
  • A selector such as list identifies the requested representation
  • The response may be cacheable depending on the Dispatcher and CDN configuration

The servlet should still return only the data the frontend actually needs rather than exposing repository structures directly.

Use Case 2: Form Submission

Requirement:

A user submits form data that requires server-side processing.

In this case, the servlet acts as the HTTP entry point for the submission.

The servlet can:

  • Receive the POST request
  • Validate the submitted data
  • Verify the required security conditions
  • Delegate the business process to an OSGi service
  • Return an appropriate response

The OSGi service handles the actual business operation.

This separation becomes useful when the processing logic grows beyond simple request handling or needs to be reused by another part of the application.

Use Case 3: Authenticated Repository Operation

Requirement:

An internal application needs to trigger a controlled operation that modifies content in the AEM repository.

This scenario requires more than simply registering a servlet.

The design needs to answer:

  • How is the caller authenticated?
  • Is the caller authorized to trigger the operation?
  • What input is accepted?
  • Which repository paths can be modified?
  • Should the operation use the caller's permissions or a service user?
  • What permissions should the service user have?
  • Should the endpoint be accessible through Dispatcher?

The servlet handles the HTTP and security boundary for the incoming request.

The OSGi service performs the business operation using the appropriate repository security context.

This is where servlet design becomes an architecture decision rather than simply an implementation task.

Part 10: Production Troubleshooting

A servlet that works during development may behave differently once deployed across Author, Publish, and Dispatcher.

When troubleshooting, avoid looking only at the servlet class.

Follow the complete request path and identify which layer is failing.

Problem: Servlet Returns 404

A servlet may be deployed successfully but still return 404.

For a resource-type-based servlet, check:

  • Does the requested resource exist?
  • Does the resolved resource type match the servlet registration?
  • Does the selector match?
  • Does the extension match?
  • Does the HTTP method match?
  • Is the servlet component registered and active?

For path-based servlets, verify that the requested path matches the servlet registration and that the endpoint is available in the target environment.

The important point is that a deployed servlet is not necessarily a resolved servlet.

For resource-type-based servlets, debug the complete request resolution rather than checking only whether the Java class or bundle is deployed.

Problem: Works on Author but Not Publish

If the servlet works on Author but fails on Publish, check:

  • Is the bundle deployed and active on Publish?
  • Does the requested resource exist on Publish?
  • Has the required content been published?
  • Are environment-specific OSGi configurations available?
  • Does the service user have the required permissions on Publish?
  • Is the request reaching Publish through the expected route?

Author and Publish serve different purposes and may have different content, permissions, and configurations.

The same servlet code can therefore behave differently depending on the environment around it.

Problem: Works Directly on Publish but Not Through Dispatcher

If an endpoint works when calling Publish directly but fails through the public URL, the problem may exist before the request reaches AEM.

Check the Dispatcher configuration and confirm that the required:

  • URL pattern
  • HTTP method
  • Selectors
  • Extensions

are intentionally allowed.

Also verify whether another layer, such as a CDN or upstream proxy, is affecting the request.

Do not change the servlet implementation until you confirm that the request is actually reaching Sling.

Problem: POST Returns 403

A 403 response can come from different security layers.

Check:

  • Is the caller authenticated where required?
  • Is the caller authorized to trigger the operation?
  • Are applicable CSRF requirements satisfied?
  • Does Dispatcher allow the URL and HTTP method?
  • Does the repository security context have the required permissions?

Do not immediately disable security protections to make the request work.

Identify which layer is rejecting the request first.

The fix depends on whether the rejection comes from the HTTP security layer, Dispatcher, or repository permissions.

Problem: Service User Cannot Modify Content

If an operation using a service resolver cannot modify repository content, check:

  • Is the subservice mapping configured correctly?
  • Does the mapped service user exist in the target environment?
  • Is the expected service user being resolved?
  • Does the service user have the required permissions?
  • Are those permissions applied to the correct repository path?

Avoid solving permission problems by granting broad repository access.

The service user should receive only the permissions required for the operation.

Problem: Wrong Servlet Is Executed

For resource-type-based servlets, verify:

  • Resource type
  • Selector
  • Extension
  • HTTP method

Servlet resolution depends on the complete request.

A small difference in the requested URL or resource type can cause Sling to select a different servlet or script.

When this happens, compare the actual request with the servlet registration rather than assuming the intended servlet should have been selected.

Part 11: Architect Perspective**

The question should not always be:

"How do we create this servlet?"

The first question should be:

"Do we need a servlet for this operation?"

Before creating one, ask:

  • Is this genuinely an HTTP operation?
  • Who needs to call it?
  • Is the operation read-only or state-changing?
  • Does the operation naturally belong to a Sling resource?
  • Should it use resource-type registration or a dedicated path?
  • Does the caller need to be authenticated?
  • What authorization rules apply?
  • Should repository operations use the caller's security context or a service user?
  • What repository permissions does the operation actually require?
  • Should the endpoint be exposed through Dispatcher?
  • Should the response be cacheable?
  • Does the business logic belong in an OSGi service?
  • Is AEM the right system to perform this operation?

These questions help define the boundaries of the implementation before code is written.

A servlet should usually remain a thin HTTP layer.

Its responsibility is to handle the request and response, while other parts of the architecture handle their own concerns:

  • Servlet — HTTP request and response handling
  • OSGi Service — Business logic
  • Request or Service ResourceResolver — Repository access under the appropriate security context
  • Service User — Controlled backend repository permissions where required
  • Sling — Resource and servlet resolution
  • Dispatcher and CDN — Request exposure and caching boundaries

Keeping these responsibilities separate makes the implementation easier to secure, test, troubleshoot, and maintain.

The goal is not to create a servlet for every backend requirement.

The goal is to use a servlet when an HTTP entry point is genuinely required and keep the responsibilities behind that entry point clearly separated.

Summary

Creating a Sling Servlet is straightforward.

Designing one correctly for a production AEM architecture requires understanding what happens around the servlet as well.

The key points to remember:

  1. Servlets are HTTP entry points — Keep request and response handling at the servlet layer.

  2. Understand Sling resolution — For resource-type-based servlets, the resolved resource type together with request properties such as selectors, extension, and HTTP method can influence which servlet handles the request.

  3. Choose registration intentionally — Resource-type-based and path-based servlets represent different endpoint designs. Choose based on the nature of the operation.

  4. Use correct HTTP semantics — Read operations belong to GET. State-changing operations should use an HTTP method that represents what the endpoint actually does.

  5. Separate business logic — Keep reusable business logic in OSGi services instead of building large servlets.

  6. Choose the correct ResourceResolver — A request resolver and a service resolver represent different security contexts and have different lifecycle ownership.

  7. Treat security as part of the design — Authentication, authorization, input validation, applicable CSRF protection, and least-privilege repository permissions should be considered from the beginning.

  8. Remember the complete production request path — Dispatcher and CDN configuration can affect whether an endpoint is reachable and how a response is handled or cached.

A servlet is only one layer of the architecture.

Understanding how that layer interacts with Sling, OSGi services, repository security, service users, Dispatcher, and caching is what turns a servlet that works locally into an endpoint designed for a production AEM environment.

What's Next

Throughout this chapter, we kept returning to the same design principle:

The servlet handles the HTTP interaction. The OSGi service handles the business logic.

That brings us to the next layer of AEM backend architecture.

Chapter 19: OSGi Services — Designing the Service Layer in AEM

In the next chapter, we will look at how OSGi services work, dependency injection, service interfaces and implementations, component lifecycle, service configuration, service ranking, and how to design reusable backend logic for production AEM applications.

Enjoyed this chapter?

Get an email when I publish the next chapter. No spam — just new technical deep-dives.

Comments

Share feedback or questions about this blog post.

No comments yet. Be the first to share your thoughts.