Updated recently
Last updated:
Quick Answer: In Liferay DXP, fragments are the page-building layer (HTML, CSS, JS plus a fragment.json descriptor), portlets are server-side Java widgets registered as OSGi components, and headless delivery is the /o/ REST API — with OData query options — for any consumer that is not a Liferay page. Picking the wrong layer is the most expensive early mistake in a Liferay project.
Liferay Fragments Portlets Headless Delivery: Three Layers, Three Jobs
Every Liferay implementation eventually has a meeting where someone asks “can we just build this as a portlet?” about a banner, or “can we just render it with a fragment?” about a screen that calls an internal service. The three layers are not interchangeable, and choosing well is most of the architecture. The platform-level version of that argument — when the argument is about WordPress or Sitecore instead — is covered in the Liferay vs WordPress vs Sitecore DXP comparison.
| Layer | What you write | Build it when | Ships as |
|---|---|---|---|
| Fragment | HTML, CSS, a little JS, plus fragment.json |
Content teams need to assemble and reassemble pages without a deploy | Fragment collection (zip upload or Liferay Workspace module) |
| Portlet (widget) | Java — an OSGi @Component extending MVCPortlet, with FreeMarker or JSP views |
You need server-side logic, a service call, permissions or stored preferences | OSGi bundle built from Liferay Workspace |
| Headless API | Little or nothing — you consume or expose /o/ REST resources |
The consumer is a mobile app, an SPA, an external portal or a scheduled sync | OAuth2 token plus a pinned API version |
A useful rule of thumb from real projects: if the requirement is “marketing wants to edit this and change the layout”, it is a fragment. If it is “this screen calls our policy system and applies permission rules”, it is a portlet. If it is “the mobile team needs this data”, it is headless delivery — and building them a portlet that returns JSON is how teams end up maintaining a private API nobody documented.
Fragments: The File Layout and fragment.json
A fragment is a folder. The descriptor sits beside the assets it references, and the whole folder travels as part of a fragment collection. In Liferay Workspace the layout looks like this:
fragment-collection/
└─ banner-with-cta/
├─ fragment.json
├─ configuration.json
├─ index.html
├─ main.css
└─ main.js
fragment.json
{
"name": "Banner With CTA",
"type": "section",
"cssPath": "main.css",
"htmlPath": "index.html",
"jsPath": "main.js",
"configurationPath": "configuration.json"
}
Four points that cause real bugs. First, type decides what the fragment can nest: a section fragment can contain columns and other fragments, a component fragment is a leaf you drop into an existing structure. Get that wrong and the page editor behaves strangely in ways that look like a platform bug.
Second, the paths are relative to the fragment folder, not to the site — a leading slash here breaks the fragment only in some environments, which is a fun afternoon.
Third, configuration.json is where the fragment becomes editable by non-developers. It declares field sets and fields — text inputs, selects, checkboxes, colour pickers, image or item selectors, URL fields — and those fields are what the page editor shows in the configuration panel. Each page instance stores its own values, which is exactly why the same fragment can look different on the home page and the campaign page. This is the fragment equivalent of widget configuration, and it is the piece teams skip when they are in a hurry.
Fourth, use index.html semantics that survive editing: put editable regions in markup the page editor can find, keep JS scoped to the fragment, and never hard-code a page URL into a fragment when a configuration field will do.
Portlets in 2026: Registration, Not portlet.xml
Older Liferay material still points you at WEB-INF/portlet.xml — the JSR-286 descriptor — alongside Liferay’s own liferay-portlet.xml and liferay-display.xml. That was the 6.x way of telling the portal a portlet existed, and you will meet it when you touch a legacy WAR portlet. It is not how you build one today. Since Liferay 7, portlets are OSGi components, and the registration is the configuration:
// Liferay 7+ / DXP — the @Component properties ARE the portlet descriptor
@Component(
property = {
"com.liferay.portlet.display-category=category.enterprise",
"com.liferay.portlet.header-portlet-css=/css/policy-lookup.css",
"com.liferay.portlet.instanceable=true",
"javax.portlet.display-name=Policy Lookup",
"javax.portlet.name=com_acme_policy_web_portlet_PolicyLookupPortlet",
"javax.portlet.resource-bundle=content.Language",
"javax.portlet.security-role-ref=guest,user,power-user"
},
service = Portlet.class
)
public class PolicyLookupPortlet extends MVCPortlet {
}
// Preferences are read at render time — never hard-code a customer value
PortletPreferences prefs = renderRequest.getPortletPreferences();
String defaultProduct = prefs.getValue("defaultProduct", "MOTOR");
boolean showRenewal = GetterUtil.getBoolean(prefs.getValue("showRenewal", "true"));
What each of those properties is really doing. javax.portlet.name is the portlet’s identity — it is what the widget selector, the page configuration and your preferences are keyed on, so renaming the class without renaming this property quietly orphans every stored preference. instanceable=true means several instances of the same widget can sit on one page, each holding its own preference set; that is the normal way to show the same widget with different product codes in two columns. The display category decides where the widget appears in the “Add” panel, and the resource bundle carries the localised display name.
Widget configuration — the gear icon in the page editor — works in two halves. You declare the configuration fields in the portlet’s configuration metadata (a configuration.xml under META-INF/resources/, or the annotation-based equivalent on recent releases; field types cover text, select, checkbox, date and item selectors), and the portal renders those fields and stores each value as a PortletPreference. On the read side you call prefs.getValue(...) with a default, exactly as above. The habit worth building: every environment-specific value — a service URL, a product code, a feature toggle — goes through a preference, because that is what lets UAT differ from production without a rebuild.
Headless Delivery: The /o/ Endpoints and OData
Headless delivery is the API layer for everything that is not a Liferay page. Services live under the portal’s /o/ root, are versioned, and — the part people discover late — accept OData query options, so you can shape the response instead of pulling a full content object and trimming it client-side.
# Approved blog postings for a site — only the fields the client renders
curl -s -G "https://portal.example.in/o/headless-delivery/v1.0/sites/20123/blog-postings"
-H "Authorization: Bearer $LIFERAY_TOKEN"
--data-urlencode "$select=title,publishDate,creator"
--data-urlencode "$filter=workflowStatusInfo/status eq 'approved' and publishDate ge 2026-01-01T00:00:00Z"
--data-urlencode "$orderby=publishDate desc"
--data-urlencode "$top=20"
--data-urlencode "$count=true"
Practical notes from building against these endpoints. -G with --data-urlencode matters: an OData filter contains spaces, quotes and slashes, and a naive quoted URL with a raw $filter will be mangled by the shell or the proxy. Auth for machine consumers is an OAuth2 application registered in the portal, whose bearer token you pass in the header; anonymous reads work only where the content’s permission model already allows guest access — the API does not widen permissions, it exposes them. And pin the version: consumers built against v1.0 should stay on v1.0 across a DXP upgrade rather than assume a minor-version response is byte-identical.
There is also the legacy JSON-WS API (/api/jsonws/... on most 7.x installs), which exposes object actions directly. It is genuinely useful for admin scripting — bulk-creating a layout, moving pages, priming caches — and it is a poor idea to expose to a mobile app, because it hands out portal-service-level operations rather than curated resources. Use it for scripting, headless delivery for products.
How the Three Fit Together in One Build
A workable shape for a real portal: the page is composed of fragments so the content team owns layout; one or two portlets carry the screens that need server-side calls and permission logic; and the mobile app consumes the same content through /o/ so nobody maintains two sources of truth. Content is entered once. That arrangement also gives you a migration path — as requirements soften, portlets become fragments plus a headless call, and the JS front end keeps talking to the same endpoints. It is also the breadth that the Liferay DXP developer salary India 2026 bands reward: developers who can work all three layers sit at the top of the mid band, and developers who only assemble page fragments sit at the bottom of it.
What Actually Happens at Work: The Fragment That Kept Reverting
A Hyderabad insurance portal shipped through CI: fragments, modules and configuration all built from Liferay Workspace and deployed as part of each release. On a Friday, a UX designer fixed hero-banner copy directly in the page editor — a two-minute change, sensible at the time, and it went live immediately.
Monday’s release deployed the fragment collection from source, and the banner reverted to the old copy. The designer re-applied the fix in the UI. The next release reverted it again. Two people spent most of a week making the same change, each convinced the other was breaking it, and the real problem was that the same fragment now had a source-of-truth copy in the repository and a second copy living with the site.
The fix was discipline rather than technology. Jar-deployed fragments are treated as read-only: copy, branch, edit in the workspace, deploy through CI. UI edits are reserved for page-level composition — moving blocks, changing instance configuration — not for editing fragment source. Where a hot fix genuinely cannot wait, it gets a ticket that lands in the next build, so the repository never falls behind production. The same argument applies to widget configuration: preferences are data, and data changes should be documented, not rediscovered by whoever opens the widget next.
Training Resources
Build all three layers on a real instance with SkilBrill Liferay Training — fragment collections and configuration, OSGi portlet development, headless endpoints with OData queries and identity integration, with placement support for Indian delivery teams.
FAQ
What is the difference between a Liferay fragment and a portlet?
A fragment is front-end markup (HTML, CSS, JS) that content teams place and configure on a page; a portlet is a server-side Java component registered as an OSGi component that can call services, enforce permissions and store preferences. Fragments are for layout, portlets for behaviour.
What are Liferay headless delivery APIs?
Versioned REST services under the portal’s /o/ root that expose content, users and other portal objects to external consumers, with OData query options such as $select, $filter and $top to shape responses. They authenticate with OAuth2 tokens and respect the same permission model as the UI.
Do I still need portlet.xml in Liferay 7.4 or DXP?
Not for new work. Modern portlets register through @Component properties on an OSGi bundle; portlet.xml is the legacy JSR-286 descriptor you will only meet in old WAR-based portlets during an upgrade.
How do you configure a Liferay widget on a page?
Configuration fields are declared in the portlet’s configuration metadata, rendered by the page editor’s configuration panel, and stored as PortletPreferences — read at render time with prefs.getValue(name, default). Each widget instance on a page keeps its own preference set.
Which should I learn first: fragments or headless delivery?
Fragments, because they are how most Liferay projects now build pages and they give you an early win you can show. Headless delivery comes next and is what separates a portal developer from someone who can only work inside the portal.
