Skip to main content

OSGi Services and References: Wiring Your Code Together

Series: AEM Backend Essentials — Part 2

Updated 5 min readMarkdown
Part of seriesAEM Backend Essentials
OSGi Services and References: Wiring Your Code Together

In the previous post we saw how a component starts and stops. Real features need components that work together: a servlet that calls a service, a service that uses another service, a workflow step that needs a helper. OSGi handles this wiring through services and references. This post shows how to design a service, consume it safely, and diagnose it when the wiring fails.

Service = Interface + Implementation

The standard pattern is a small public interface and one implementation:

package com.myproject.core.services;

public interface PriceService {
    String formatPrice(double amount, String currencyCode);
}
package com.myproject.core.services.impl;

import com.myproject.core.services.PriceService;
import java.text.NumberFormat;
import java.util.Currency;
import java.util.Locale;
import org.osgi.service.component.annotations.Component;

@Component(service = PriceService.class)
public class PriceServiceImpl implements PriceService {

    @Override
    public String formatPrice(double amount, String currencyCode) {
        NumberFormat format = NumberFormat.getCurrencyInstance(Locale.US);
        format.setCurrency(Currency.getInstance(currencyCode));
        return format.format(amount);
    }
}

service = PriceService.class is what registers the component as a service under that interface. Without it, the component exists but nobody can look it up.

Keep the interface in an API package and the implementation in a separate impl package. Other code depends only on the interface, so you can replace or mock the implementation without touching callers.

Consuming a Service with @Reference

Inside another component, ask for the service with @Reference:

@Component(service = Servlet.class)
public class PriceServlet extends SlingSafeMethodsServlet {

    @Reference
    private PriceService priceService;

    @Override
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
            throws IOException {
        response.setContentType("text/plain");
        response.getWriter().write(priceService.formatPrice(19.99, "USD"));
    }
}

OSGi finds an active PriceService, injects it, and only then activates the servlet. If no matching service exists, the servlet stays unsatisfied.

In a Sling Model, don't use @Reference. Use @OSGiService:

@Model(adaptables = SlingHttpServletRequest.class,
       defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class PriceModel {
    @OSGiService
    private PriceService priceService;
}

Reference Options That Matter

@Reference has defaults that suit most cases: mandatory, static. These attributes change the behavior:

Attribute Values Meaning
cardinality MANDATORY (default), OPTIONAL, MULTIPLE, AT_LEAST_ONE How many matching services you need and whether you can run without one
policy STATIC (default), DYNAMIC Whether a change to the service restarts your component (STATIC) or is handled live (DYNAMIC)
target an LDAP-style filter Pick a specific implementation by its properties

Example of an optional, dynamic reference. The field must be volatile:

@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
private volatile AuditLogger auditLogger;

private void audit(String message) {
    AuditLogger logger = auditLogger;   // read once into a local variable
    if (logger != null) {
        logger.log(message);
    }
}

Copying the field into a local variable before the null check avoids a race, because a dynamic reference can disappear between two reads.

Choosing Between Implementations

If two components implement the same interface, OSGi needs a way to pick one. You have two options:

Service ranking. The highest service.ranking wins:

@Component(service = PriceService.class, property = {"service.ranking:Integer=100"})

A target filter. Select by a service property:

@Component(service = PriceService.class, property = {"channel=web"})
public class WebPriceService implements PriceService { ... }

@Reference(target = "(channel=web)")
private PriceService priceService;

The Advanced series covers multiple implementations and factory configurations in more depth.

Designing Good Services

  • Make services stateless where possible. They are shared by many requests at once
  • Keep each service focused. One responsibility is easier to test and replace
  • Don't use new on a service. A hand-created object isn't wired, so its @Reference fields are null
  • Don't expose AEM internals in your interface. Return plain types and your own classes
  • Don't implement interfaces that AEM provides for you to call. Many product interfaces are meant to be consumed, not implemented. The Cloud build checks API usage against the SDK, so a misuse shows up as a build failure rather than a runtime surprise

Diagnosing Wiring Problems

When something isn't injected, the cause is almost always one of these:

Symptom Likely cause Where to look
Component never appears as active A mandatory reference is unsatisfied Components list: it names the missing reference
Field is null inside a normal class The object was created with new, not by OSGi Search for new MyServiceImpl()
@OSGiService field is null in a Sling Model The service isn't active, or the interface is wrong Check the service in the Services list
Wrong implementation used Two services match and ranking decides Check service.ranking and any target filter
Works after a restart only A static reference changed and the component was restarted Consider a dynamic optional reference

On the local SDK, use the Felix console's Components and Services pages. On Cloud Service, the Developer Console shows the same information read-only.

AEM as a Cloud Service vs 6.5

The annotations and the lifecycle are the same on both. A few practical differences:

  • Deploy time: on Cloud Service a wiring mistake only shows after the pipeline deploys, so test on a Dev environment, not just locally
  • Annotations: older 6.5 code often uses the Felix SCR annotations (org.apache.felix.scr.annotations). Newer code, and everything for Cloud Service, should use the OSGi DS annotations shown here. If you're migrating, the SCR annotations are among the first things to replace
  • Constructor injection: newer DS versions support injecting references through the constructor, but field injection works everywhere. Check what your runtime supports before using the newer style

What's Next

Services often need settings: an endpoint, a toggle, a timeout. The next post covers OSGi configuration: where the files live, how run modes work, and how to supply secrets safely on Cloud Service.


Next in this series: [OSGi Configuration: Files, Run Modes and Environment Variables]

About the author

  • AEM Technical Lead experienced with building and architecting solutions on AEM Sites and Assets. Sharing practical tutorials, real-world implementation lessons, and best practices to help fellow developers and leads navigate AEM.

Do you mostly use field injection with @Reference, or constructor injection? And has an unsatisfied reference ever cost you a day? Tell me how you tracked it down.

AEM Backend Essentials

Part 2 of 6

OSGi, services, configuration, service users, servlets and testing: the core backend skills for AEM as a Cloud Service, with 6.5 differences.

Up nextOSGi Configuration: Files, Run Modes and Environment VariablesSeries: AEM Backend Essentials — Part 3