If you’re going to use Liferay to develop serious, enterprise-grade systems and applications, you will need to learn how to take advantage of the various tools the platform provides, such as Service Builder, Dynamic Queries, and Custom SQLs. I use them in my everyday development and I’ll show you how to do that – step by step.

Disclaimer: this article was originally published on August 11, 2023. However, in February 2024 it was updated with new information with the aim to provide a business context for the use of the described features. 

Service Builder

Let’s start with the Service Builder. It plays a crucial role in Liferay, providing an abstraction layer between the application and the database. This allows for the separation of business logic from the data access layer, resulting in easier management and development of the application. Thanks to the Service Builder, it is possible to generate a set of service classes that implement interfaces for each entity in the database. Additionally, model objects representing corresponding records in the database are generated.

When building an application based on Service Builder, it is necessary to configure the appropriate file and directory structure. All files related to Service Builder are located in a dedicated folder named “service.”

The main elements of the structure are:

  • service.xml file – it contains data model definitions in XML format, where we specify the entities and their fields that should be stored in the database
  • service-impl folder – it contains the generated service classes that implement interfaces for individual entities
  • service-api folder – it stores service interfaces used for communication with the application’s business logic layer

How to get started with Service Builder?

The first step is to define the data model, i.e., the entities and fields we want to store in the database. To do this, we create a service.xml file in the service folder. Here’s sample data model definition created for the purposes of this article: 

<?xml version="1.0"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.4.0//EN" "">

<service-builder package-path="">

  <entity name="Book" local-service="true" remote-service="true">
      <column name="bookId" type="long" primary="true"/>
      <column name="title" type="String" />
      <column name="author" type="String" />

      <finder name="FindByAuthor" return-type="Collection">
        <finder-column name="author" />


Let’s take a look at each of the tags used in the script above:

  • <service-builder> – the main tag that specifies this is a Service Builder file. The package-path attribute defines the package path in which the service classes and model objects will be generated
  • <namespace> – it specifies a unique prefix used in the generated names of service classes and model objects
  • <entity> – defines the data model, i.e., the entity in the database. The name, local-service, and remote-service attributes indicate the entity’s name, whether the local service should be generated (true or false), and whether the remote service should be generated (true or false)
  • <column> – specifies a field for a given entity. The name and type attributes represent the field’s name and its type, respectively
  • <finder> – defines a finder, which is a database query that allows filtering data based on selected criteria. The name attribute is the finder’s name, and return-type specifies the type of data to be returned (e.g., Collection, List, Single)
  • <finder-column> – specifies the field by which data should be filtered in a particular finder

In this example, I have defined a model called Book with fields bookId, title, and author. Additionally, I have created a finder named FindByAuthor, which allows searching for books by the author’s name. With this definition, Service Builder will automatically generate service classes and model objects for this entity, which I can then use to manage data in the database in my Liferay application.

The next step is to generate the service classes and model objects based on the service.xml file. For this purpose, I can use either tools provided by Liferay or Gradle. Executing the appropriate command will automatically generate service classes in the service-impl folder and model objects in the service-api folder.

After generating the service classes and model objects, I can use them in my application, which provides simple access to CRUD (Create, Read, Update, Delete) operations on data in the database.

Here’s a sample script that demonstrates how to use the service to add a new book to the database:

// Create new book
Book book = BookLocalServiceUtil.createBook(CounterLocalServiceUtil.increment(Book.class.getName()));
book.setTitle("Example book");
book.setAuthor("John Doe");

// Add book to database

book = BookLocalServiceUtil.addBook(book);

System.out.println("New book was added ID: " + book.getBookId());
System.out.println("Book: " + book);

This results in displaying the following message:

New book was added ID: 3
Book: {“bookId”: 3, “title”: “Example book”, “author”: “John Doe”}

Need a team with Liferay experience? 🔥 Let us know at and we’ll arrange free consultations

Dynamic Queries

Dynamic Queries allow you to generate database queries programmatically – making them a crucial tool for application development in the Liferay environment. They enable flexible generation of database queries, which is essential when complex filters or dynamic data manipulations are needed (for example, when you need intuitive and flexible search functionalities). Thanks to Dynamic Queries, you can create more advanced and tailored queries – which means the systems you make can meet more specific and complex business requirements.

In Liferay, Dynamic Query is accessible through the DynamicQuery class API and allows data manipulation in a dynamic manner.

Here’s an example of using Dynamic Queries to retrieve all books written by the author “John Doe”:

// Create new dynamicQuery

DynamicQuery dynamicQuery = BookLocalServiceUtil.dynamicQuery();

// Add a condition for the "author" field equal to "John Doe"
dynamicQuery.add(RestrictionsFactoryUtil.eq("author", "John Doe"));

// Retrieve a list of books that meet the condition
List<Book> booksByJohnDoe = BookLocalServiceUtil.dynamicQuery(dynamicQuery);

How to use Conjunction in Liferay?

Conjunction and Disjunction allow combining conditions in Dynamic Queries, enabling more advanced data filtering.

Conjunction represents the logical AND condition. This means that all specified conditions must be met for a record to be included in the search results. For example, if you have two conditions – A and B – using conjunction, both must be true for a record to be returned in the results.

How to use Disjunction in Liferay?

Disjunction represents the logical OR condition. This means that at least one specified condition must be met for a record to be included in the search results. For example, if you have two conditions – A and B – and one of the conditions is true for a record, it’s enough for it to be returned in the results when using disjunction.

Here’s an example of using disjunction to retrieve books written by the author “John Doe” or books containing the phrase “Harry Potter” in the title:

// Create a new dynamic query object
DynamicQuery dynamicQuery = BookLocalServiceUtil.dynamicQuery();

// Create a criterion for the "author" field equal to "John Doe"
Criterion authorCriterion = RestrictionsFactoryUtil.eq("author", "John Doe");

// Create a criterion for the "title" field containing "Harry Potter"
Criterion titleCriterion ="title", "%Harry Potter%");

// Create a Disjunction object and add both criteria to it
Disjunction disjunction = RestrictionsFactoryUtil.disjunction();

// Add the Disjunction as a condition to the dynamic query

// Retrieve a list of books that meet the condition
List<Book> books = BookLocalServiceUtil.dynamicQuery(dynamicQuery);

How to use Projection in Liferay?

In the context of Liferay’s Dynamic Query API, Projection can be applied to specify which entity fields should be included in the query results. This allows fetching only those fields required to meet the application’s needs, avoiding excessive data transfer, and reducing the system’s load.

In Liferay’s Dynamic Query API, you can apply Projection using the setProjection() method on the dynamic query object (DynamicQuery). For example, if you want to retrieve only the title and author fields from the Book entity instead of the entire Book object, you can do it using the following script:

DynamicQuery dynamicQuery = BookLocalServiceUtil.dynamicQuery();
ProjectionList projectionList = ProjectionFactoryUtil.projectionList();

// Now you can execute the query, which will return only the "title" and "author" fields
List<Object[]> results = BookLocalServiceUtil.dynamicQuery(dynamicQuery);

// The results are a list of object arrays, where each array contains the "title" and "author" fields
for (Object[] result : results) {
  String title = (String) result[0];
  String author = (String) result[1];
  System.out.println("Title: " + title + ", Author: " + author);

The query result will be a list of object arrays, where each array contains the values of the title and author fields corresponding to the Book entity. This allows you to limit the retrieved data to only these two fields, which can speed up the application, especially if the entities have many other fields that are currently unnecessary.

Custom SQL

Image source: Pexels.

Custom SQL is another advanced technique to help you level up your Liferay projects. It allows direct writing of SQL queries to the database in applications based on Liferay. 

Although Dynamic Queries are powerful tools for data filtering, there are situations when you need more complex operations or access to advanced database functions that aren’t provided by the DynamicQuery interface – such as joining tables, utilizing aggregate functions, or optimizing queries to enhance performance (which can be important for companies in need of more complex and/or scalable solutions)

In such cases, Custom SQL becomes a crucial solution. Custom SQL consists of SQL code snippets that can be directly inserted into your Liferay applications. This allows for the direct execution of complex database queries. However, it is important to be careful when using Custom SQL as it requires more complex management and may pose security risks, such as SQL injection attacks.

Examples of situations where it is worth using Custom SQL:

  • Performing more advanced table joins
  • Accessing aggregate functions (e.g., SUM, AVG) or database-specific functions
  • Optimizing queries to improve performance

How to use Custom SQL in Liferay?

One of the main challenges with Custom SQL is ensuring security. Avoid directly inserting user variables into queries to prevent SQL injection attacks. Liferay offers several mechanisms to execute Custom SQL queries safely – like the SQLQuery and SQLQueryFactory classes in Liferay API, for example, which automatically protect the system against SQL injection attacks through query parameterization.

To make use of this, you need to add the implementation of the following methods to the BookLocalServiceImpl class and then rebuild the module using Gradle:

public List<Book> books() {
String sql = "SELECT * FROM example_book WHERE title LIKE ? OR author = ?";
return executeQuery(sql);

private List<Book> executeQuery(String sql) {
Session session = null;
List<Book> details = null;
try {
    session = bookPersistence.openSession();
    SQLQuery query = session.createSQLQuery(sql);
    query.addEntity("Book", BookImpl.class);
    query.setString(0, "%Harry%");
    query.setString(1, "John Doe");
    details = (List<Book>) QueryUtil.list(query,
            bookPersistence.getDialect(), -1, -1);
} catch (UndeclaredThrowableException ex) {
    Throwable cause = ex.getCause();
} finally {
return details;

I’d like to stress once again – you need to manage Custom SQL carefully to avoid potential performance and security issues. I’d advise you to use Dynamic Queries when it is sufficient to achieve your goals, and resort to Custom SQL only when necessary.


While learning the basics of Liferay application development is relatively easy (check out the Liferay tutorial I wrote with Patryk Rutkowski if you want to refresh your memory), mastering the platform is something else. As you can see, the solution offers plenty of powerful tools and features you can use to create great solutions (CMS systems, customer portals, etc.), and make your apps more customizable, flexible, and powerful.

It’s also worth emphasizing the significance of some of these features – like Dynamic Queries and Custom SQL – for the business development of applications based on Liferay. Dynamic Queries enable the creation of more intuitive and flexible search functionalities, contributing to an improved user experience. Custom SQL provides developers with even greater control over database operations, which can be crucial in situations requiring optimized processes. Ultimately, understanding these tools allows for a more efficient adaptation of the application to specific business needs.

If you have any questions, contact me at Also, this article only scratches the surface of what’s possible. If you want more information about Liferay and its potential use scenarios, check out other articles on the Pretius blog and the video my colleague Patryk Rutkowski made, which is available on the Pretius YouTube channel:

  1. Liferay portlet for front-end development – An introduction to different options in 2024
  2. Liferay tutorial for CTOs: Business-friendly features and more advanced functionalities
  3. Liferay tutorial: Functional app in 30 minutes without programming skills

Interested in Liferay-based apps?

Pretius has experience with using the Liferay platform to create excellent enterprise-grade applications, for example, customer portals. Reach out to us at and tell us about your needs. We’ll see what we can do to help!