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.
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.