Virtual Instances

Database Partitioning for Virtual Instances

Database partitioning gives each Liferay DXP virtual instance its own database schema, replacing the default single-schema layout where every instance shares one schema and rows are separated by a companyId column. The result: stronger tenant isolation, faster queries against large tables, and per-instance control over backups and data residency. (Liferay DXP uses “virtual instance” and “company” interchangeably; the underlying database identifier is companyId.)

Important

Database partitioning applies to Liferay Self-Hosted and Liferay PaaS deployments. On Liferay SaaS, virtual instance setup is managed by Liferay.

Partitioning offers four main benefits:

  • Query performance: Large tables are split per company, so the database doesn’t scan or filter unnecessary records during queries, index rebuilds, or other maintenance operations.

  • Stronger data isolation: Each instance’s data lives in its own schema. Even code paths that bypass permission checks (the classic source of cross-company Insecure Direct Object Reference (IDOR) exposures) can only reach the partition belonging to the current request.

  • Data residency control: Per-schema storage gives you explicit control over where each tenant’s data sits, which simplifies compliance with GDPR and other current or future data-residency laws.

  • Simpler maintenance: Backups, migrations, and exports/imports can be scoped to a single virtual instance, which is faster and lower-risk than operating against a shared schema that contains every tenant’s data.

Prerequisites

Before enabling partitioning, confirm that the installation meets these requirements:

  • The database is MySQL or PostgreSQL.

  • The configured database user has Data Definition Language (DDL) privileges on every schema Liferay DXP creates. Schema names follow the pattern [prefix][companyId] (default prefix lpartition_), and Liferay DXP creates a new schema each time you add a virtual instance.

Warning

Database partitioning is irreversible. There is no supported path to revert a partitioned installation to a single-schema layout. Take a full database backup before enabling, and plan the rollout accordingly. Partitioning is supported only on MySQL and PostgreSQL.

Enabling Database Partitioning

To enable database partitioning on a new Liferay DXP installation, add this property to portal-ext.properties before starting the server for the first time:

database.partition.enabled=true

After the server starts with partitioning enabled, the default company’s schema continues to use the name from the JDBC/JNDI connection (for example, lportal). Every new virtual instance creates a dedicated schema following the configured prefix pattern. With the default prefix and a company ID of 20001, the schema is lpartition_20001.

If the installation already has multiple virtual instances, the property alone isn’t enough. The existing data lives in a single schema and must first be split into per-company schemas. Use the virtual instance migration tool to extract each existing instance and reinsert it as a partition before turning the property on.

Customizing the Schema Prefix

Each new schema follows the pattern [prefix][companyId]. The default prefix is lpartition_. To use a different prefix, set this property in portal-ext.properties:

database.partition.schema.name.prefix=myprefix_

The prefix has a maximum length of 11 characters. Liferay DXP resolves a partition schema by concatenating the current prefix value with the company ID, so if you change the prefix after partition schemas already exist, rename those schemas to use the new prefix before restarting the server.

Disabling Multi-Threading

By default, Liferay DXP parallelizes operations that iterate over companies (upgrades, verify processes, and index regeneration), so independent partitions are processed concurrently. Disable parallel iteration only if the parallel workload is causing problems on your database or application server:

database.partition.thread.pool.enabled=false

Configuration File Limitations

OSGi configuration files (.config files in Liferay Home) operate at four scopes: SYSTEM, COMPANY, GROUP, and PORTLET_INSTANCE. With partitioning enabled, PORTLET_INSTANCE scoped configuration files are rejected outright. GROUP scoped configuration files are accepted only when they also identify the target company (via a companyId property or a groupKey that resolves to one). Configure portlet-instance-scoped settings through the UI or the Configuration headless API instead.

Reserve the Default Company for Maintenance

The default company (the one created when Liferay DXP was first installed) hosts the cross-tenant control tables and is the entry point for administering other instances. Reserve it for cross-tenant administration (creating new instances, running upgrades, and performing maintenance) and avoid hosting customer content there.

How Partitioning Works

Liferay DXP routes every SQL operation to the schema that matches the current companyId. From your perspective, virtual instances continue to share an application server, OSGi container, and JVM, but each instance’s data lives in its own schema.

Table Categories

Tables fall into two categories:

  • Partitioned tables contain a companyId column and exist physically in every schema. Most Liferay DXP tables fit this category.

  • Control tables are managed from the default schema. Non-default schemas can read and update them through views, but the data lives in one place.

Control tables split further into two kinds:

  • Pure control tables exist only in the default schema. The list includes Company, VirtualHost, Release_, ServiceComponent, and the QUARTZ_ tables (Liferay DXP’s scheduled-job storage). Other schemas reach these through updatable views.

  • Partitioned control tables don’t have a companyId column but still exist in every partition. The list is ClassName_, Counter, and ResourceAction. IDs in these tables can differ between partitions: a class name added to one partition is added to the others independently and may receive a different numeric ID in each.

Quartz Job Behavior

Liferay DXP uses Quartz (an open-source job scheduler) to run scheduled jobs. When partitioning is enabled, Liferay DXP appends a company suffix to persisted job and trigger names: [job name]@[companyId]. For example, a job ending in @20001 belongs to the company whose companyId is 20001. When inspecting QUARTZ_ tables or debugging scheduler logs, the suffix tells you which instance a job came from.

Upgrade and Verify Behavior

When partitioning is enabled, Liferay DXP runs upgrade processes and verify processes (built-in data-integrity checks that run after upgrades) once per company instead of once globally. Each schema is brought to the new version independently. Expect upgrades to take proportionally longer as the number of virtual instances grows.

Message Bus Behavior

Scheduled jobs and message bus (Liferay DXP’s internal pub/sub system) listeners that aren’t tied to a specific companyId run once per active company by default. To exempt a specific destination or scheduler job, configure the entries through Control PanelConfigurationSystem SettingsDatabase Partition Configuration.

Managing Virtual Instances with the Headless API

When partitioning is enabled, you can provision and manage virtual instances programmatically through the headless-portal-instances REST API. Creating an instance creates its schema; deleting one removes the schema along with the company record.

To browse the full OpenAPI specification and run requests interactively, open the API Explorer at http://[host]:[port]/o/api.

The available portal-instance operations are:

OperationMethod and Path
CreatePOST /o/headless-portal-instances/v1.0/portal-instances
ListGET /o/headless-portal-instances/v1.0/portal-instances
ReadGET /o/headless-portal-instances/v1.0/portal-instances/{portalInstanceId}
UpdatePATCH /o/headless-portal-instances/v1.0/portal-instances/{portalInstanceId}
ActivatePUT /o/headless-portal-instances/v1.0/portal-instances/{portalInstanceId}/activate
DeactivatePUT /o/headless-portal-instances/v1.0/portal-instances/{portalInstanceId}/deactivate
DeleteDELETE /o/headless-portal-instances/v1.0/portal-instances/{portalInstanceId}

For example, this curl call creates a new instance and its associated schema. Replace test@liferay.com:test with administrative credentials; do not use the default credentials in production.

curl -X POST 'http://localhost:8080/o/headless-portal-instances/v1.0/portal-instances' \
  -H 'Content-Type: application/json' \
  -u 'test@liferay.com:test' \
  -d '{
        "domain": "liferay.com",
        "virtualHost": "localhost2",
        "portalInstanceId": "pizzametro"
      }'