Using JHipster JDL as a Single Source of Truth for Domain Models
Treat JHipster's JDL as the canonical domain contract. This blog shows a concrete order-domain example, explains how protected regions preserve custom logic during re-imports, and outlines a safe adoption path for existing projects.
05 Jul 2025, 13:43 UTC

The drift between model and code
In a typical JHipster project, the domain model starts in a whiteboard session, moves to a JDL file for initial scaffolding, and then diverges as developers add custom validation, business rules, and query methods directly into the generated Java classes. Six months later, a new relationship is needed. Someone edits the JDL and runs jhipster import-jdl — only to watch the generator overwrite hand-written service logic because the team never agreed on where custom code belongs.
This drift is the central problem JDL was designed to solve, but only if you treat the .jdl file as the canonical definition and adopt a strict convention for protecting custom work.
JDL as the contract
JHipster Domain Language (JDL) is a declarative DSL that describes entities, fields, enumerations, relationships, and deployment targets. When you run jhipster import-jdl app.jdl in a project root (requires Node.js and the generator installed globally via npm install -g generator-jhipster), the generator produces JPA entities, Spring Data repositories, REST controllers, DTOs, MapStruct mappers, and frontend components for Angular, React, or Vue.
Because the same JDL drives both backend and frontend, the contract stays consistent across the stack. Version-controlling the JDL file gives you a diffable history of domain changes — something you lose when developers edit generated classes directly.
Worked example: a minimal order domain
Consider a small e-commerce core with products, orders, and line items. The following JDL captures the entities, a many-to-one relationship from OrderItem to Product, a one-to-many from Order to OrderItem, and a deployment hint for Docker Compose.
// app.jdl
entity Product {
name String required unique
price BigDecimal required min(0)
stock Integer required min(0)
}
enum OrderStatus {
PLACED, PAID, SHIPPED, CANCELLED
}
entity Order {
placedAt Instant required
status OrderStatus required
}
entity OrderItem {
quantity Integer required min(1)
unitPrice BigDecimal required min(0)
}
relationship OneToMany {
Order{orderItems} to OrderItem{order}
}
relationship ManyToOne {
OrderItem{product} to Product
}
service * with serviceClass
paginate * with pagination
deployment {
dockerContainer
}
Run the import in a fresh JHipster monolith project:
# In project root (where .yo-rc.json lives)
jhipster import-jdl app.jdl
The generator creates Product, Order, OrderItem under src/main/java/.../domain, corresponding repositories in .../repository, service classes in .../service, and REST endpoints in .../web/rest. Frontend components appear under the entities folder of the chosen client framework.
Verification step: Start the application (./mvnw in the project root, or the generated npm scripts for the frontend) and request GET /api/orders. You should get a paginated JSON response. Create an order with items through the generated UI or API and confirm the relationship mapping persists. Run the generated tests (./mvnw test) to confirm the scaffolding compiles cleanly.
Where the model stops and code begins
JDL covers CRUD, validation annotations (required, min, unique), pagination, and DTO mapping. It does not generate:
- Custom JPQL/SQL queries (e.g., "total revenue per product last month")
- Transaction boundary decisions beyond the defaults on generated service methods
- Domain events or saga orchestration
- Complex business rules that span multiple aggregates
These belong in the service layer. The generator marks extension points with comments such as // jhipster-needle-.... Place custom code inside those regions so a later re-import preserves it.
Risk: If you edit outside the needle regions, the next import can silently overwrite your changes. The --force flag bypasses interactive conflict prompts — reserve it for CI pipelines where you have already reviewed the JDL diff, and always commit before re-importing so you can inspect the result with git diff.
Upgrading JHipster versions
JDL syntax and the generated code structure shift between major versions. A JDL written for JHipster 7 (Spring Boot 2, Java EE) may need adjustments for JHipster 8 (Spring Boot 3, Jakarta EE namespaces). The official reference at jhipster.tech/jdl/ documents the current syntax — check it against your generator version before writing new JDL. Before upgrading, import your JDL into a throwaway project on the new version and compare the generated output with your existing codebase.
Adopting JDL in an existing project
- Extract the current domain model into a JDL file, by hand or with the visual editor at JDL Studio (
start.jhipster.tech/jdl-studio), which validates syntax and renders the entity diagram. - Create a feature branch and commit everything. Run
jhipster import-jdl your-model.jdland let the generator show conflicts. - For each conflict, move custom logic into the nearest needle region, then accept the generator's version.
- Run the full test suite and fix any compilation errors caused by moved code.
- Merge the branch. From now on, all domain changes (new fields, relationships, validation rules) go through the JDL first.
After the first disciplined cycle, the JDL becomes the living documentation your onboarding developers actually read.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.