A modular Java ERP built on OSGi, served through a ZK web client, driven by a metadata layer and backed by PostgreSQL or Oracle. This is how it fits together — and where you hook your own code in without ever touching the core.
iDempiere is a server-side Java application. At the bottom sits a relational database — PostgreSQL or Oracle — that holds both business data and the metadata that describes the application itself. Above it, a persistence layer maps rows to Java objects. Above that, the business logic and the Application Dictionary drive behaviour. At the top, the ZK web client renders windows dynamically from that metadata, and a REST API exposes the same domain to external clients. The whole runtime is assembled from OSGi bundles rather than a single monolith, which is the single most important architectural fact about it.
The practical consequence for an architect is separation. Your extensions are their own bundles. They register themselves against well-defined extension points and are wired in at runtime. You never fork the core, which is why upgrading the platform underneath your code is a controlled operation rather than a merge nightmare.
Four layers do most of the architectural work — the OSGi runtime, the ZK client, the Application Dictionary and the persistence layer. Understand these and the rest of the platform follows.
Explore Customization & DevelopmentiDempiere runs on the Equinox OSGi framework. Every functional area — the base model, the web client, the REST API, each localisation — is packaged as an OSGi bundle with an explicit manifest declaring what it exports and imports. Services are published and consumed through OSGi Declarative Services, so a bundle can contribute behaviour to the running system simply by registering a component. Bundles can be added, started and stopped without rebuilding the application. The project builds with Maven and Tycho, producing a p2 repository from which a runtime is provisioned.
For you, this means a customization is a plugin: a small bundle you deploy alongside the standard ones. It has its own lifecycle, its own version, and a clean boundary. Two clients on the same core can run entirely different extension sets without interfering with each other.
The user interface is built on ZK, a Java server-side web framework. Crucially, screens are not hand-coded per window. The client reads the Application Dictionary and renders windows, tabs, fields, and their validation and display logic from metadata at runtime. Add a column to a table and expose it in the dictionary, and it appears in the window without a line of UI code. The client speaks to the browser over Ajax; there is a companion theme layer for branding, and the same metadata can drive alternative front ends.
The Application Dictionary is the metadata layer that defines the application to itself. Tables, columns, windows, tabs, fields, references, validation rules, workflows, reports, processes and menu structure are all data, held in tables such as AD_Table, AD_Column, AD_Window, AD_Field and AD_Process. Because the dictionary is data, a large share of customization is configuration rather than code, and it is captured in migration scripts that move cleanly between environments. This is the mechanism that lets customization survive version upgrades: your changes live in metadata and plugins, not in patched source.
iDempiere maps each dictionary table to a generated model class. The base class PO (Persistent Object) provides load, save, delete and change-tracking; generated X_ classes (for example X_C_Order) provide typed getters and setters per column; and hand-written M classes (for example MOrder, MInvoice, MInOut) add business logic on top. You look tables up through MTable and query with the fluent Query API rather than raw SQL. Transactions are managed through a Trx object so that a document and all its accounting post atomically. Working with the model this way keeps your code aligned with the framework's validation, security and event pipeline.
This is where an architect spends most of their attention. iDempiere exposes several well-defined ways to inject behaviour, each suited to a different job. Callouts react to a field change in the UI; model validators and event handlers react to database lifecycle events; processes and document actions add server-side operations; and the REST API exposes everything to the outside world. The table below summarises when to reach for each.
| Mechanism | Fires when | Typical use | Upgrade-safe |
|---|---|---|---|
| Callout | A field value changes in the ZK client | Derive or default one field from another, live validation | ✔ |
| Model Validator | Before/after save or delete of a record; on document events | Cross-record rules, enforce policy, trigger side effects | ✔ |
| Event Handler (OSGi) | Same lifecycle events, via the OSGi event bus | The modern, decoupled way to react to model events | ✔ |
| Process | User runs it, or a scheduler triggers it | Batch jobs, reports, custom server operations | ✔ |
| Document Action | A document is completed, reversed, voided, etc. | Custom posting, approvals, downstream document creation | ✔ |
| REST endpoint | An external client calls the API | Integrations, mobile, headless automation | ✔ |
A minimal OSGi model validator or event handler looks like the following. It registers for events on the C_Order table and runs a check before the record is saved. In a real plugin this class is wired up through an IModelValidator registration in your bundle activator or as a Declarative Services component.
package com.scmsoftwarelab.example;
import org.adempiere.base.event.AbstractEventHandler;
import org.adempiere.base.event.IEventTopics;
import org.compiere.model.MOrder;
import org.compiere.model.PO;
import org.osgi.service.event.Event;
/**
* Fires before a sales order is saved and blocks orders
* that exceed the customer's remaining credit.
*/
public class CreditCheckHandler extends AbstractEventHandler {
@Override
protected void initialize() {
// React to "before new" and "before change" on C_Order
registerTableEvent(IEventTopics.PO_BEFORE_NEW, MOrder.Table_Name);
registerTableEvent(IEventTopics.PO_BEFORE_CHANGE, MOrder.Table_Name);
}
@Override
protected void doHandleEvent(Event event) {
PO po = getPO(event);
if (po instanceof MOrder order && order.isSOTrx()) {
if (order.getGrandTotal().signum() > 0
&& exceedsCreditLimit(order)) {
throw new RuntimeException(
"Order blocked: customer over credit limit");
}
}
}
private boolean exceedsCreditLimit(MOrder order) {
// ... look up C_BPartner remaining credit and compare
return false;
}
}
The shape, not the logic. The point of the example is not the credit logic but the shape: a self-contained handler, registered against a table and topic, that the framework invokes. It ships in its own bundle. The core never knows it exists until it registers, and removing it is as simple as pulling the bundle.
The parts of the architecture that matter once real users and real transaction volumes arrive.
iDempiere supports PostgreSQL and Oracle as first-class databases, with an abstraction layer that keeps the model code database-neutral. The overwhelming majority of new deployments run on PostgreSQL — it is free, well understood and more than capable at this scale. Oracle remains supported for organisations with an existing estate or a licensing commitment. The same dictionary, the same model classes and the same migration scripts run against either.
iDempiere ships a REST API that exposes models, windows and processes over HTTP with token-based authentication. It respects the same roles, organisations and record-level security as the web client, so an external caller cannot see or do anything the equivalent user could not. This is the backbone of our iDempiere Mobile app: the Flutter client authenticates against the API, reads price lists and stock, and posts sales orders, goods receipts and approvals straight back into iDempiere — with an offline queue that reconciles safely on reconnect. Because it is the standard API, the same surface powers eCommerce, carrier and bank integrations.
A typical production topology runs the iDempiere application server on embedded Jetty behind a reverse proxy (nginx or Apache) that terminates TLS and can load-balance across application nodes. The database is PostgreSQL, usually configured for high availability with streaming replication and automated failover, and with a hot standby that doubles as a reporting replica. Everything can be containerised: we deploy iDempiere and PostgreSQL as Docker services, which makes environments reproducible and upgrades predictable. On-premise, AWS and Azure are all common; the architecture does not care where it runs.
iDempiere scales vertically well and horizontally with care. The first lever is connection pooling — the application pools database connections, and tuning that pool against PostgreSQL's own limits is the usual first optimisation. For very large transaction tables, native database partitioning keeps indexes and vacuum times sane. The most valuable pattern at month-end is offloading heavy reporting to the read replica, so BIRT and JasperReports runs, and our Dashboards & Analytics application, never contend with order entry on the primary. Beyond that, sensible indexing of custom columns, batching in long-running processes, and keeping model validators lean are what separate a system that stays fast from one that degrades as data grows.
Design principle we hold to. Every extension we ship is a bundle, every change to behaviour goes through a documented extension point, and nothing patches the core. That discipline is what makes a platform upgrade a weekend rather than a project.
Every layer is a well-supported technology your team can hire for and reason about.
Java on the Equinox OSGi framework, embedded Jetty, built with Maven and Tycho into a modular bundle set.
ZK server-side web client rendering windows from the Application Dictionary, plus a token-secured REST API.
PO / X_ model classes and MTable over PostgreSQL or Oracle, with transactions managed through Trx.
Callouts, model validators, OSGi event handlers, processes and document actions — all as deployable plugins.
Where to head next once the architecture picture is clear.
OSGi plugins, callouts, model validators and processes — built the upgrade-safe way.
Live module-wise KPI boards — every figure one click from the iDempiere document behind it.
Our Android & iOS field app, built on the standard iDempiere REST API.
Role-based KPI boards that read from a replica, off the production primary.
The metadata layer that defines windows, fields, rules and workflows as data.
Bring your integration map, your scale numbers and your must-not-break constraints. We will walk the topology and extension design with you before anyone writes a line of code.