Skip to main content

Creating OSGi Modules and Sharing Packages

  Overview This guide walks through creating two OSGi modules in Liferay 7.4 and sharing a Java package between them using the standard API + Implementation pattern. This is the most common and correct approach for exposing reusable services in a Liferay-based architecture. In OSGi, a bundle (module) does not expose its internal packages by default — everything is private unless explicitly exported. To share code between modules, the standard practice is to split functionality into: · An API module — contains interfaces/DTOs and exports the package · A Service (Implementation) module — implements the interface, imports the package, and registers the implementation as an OSGi component/service The consuming module then simply imports the API package and looks up the service using @Reference injection. Step 1: Create the API Module Using Liferay Workspace + Blade CLI, generate a new API module: blade create -t api -p com.sgc.greeting greeting-api This generates a Gradle module unde...

Service Builder Implementation in Liferay 7.4

 




If you've worked with Liferay DXP, you already know that building custom data-driven portlets involves a lot of boilerplate — entity models, persistence layers, service interfaces, SQL scripts. Service Builder exists to eliminate exactly that. In this guide, we'll walk through everything you need to know to use Service Builder effectively in Liferay DXP 7.4, from project setup to production best practices.

What Is Service Builder?

Service Builder is Liferay's proprietary code generation framework. You describe your data model in a single XML file (service.xml), and Service Builder generates a fully functional, layered Java application stack — models, persistence, services, and SQL DDL — all wired into Liferay's OSGi and Declarative Services (DS) framework.

The layers it generates automatically include:

  • Model Layer — Entity POJOs and their implementations
  • Persistence Layer — Hibernate-based DAOs with auto-generated finder methods
  • Service Layer — Local service interfaces, remote service interfaces, and base implementations
  • Util ClassesServiceUtil and ClpSerializer helper classes
  • SQL Scripts — DDL scripts for MySQL, Oracle, PostgreSQL, and more

In Liferay 7.4, all generated code is deployed as OSGi bundles and integrates directly with Liferay's Declarative Services component framework.

When Should You Use Service Builder?

Service Builder is the right tool when your custom module needs:

  • Custom database tables managed by Liferay's persistence layer
  • Auto-generated finder methods for complex queries
  • Entity caching via Liferay's built-in cache infrastructure
  • Permission checking integration through resourcedModel
  • Workflow support for custom entities
  • Export/import and Staging support

The Two-Module Architecture

One of the first things to understand about Service Builder is that it always generates code across two OSGi modules:

ModulePackage PatternContents
*-apicom.example.service.apiInterfaces, Model interfaces, ServiceUtil, constants
*-servicecom.example.service.implImplementation classes, persistence, SQL scripts

This separation enforces clean API boundaries and allows other modules to depend only on the *-api module without pulling in implementation details.

Setting Up Your Project

Creating the Module

Use the Blade CLI to scaffold both modules in one command:

blade create -t service-builder -p com.example.service -c Employee my-module

This generates my-module-api and my-module-service automatically.

Building and Deploying

# Regenerate Service Builder code
./gradlew :modules:my-module:my-module-service:buildService

# Deploy both modules
./gradlew :modules:my-module:my-module-api:deploy
./gradlew :modules:my-module:my-module-service:deploy

# Full clean rebuild and deploy
./gradlew clean buildService deploy

Project Structure
my-module/

  my-module-api/
    src/main/java/
      com/example/model/       ← Model interfaces (DO NOT EDIT)
      com/example/service/     ← Service interfaces (DO NOT EDIT)
  my-module-service/
    src/main/java/
      com/example/model/impl/  ← EmployeeImpl.java (SAFE TO EDIT)
      com/example/service/impl/← LocalServiceImpl, ServiceImpl (SAFE)
    service.xml                ← The master descriptor
    build.gradle
    bnd.bnd

Key Gradle Configuration
groovy

// my-module-service/build.gradle
dependencies {
    api project(":modules:my-module:my-module-api")
    compileOnly group: "com.liferay.portal", name: "release.portal.api"
}

buildService {
    apiDir = "../my-module-api/src/main/java"
}

The bnd.bnd File
Bundle-Name: My Module Service

Bundle-SymbolicName: com.example.service
Bundle-Version: 1.0.0
Liferay-Require-SchemaVersion: 1.0.0
Liferay-Service: true

The Liferay-Service: true flag is critical — it tells Liferay to register generated services like EmployeeLocalService and EmployeePersistence into the OSGi service registry at deploy time. Without it, your services simply won't be available.

The service.xml Descriptor

The service.xml file is the single source of truth for Service Builder. Everything — entities, columns, finders, ordering, relationships — is defined here.

Root Element
xml

<?xml version="1.0"?>
<!DOCTYPE service-builder PUBLIC
  "-//Liferay//DTD Service Builder 7.4.0//EN"
  "http://www.liferay.com/dtd/liferay-service-builder_7_4_0.dtd">

<service-builder
  dependency-injector="ds"
  package-path="com.example.service">
  <author>Your Name</author>
  <namespace>EX</namespace>
</service-builder>

The namespace attribute prefixes all SQL table names — so your Employee entity becomes EX_Employee in the database.

Defining an Entity 

xml

<entity
  name="Employee"
  local-service="true"
  remote-service="true"
  uuid="true">
AttributeDescription
local-serviceGenerates EmployeeLocalService interface and implementation
remote-serviceGenerates permission-checked remote service layer
uuidAdds uuid column for Staged model / Publications support

Column Types

<column name="employeeId"   primary="true" type="long"/>
<column name="groupId"      type="long"/>
<column name="companyId"    type="long"/>
<column name="userId"       type="long"/>
<column name="createDate"   type="Date"/>
<column name="modifiedDate" type="Date"/>
<column name="firstName"    type="String"/>
<column name="email"        type="String"/>
<column name="department"   type="String"/>
<column name="salary"       type="double"/>
<column name="active"       type="boolean"/>

Defining Finders

Finders are automatically turned into findByXxx and countByXxx methods on the persistence interface:

xml

<finder name="GroupId" return-type="Collection">
  <finder-column name="groupId"/>
</finder>

<finder name="Email" return-type="Employee" unique="true">
  <finder-column name="email"/>
</finder>

<finder name="Department_Active" return-type="Collection">
  <finder-column name="department"/>
  <finder-column name="active" comparator="="/>
</finder>

Setting unique="true" generates a single-entity finder that throws NoSuchEmployeeException if not found.

Understanding Generated Code

What's Safe to Edit vs. What Isn't



Customizing EmployeeImpl

Add transient model-level methods here — these are never overwritten:java

public class EmployeeImpl extends EmployeeBaseImpl {

    public String getFullName() {
        return getFirstName() + " " + getLastName();
    }

    public boolean isSenior() {
        long diff = System.currentTimeMillis() - getHireDate().getTime();
        return diff > 
    }
}

Implementing Business Logic

CRUD in EmployeeLocalServiceImpl

@Component(
    property = "model.class.name=com.example.service.model.Employee",
    service = AopService.class
)
public class EmployeeLocalServiceImpl extends EmployeeLocalServiceBaseImpl {

    public Employee addEmployee(
        long userId, long groupId,
        String firstName, String lastName,
        String email, String department,
        ServiceContext serviceContext) throws PortalException {

        User user = userLocalService.getUser(userId);
        long employeeId = counterLocalService.increment();

        Employee employee = employeePersistence.create(employeeId);

        // Audit fields
        employee.setGroupId(groupId);
        employee.setCompanyId(serviceContext.getCompanyId());
        employee.setUserId(userId);
        employee.setUserName(user.getFullName());
        employee.setCreateDate(serviceContext.getCreateDate(new Date()));
        employee.setModifiedDate(serviceContext.getModifiedDate(new Date()));

        // Business fields
        employee.setFirstName(firstName);
        employee.setLastName(lastName);
        employee.setEmail(email);
        employee.setDepartment(department);
        employee.setStatus(WorkflowConstants.STATUS_APPROVED);
        employee.setActive(true);

        employeePersistence.update(employee);

        // Resource permissions
        resourceLocalService.addResources(
            serviceContext.getCompanyId(), groupId, userId,
            Employee.class.getName(), employeeId,
            false, true, true);

        return employee;
    }
}

Using Generated Finders

public List<Employee> getEmployeesByGroupId(long groupId) {
    return employeePersistence.findByGroupId(groupId);
}

public List<Employee> getActiveEmployeesByDepartment(
    String department, boolean active) {
    return employeePersistence.findByDepartment_Active(department, active);
}

public Employee getEmployeeByEmail(String email)
    throws NoSuchEmployeeException {
    return employeePersistence.findByEmail(email);
}

Dynamic Queries for Complex Searches

When generated finders aren't enough, use DynamicQuery:

public List<Employee> searchEmployees(
    long companyId, String keywords, int start, int end) {

    DynamicQuery query = dynamicQuery();

    if (Validator.isNotNull(keywords)) {
        String like = "%" + keywords + "%";
        Disjunction dis = RestrictionsFactoryUtil.disjunction();
        dis.add(RestrictionsFactoryUtil.ilike("firstName", like));
        dis.add(RestrictionsFactoryUtil.ilike("lastName", like));
        dis.add(RestrictionsFactoryUtil.ilike("email", like));
        query.add(dis);
    }

    query.add(PropertyFactoryUtil.forName("companyId").eq(companyId));
    query.addOrder(OrderFactoryUtil.asc("lastName"));

    return dynamicQuery(query, start, end);
}

Remote Service & Permission Checks

The EmployeeServiceImpl layer wraps the local service with permission checks. This is what gets exposed via JSONWS and REST:

public class EmployeeServiceImpl extends EmployeeServiceBaseImpl {

    public Employee addEmployee(
        long groupId, String firstName, String lastName,
        String email, String department,
        ServiceContext serviceContext) throws PortalException {

        EmployeePermission.check(
            getPermissionChecker(), groupId,
            EmployeeActionKeys.ADD_EMPLOYEE);

        return employeeLocalService.addEmployee(
            getUserId(), groupId, firstName, lastName,
            email, department, serviceContext);
    }
}

Custom SQL for Complex Native Queries

For queries that go beyond what finders or DynamicQuery can handle, use Liferay's Custom SQL support.

Place your SQL in src/main/resources/META-INF/custom-sql/default.xml:

<custom-sql>
  <sql id="com.example.service.persistence.EmployeeFinder.findByKeyword">
    <![CDATA[
      SELECT e.* FROM EX_Employee e
      WHERE e.companyId = ?
      AND (LOWER(e.firstName) LIKE LOWER(?)
        OR LOWER(e.lastName)  LIKE LOWER(?)
        OR LOWER(e.email)     LIKE LOWER(?))
      ORDER BY e.lastName ASC
    ]]>
  </sql>
</custom-sql>

Then implement a EmployeeFinderImpl class that loads and executes this SQL using CustomSQLUtil.get().

Database Schema Upgrades

When you add or modify columns in service.xml, you need an UpgradeProcess to migrate existing data:

public class UpgradeDepartment extends UpgradeProcess {

    @Override
    protected void doUpgrade() throws Exception {
        if (!hasColumn("EX_Employee", "departmentCode")) {
            runSQL("ALTER TABLE EX_Employee ADD departmentCode VARCHAR(20)");
        }
        runSQL("UPDATE EX_Employee SET departmentCode = " +
               "UPPER(SUBSTRING(department, 1, 3))");
    }
}

Register it with the upgrade framework:

@Component(service = UpgradeStepRegistrator.class)
public class ServiceUpgrade implements UpgradeStepRegistrator {

    @Override
    public void register(Registry registry) {
        registry.register("0.0.1", "1.0.0", new UpgradeDepartment());
    }
}

Always update Liferay-Require-SchemaVersion in bnd.bnd to match the target version. Without this, upgrade processes will never be triggered.

Consuming Generated Services

From OSGi Components (Recommended)

@Component(service = EmployeeManager.class)
public class EmployeeManager {

    @Reference
    private EmployeeLocalService _employeeLocalService;

    public void processEmployee(long employeeId) throws PortalException {
        Employee emp = _employeeLocalService.getEmployee(employeeId);
        // business logic
    }
}

From Portlet Classes

public class EmployeePortlet extends MVCPortlet {

    @Reference
    private EmployeeLocalService _employeeLocalService;

    @Override
    public void processAction(ActionRequest request, ActionResponse response)
        throws IOException, PortletException {

        try {
            ServiceContext ctx = ServiceContextFactory.getInstance(
                Employee.class.getName(), request);

            _employeeLocalService.addEmployee(
                ctx.getUserId(), ctx.getScopeGroupId(),
                "John", "Doe", "john@example.com", "Engineering", ctx);

        } catch (PortalException pe) {
            SessionErrors.add(request, pe.getClass());
        }
    }
}

Use @Reference for OSGi-safe dependency injection. Never use @Autowired — Spring annotations don't work in OSGi DS components.

Top 10 Best Practices




Common Errors and How to Fix Them

ErrorResolution
buildService fails with "Cannot resolve entity"Check namespace conflicts; ensure all referenced entities are defined in service.xml
NoSuchEmployeeException on startupSchema version mismatch; run portal upgrade or check Liferay-Require-SchemaVersion
UnsatisfiedDependencyException for @ReferenceAPI module not exported in bnd.bnd; add Export-Package: com.example.service.api.*
Table already exists on deployService Builder attempts DDL creation on deploy; safe to ignore if schema is already correct
Finder method not generatedColumn in <finder> must exist in same entity; run buildService again after fix
StackOverflowError in circular callsDo not inject EmployeeLocalService inside EmployeeLocalServiceImpl; use persistence directly

Quick Reference: Key Generated Methods

MethodDescription
addEmployee(Employee)Persist new entity (low-level)
getEmployee(long id)Fetch by PK; throws exception if not found
fetchEmployee(long id)Fetch by PK; returns null if not found
updateEmployee(Employee)Persist changes to existing entity
deleteEmployee(long id)Remove by primary key
getEmployees(int start, int end)Paginated list of all records
getEmployeesCount()Total count of all records
dynamicQuery(DynamicQuery)Execute a DynamicQuery
createEmployee(long id)Create unsaved instance (not yet persisted)

Wrapping Up

Service Builder remains one of the most productive tools in the Liferay DXP 7.4 developer toolkit. Once you understand its patterns — the two-module split, what to edit and what not to, how finders and upgrades work — it dramatically reduces the time spent on persistence boilerplate and lets you focus on actual business logic.

The key things to always keep in mind:

  • service.xml is your single source of truth — define everything there
  • Run buildService after every service.xml change
  • Only *Impl.java files are safe to edit
  • Remote service always wraps local service with permission checks
  • Schema changes need an UpgradeProcess and a version bump

Happy coding with Liferay!

Comments

Popular posts from this blog

service builder with crud operation in liferay

   Crud and Search opration in Liferay:      ->  Liferay use service builder techniq for Crud opration.    ->  Service builder purform by  Service.xml file.    ->  Service.xml file create table in database and also create class and diffrent method.   1)      Create service.xml file.             ->Create service.xml file in WEB-INF and write below code.             ->CODE:        < service-builder package-path = "com.test" >         < namespace > qr </ namespace >           < entity name = "Searchclass" local-service = "true"                     ...

How to create new site programmaticly in liferay with validation

Create site in liferay <%@page import="javax.portlet.PortletPreferences"%> <%@page import="com.liferay.portal.kernel.util.ParamUtil"%> <%@page import="com.liferay.portal.kernel.util.HtmlUtil"%> <%@page import="com.liferay.portal.kernel.util.StringPool"%> <%@page import="com.liferay.portal.kernel.util.UnicodeProperties"%> <%@page import="com.liferay.portal.service.LayoutSetPrototypeServiceUtil"%> <%@page import="com.liferay.portal.model.LayoutSetPrototype"%> <%@page import="com.liferay.portal.service.GroupLocalServiceUtil"%> <%@page import="java.util.List"%> <%@page import="com.liferay.portal.kernel.bean.BeanParamUtil"%> <%@page import="com.liferay.portal.theme.ThemeDisplay"%> <%@page import="com.liferay.portal.model.Group"%> <%@page import="com.liferay.portal.kernel.util.WebK...

The Ultimate Guide to Liferay DXP Performance Tuning: Speed Up Your Portal

The Ultimate Guide to Liferay DXP Performance Tuning: Speed Up Your Portal In the enterprise web space, milliseconds equal millions. Whether you are running a B2B commerce storefront, a customer support portal, or an employee intranet on Liferay DXP, slow load times will devastate your user experience and destroy your SEO rankings. Out of the box, Liferay is configured to run on almost any machine. This means its default settings are highly conservative to ensure compatibility, not maximum performance. If you are launching a production environment without tuning your server, you are leaving massive amounts of speed and scalability on the table. In this comprehensive, deep-dive guide, we are going to explore the critical layers of Liferay performance tuning. We will cover backend Java Virtual Machine (JVM) configuration, Database Connection Pooling, Elasticsearch optimization, and Frontend caching strategies. By the end of this guide, you will have a blazing-fast, enterprise-grade...