OSGi Services Explained
Building Reusable Business Logic in Enterprise AEM
Sling Models are responsible for preparing data, but they shouldn't contain the application's business logic. As AEM projects grow, multiple components often need to perform the same operationsβcalling external APIs, validating data, generating URLs, or communicating with other systems. Rather than duplicating that code, AEM uses OSGi Services to centralize reusable business logic. In this article, we'll explore what OSGi is, why it exists, and how it fits into the overall AEM architecture.
Reading time: 10β12 min
AEM Cloud Master Series
This article is Part 5 of a 10-part series designed to help frontend engineers and AEM developers understand Adobe Experience Manager from architecture to production deployment.
Series Roadmap
β Part 1 β Understanding Adobe Experience Manager Architecture
β Part 2 β The Complete AEM Request Lifecycle
β Part 3 β Inside Apache Sling
β Part 4 β Sling Models & Dependency Injection
β Part 5 β OSGi Services Explained (Current)
Part 6 β HTL Explained
Part 7 β Dispatcher Deep Dive
Part 8 β Building Enterprise Components in AEM
Part 9 β React + Adobe AEM Cloud Service
Part 10 β From Development to Production
In this article, we'll explore how OSGi Services centralize reusable business logic, integrate with external systems, and help keep AEM applications modular, testable, and maintainable as they grow.
Why OSGi Services Exist
Imagine you're building a product website.
Several components need to display information from the same backend service.
For example:
A Hero banner displays featured products.
A Search component retrieves search results.
A Recommendation widget suggests related products.
A Product Card shows live pricing.
All four components need data from the same API.
One approach would be to write API calls inside every Sling Model.
That works initially, but problems appear quickly.
The same code is copied into multiple models.
A change to the API requires updates in several places.
Testing becomes harder.
Bugs become easier to introduce.
Instead, AEM encourages a different design.
Move reusable business logic into a single service and let every component share it.
Where OSGi Fits
Let's revisit the rendering pipeline.
Notice that the Sling Model doesn't perform the business logic itself.
Instead, it delegates reusable work to an OSGi Service.
This keeps each layer focused on one responsibility.
What Is OSGi?
OSGi stands for Open Services Gateway initiative, although most AEM developers simply think of it as the framework responsible for managing reusable application services.
The name sounds intimidating, but the idea is surprisingly simple.
Think of OSGi as a service container.
Instead of creating objects manually throughout your application, services are registered once and can be injected wherever they're needed.
If you've used Dependency Injection in Spring Boot, the concept will feel familiar.
The difference is that AEM uses the OSGi framework to manage those services.
A Simple Example
Imagine multiple components need to generate product URLs.
Instead of writing the same logic everywhere, create one service.
public interface ProductUrlService {
String buildProductUrl(String productId);
}
Now provide an implementation.
@Component(service = ProductUrlService.class)
public class ProductUrlServiceImpl implements ProductUrlService {
@Override
public String buildProductUrl(String productId) {
return "/products/" + productId + ".html";
}
}
This service is now available throughout the application.
Using the Service
A Sling Model simply requests the service.
@Model(adaptables = SlingHttpServletRequest.class)
public class ProductModel {
@OSGiService
private ProductUrlService productUrlService;
@ValueMapValue
private String productId;
public String getProductUrl() {
return productUrlService.buildProductUrl(productId);
}
}
Notice how the Sling Model doesn't know how URLs are built.
It simply delegates the work.
If the URL structure changes later, only the service needs to be updated.
Every component benefits automatically.
Separation of Responsibilities
A useful way to think about AEM is that every layer has a clearly defined job.
| Layer | Responsibility |
|---|---|
| JCR | Store content |
| Apache Sling | Resolve requests |
| Sling Model | Prepare presentation data |
| OSGi Service | Execute reusable business logic |
| HTL | Generate HTML |
When these responsibilities stay separate, applications remain much easier to maintain as they grow.
A Real Enterprise Example
Suppose you're building an enterprise search experience.
The author configures:
Search title
Placeholder text
Default filters
Those values are stored in the JCR.
However, the actual search results come from Azure AI Search.
The architecture might look like this.
Notice how each layer has a distinct responsibility.
The Sling Model prepares the page.
The Search Service communicates with Azure.
HTL renders the results.
This separation makes the component reusable, easier to test, and much simpler to maintain as the integration evolves.
Understanding the OSGi Lifecycle
One of the biggest advantages of OSGi is that developers don't need to manage service instances manually.
When AEM starts, the OSGi framework discovers your services, creates them, manages their lifecycle, and makes them available for dependency injection.
As a developer, you simply define the service.
The framework takes care of the rest.
A simplified lifecycle looks like this.
Unlike creating objects with new, OSGi manages a single service instance that can be shared throughout the application.
Declarative Services
Modern AEM projects typically use OSGi Declarative Services (DS).
Instead of writing XML configuration files by hand, services are declared using annotations.
The most common annotation is:
@Component(service = SearchService.class)
public class SearchServiceImpl implements SearchService {
}
This tells AEM:
Register this class as the implementation of
SearchService.
Once registered, other parts of the application can inject it automatically.
Service Interfaces
A best practice in enterprise AEM development is to depend on interfaces, not implementations.
Instead of injecting:
SearchServiceImpl
inject:
SearchService
Why?
Because callers shouldn't care how the service works.
They only care what it provides.
This loose coupling makes applications easier to extend and test.
For example, replacing an Azure Search implementation with another search provider might only require changing the service implementation, while the rest of the application remains untouched.
Injecting Services
Once a service has been registered, Sling Models can request it through dependency injection.
@OSGiService
private SearchService searchService;
No constructors.
No factory methods.
No manual object creation.
Sling simply injects the registered service before the model is used.
This keeps the model focused on presentation instead of infrastructure.
Service Configuration
Hardcoding values inside services is rarely a good idea.
Imagine an external search service.
These values might change between environments:
API endpoint
API key
Timeout
Index name
Instead of hardcoding them, OSGi allows services to receive configuration.
A simplified example looks like this.
@Activate
protected void activate(SearchConfiguration config) {
this.endpoint = config.endpoint();
}
When the configuration changes, AEM can update the service without requiring changes throughout the application.
This makes deployments more flexible and keeps environment-specific values out of the source code.
Why OSGi Services Should Be Stateless
Most OSGi services are effectively shared across the application.
That means every request may use the same service instance.
This is efficient, but it also means services should generally be stateless.
A good service:
performs work,
returns a result,
doesn't store request-specific information.
For example, this is a good design:
public List<SearchResult> search(String query)
The service receives everything it needs as parameters.
It doesn't remember information from previous requests.
That makes it safe for multiple users to access simultaneously.
Enterprise Examples
Once you start looking at real AEM projects, you'll notice that OSGi Services appear almost everywhere.
Typical examples include:
| Service | Responsibility |
|---|---|
| SearchService | Query Azure AI Search |
| ProductService | Retrieve product information |
| UrlService | Generate consistent URLs |
| ValidationService | Validate authored content |
| AnalyticsService | Send tracking events |
| NotificationService | Integrate with messaging platforms |
None of these services generate HTML.
None of them know anything about HTL.
Their job is simply to provide reusable business functionality that any component can consume.
Mental Model
A useful way to think about OSGi Services is to compare them to departments inside a company.
The Sling Model is the employee working on a document.
The OSGi Services are specialized departments.
If the employee needs pricing information, they contact the Pricing department.
If they need customer information, they contact Customer Support.
If they need shipping details, they contact Logistics.
The employee doesn't perform every job themselves.
They simply ask the appropriate department.
OSGi Services work the same way.
Instead of every component implementing its own business logic, specialized services handle reusable tasks for the entire application.
Designing Good OSGi Services
Writing an OSGi Service isn't just about making code reusable.
It's about creating a stable layer that other parts of your application can rely on.
A well-designed service should have one clear responsibility.
For example:
A
SearchServiceperforms searches.A
UrlServicegenerates URLs.An
ImageServiceprepares image URLs.A
ValidationServicevalidates business rules.
If a service starts doing several unrelated jobs, it's usually time to split it into smaller services.
The same principle that applies to components also applies to services:
One responsibility. One service.
A Good Service Is Reusable
Imagine you have five different components that need product information.
A poor design would have each Sling Model calling the external API independently.
HeroModel
β
Product API
ProductCardModel
β
Product API
RecommendationModel
β
Product API
Now imagine the API changes.
You have five different places to update.
Instead, every component should depend on the same service.
Now the integration exists in one place.
Every component automatically benefits from improvements, bug fixes, and new functionality.
Testing Becomes Easier
Another advantage of using interfaces is testing.
Suppose your Sling Model depends on:
@OSGiService
private ProductService productService;
During testing, you don't need to call the real API.
Instead, you can replace it with a mock implementation.
public class MockProductService implements ProductService {
@Override
public Product getProduct(String id) {
return new Product("Surface Laptop");
}
}
Your tests become:
faster,
more reliable,
independent of external systems.
That's one of the reasons interface-based design is considered a best practice in enterprise Java applications.
Common Mistakes
As AEM projects grow, I've seen a few patterns repeated across different teams.
Avoiding these mistakes will make your services much easier to maintain.
Putting Business Logic in Sling Models
A Sling Model should prepare data.
It shouldn't become the application's business layer.
If multiple models need the same code, move it into an OSGi Service.
Making One Giant Service
Sometimes developers create a single service that handles:
search,
validation,
URLs,
pricing,
notifications,
analytics.
That quickly becomes difficult to understand and maintain.
Smaller, focused services are easier to reuse and test.
Hardcoding Configuration
Avoid code like this:
private static final String API =
"https://api.company.com";
URLs, API keys, timeouts, and similar values should come from OSGi configuration rather than being embedded in the source code.
This keeps services portable across development, staging, and production environments.
Storing Request Data Inside Services
Because services are shared across requests, they shouldn't keep request-specific information.
Avoid storing values like:
current user,
current request,
current page.
Instead, pass that information as method parameters.
Stateless services are easier to scale and safer to use in concurrent environments.
Service Lifecycle in Context
Now let's place OSGi back into the overall AEM architecture.
Notice that OSGi Services never generate HTML.
They don't know anything about templates.
They simply provide business capabilities that other layers can use.
This separation is one of the reasons enterprise AEM applications remain maintainable as new integrations are added over time.
Production Example
A pattern I've used on enterprise projects looks something like this:
A Sling Model reads authored component content from the JCR.
The model asks a
SearchServicefor search results.The
SearchServicecommunicates with Azure AI Search.The service transforms the API response into Java objects.
The Sling Model combines authored content with search results.
HTL renders the final HTML.
The component doesn't know how Azure Search works.
The Search Service doesn't know anything about HTL.
Each layer stays focused on its own responsibility.
As the application grows, that separation becomes increasingly valuable because changes in one layer rarely affect the others.
Debugging OSGi Services
When an OSGi Service doesn't behave as expected, the problem usually isn't in the HTL template or even the Sling Model.
A structured debugging approach can save a lot of time.
Start by asking a few simple questions:
Has the service been registered successfully?
Is the correct implementation being injected?
Is the service active?
Is the configuration loaded correctly?
Is the external system responding as expected?
Working through these questions in order usually leads you to the root cause much faster than jumping straight into the code.
Following the same process every time helps narrow the problem to one layer instead of guessing where it might be.
OSGi Service Design Checklist
As projects grow, service design becomes more important than the implementation itself.
Before creating a new service, ask yourself:
Does this service have a single responsibility?
Could another component reuse this logic?
Should this behavior live in a Sling Model instead?
Does it depend on configuration?
Is it stateless?
Am I programming against an interface?
Can it be tested independently?
If you answer "yes" to most of these questions, you're probably designing the service at the right level.
OSGi Service Cheat Sheet
| Concept | Purpose |
|---|---|
@Component | Registers a class as an OSGi service |
| Interface | Defines the service contract |
| Implementation | Contains the business logic |
@OSGiService | Injects a registered service |
@Activate | Initializes the service after activation |
| OSGi Configuration | Provides environment-specific settings |
Final Thoughts
As your AEM applications become larger, you'll notice that components become smaller while services become more important.
That's a good sign.
A component should focus on presenting information.
A Sling Model should prepare that information.
An OSGi Service should contain the reusable business logic that powers multiple parts of the application.
Keeping those responsibilities separate has several advantages:
Components remain simple and easy to read.
Business logic is implemented once instead of copied across the codebase.
Integrations with external systems stay isolated behind a clean interface.
Testing becomes easier because services can be mocked independently.
Future changes are localized, reducing the impact on the rest of the application.
This layered approach is one of the reasons enterprise AEM applications remain maintainable even after years of development.
Key Takeaways
OSGi Services are the reusable business layer of an AEM application.
Register services with
@Componentand inject them with@OSGiService.Program against interfaces rather than concrete implementations.
Keep services stateless and focused on a single responsibility.
Store environment-specific values in OSGi configuration instead of hardcoding them.
Let Sling Models orchestrate data preparation while OSGi Services perform reusable work.
Keep HTL focused exclusively on rendering HTML.
A simple way to remember the relationship between these layers is:
Sling finds the content, Sling Models prepare the data, OSGi Services perform the work, and HTL renders the result.
Understanding that separation of responsibilities is one of the key differences between small AEM projects and enterprise-scale implementations. It leads to applications that are easier to maintain, easier to extend, and significantly easier to debug as new integrations and business requirements are introduced.
Continue Reading
You've now seen how OSGi Services encapsulate reusable business logic, integrate with external systems, and keep Sling Models focused on preparing presentation data rather than implementing application logic.
In the next article, we'll move to the presentation layer and explore HTL (HTML Template Language). You'll learn how HTL transforms prepared data into secure, maintainable HTML, how data-sly-* attributes work, and why keeping templates free of business logic is one of the key principles of AEM development.
Next Article β Part 6: HTL Explained
By understanding both Sling Models and OSGi Services, you've now covered the two core Java building blocks used in almost every AEM component. The next step is learning how HTL brings that prepared data to life by rendering the final HTML delivered to the browser.
Masoud
September 8th, 2025