3

OSGi Service References: Managing Multiple Implementations in AEM

In enterprise Adobe Experience Manager (AEM) development, modular architectures are heavily reliant on the OSGi framework. When building highly scalable…

In enterprise Adobe Experience Manager (AEM) development, modular architectures are heavily reliant on the OSGi framework. When building highly scalable applications, multiple implementations of a single interface are frequently deployed. To manage these scenarios efficiently, the OSGi declarative services specification provides robust mechanisms. Specifically, the @Reference annotation is utilized, where cardinality and policy options must be precisely configured to handle multiple service providers.


Understanding Cardinality and Policy in OSGi

When a component requires access to OSGi services, the relationship is defined by cardinality. Cardinality dictates whether the reference is mandatory or optional, and whether it accepts a single instance or multiple instances.

To handle multiple implementations, the following configuration options are provided by the OSGi framework:

  • Cardinality (ReferenceCardinality.MULTIPLE): The referencing component is kept active even if zero services are available, but it dynamically binds all matching implementations.
  • Cardinality (ReferenceCardinality.AT_LEAST_ONE): At least one matching service implementation must be present for the consuming component to satisfy its dependencies and activate.
  • Policy (ReferencePolicy.DYNAMIC): Services are bound and unbound dynamically at runtime without restarting the consuming component.
  • Policy Option (ReferencePolicyOption.GREEDY): The consuming component is forced to bind newly available higher-ranking services or additional implementations immediately.

Use-Case Scenario: Dynamic Notification Engine

A common use case in enterprise AEM applications is a pluggable notification ecosystem. For instance, different business requirements might dictate that an event triggers an Email notification, an SMS notification, or a Slack alert.

Instead of hardcoding references to individual service classes, a single NotificationService interface is exposed. Multiple strategy implementations are then deployed independently. The primary engine must dynamically bind all available notification channels and execute them uniformly without requiring code modifications when a new channel is introduced.

Architectural Diagram

The interaction between the consumer component and multiple service providers is illustrated in the architectural diagram below:

+-------------------------------------------------------------------------------------------------------------------+
| NotificationEngineImpl |
| (Consuming Component via ReferenceCardinality.MULTIPLE) |
+-------------------+-----------------------------------------------------------------------------------------------+
| |
| (Binds dynamically) |
v v
+-------------------+---------------+ +-----------------+-------------+
| EmailNotificationServiceImpl | | SMSNotificationServiceImpl |
| (Provides: NotificationService) | | (Provides:NotificationService)|
+-----------------------------------+ +-------------------------------+

Technical Implementation & Sample Code

The strategy interface and its respective implementations are defined in the code samples below. Subsequently, the consuming engine utilizes the dynamic collection approach to bind the services.

The Service Interface

package com.stacknowledge.core.services;

public interface NotificationService {
    String getChannelName();
    void sendNotification(String message);
}

Service Implementation A (Email)

package com.stacknowledge.core.services.impl;

import com.stacknowledge.core.services.NotificationService;
import org.osgi.service.component.annotations.Component;

@Component(service = NotificationService.class, property = { "service.ranking=100" })
public class EmailNotificationServiceImpl implements NotificationService {
    
    @Override
    public String getChannelName() {
        return "EMAIL";
    }

    @Override
    public void sendNotification(String message) {
        // Logic for sending email is executed here
    }
}

Service Implementation B (SMS)

package com.stacknowledge.core.services.impl;

import com.stacknowledge.core.services.NotificationService;
import org.osgi.service.component.annotations.Component;

@Component(service = NotificationService.class, property = { "service.ranking=200" })
public class SMSNotificationServiceImpl implements NotificationService {

    @Override
    public String getChannelName() {
        return "SMS";
    }

    @Override
    public void sendNotification(String message) {
        // Logic for sending SMS is executed here
    }
}

The Consuming Component (The Engine)

To manage multiple implementations seamlessly, thread-safe collections such as CopyOnWriteArrayList are recommended. The bind and unbind methods are invoked by the OSGi runtime whenever services are modified.

package com.stacknowledge.core.engine;

import com.stacknowledge.core.services.NotificationService;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.component.annotations.ReferencePolicyOption;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

@Component(service = NotificationEngine.class, immediate = true)
public class NotificationEngineImpl implements NotificationEngine {

    // A thread-safe list is utilized to hold references to all bound implementations
    private final List<NotificationService> notificationServices = new CopyOnWriteArrayList<>();

    @Reference(
            service = NotificationService.class,
            cardinality = ReferenceCardinality.MULTIPLE,
            policy = ReferencePolicy.DYNAMIC,
            policyOption = ReferencePolicyOption.GREEDY,
            bind = "bindNotificationService",
            unbind = "unbindNotificationService"
    )
    protected void bindNotificationService(NotificationService service) {
        notificationServices.add(service);
    }

    protected void unbindNotificationService(NotificationService service) {
        notificationServices.remove(service);
    }

    public void dispatchMessage(String message) {
        // All active implementations are iterated through smoothly
        for (NotificationService service : notificationServices) {
            service.sendNotification(message);
        }
    }
}

Key Architectural Benefits

Consequently, by utilizing ReferenceCardinality.MULTIPLE combined with ReferencePolicy.DYNAMIC, decoupling is strictly achieved within AEM backend architectures. Furthermore, new business logic can be introduced via separate OSGi bundles without modifying existing code bundles, matching the requirements of microservice-oriented design patterns within monolithic platforms.

Ashish Sharma

I’ve always believed that collaboration is the engine of progress. While many say knowledge is power, I believe the true power lies in its distribution. To that end, I am building a curated knowledge base of my professional journey—refined by AI for maximum clarity and depth. Whether you’re here to master a new skill or sharpen an existing one, my goal is to provide a roadmap for your success. This collection will evolve as I do, and I welcome your insights and dialogue as we grow together.
  1. A point that often becomes important in real-world AEM projects is how service ranking interacts with MULTIPLE cardinality when several implementations are available. It would be interesting to see an example of dynamically adding or removing services at runtime and how the component behaves under different reference policies, since that can help avoid unexpected binding issues.

Leave a Reply

Your email address will not be published. Required fields are marked *