One File to Generate Them All: JHipster JDL in Practice
JHipster's JDL lets you declare entities, relationships, DTOs, and pagination in one file and generate a consistent Spring Boot + Angular/React/Vue stack. A worked User–Authority example shows the output, and the regeneration discipline you'll need.
06 Feb 2026, 01:41 UTC

The Synchronization Tax
Every full-stack team knows the drill: you design a domain model, then manually create JPA entities, repositories, DTOs, MapStruct mappers, Liquibase changelogs, TypeScript interfaces, Angular services, and HTML templates. A single field addition touches six files across two repositories. A relationship change cascades into join tables, bidirectional navigation, and multi-select components. The synchronization tax isn't just tedious—it's where bugs hide.
JHipster's JDL (JHipster Domain Language) attacks this by making the domain model the single source of truth. One .jdl file declares entities, fields, validation, relationships, and architectural options (DTO pattern, service layer, pagination). Running jhipster import-jdl generates the entire stack—Spring Boot backend and React/Vue/Angular frontend—in one pass. This blog walks through a concrete example, shows where the approach shines, and highlights the discipline it demands.
What JDL Actually Declares
A JDL file is a concise DSL. Entities look like this:
entity User {
login String required unique
email String required unique
firstName String
lastName String
activated Boolean
}
entity Authority {
name String required unique
}Relationships use explicit cardinality and an ownerSide to control foreign keys:
relationship ManyToMany {
User{authority} to Authority{user}
}Options apply globally or per-entity:
dto * with mapstruct
service * with serviceClass
paginate * with paginationThese three lines give you MapStruct mappers for every DTO, a service layer with interface + implementation, and Spring Data Pageable endpoints with frontend pagination components—no manual wiring.
Worked Example: User–Authority ManyToMany
Create a minimal JHipster project (monolith, Angular, Maven):
jhipster --blueprints=none --skip-client=false --skip-server=falseSave the following as model.jdl:
entity User {
login String required unique minlength(2) maxlength(50)
email String required unique maxlength(254)
firstName String maxlength(50)
lastName String maxlength(50)
activated Boolean
}
entity Authority {
name String required unique maxlength(50)
}
relationship ManyToMany {
User{authority} to Authority{user}
}
dto * with mapstruct
service * with serviceClass
paginate * with paginationRun the import:
jhipster import-jdl model.jdlWhat gets generated (backend):
User.javaandAuthority.javawith JPA annotations,@ManyToMany,@JoinTable(name = "jhi_user_authority"), and bidirectional navigation.UserRepository.javaextendingJpaRepositorywith pagination methods.UserService.java(interface) andUserServiceImpl.javawith transactional CRUD.UserMapper.java(MapStruct interface) converting betweenUserandUserDTO.- Liquibase changelog creating
jhi_user,jhi_authority, and the join tablejhi_user_authoritywith foreign keys.
What gets generated (frontend, Angular):
user.model.tsandauthority.model.tsTypeScript interfaces mirroring the JPA entities.user.service.tswithquery(req?: any): Observableaccepting pagination params.user-management.component.htmlwith a multi-select for authorities bound touser.authorities.- Routing, resolvers, and pagination directives wired automatically.
Add a field to User in the JDL (e.g., phoneNumber String pattern("^\+?[0-9\s-]{10,}$")), re-run import-jdl, and the field appears in the entity, DTO, mapper, changelog, TypeScript model, and form—while your custom code inside // jhipster-needle- protected regions stays untouched.
The Trade-off: Regeneration Discipline
JDL's power is also its risk. Re-running import-jdl after edits produces large diffs. The generator preserves code only inside marked protected regions (// jhipster-needle-entity-add-field, // jhipster-needle-service-add-method, etc.). If you move or delete those markers, your manual additions vanish.
Three practical guards:
- Commit before every import. A clean baseline lets you diff generated changes vs. your edits.
- Review Liquibase changelogs carefully. Column type changes (e.g.,
String→Long) generate drop/create columns—data loss in production. Write manual migration scripts for destructive changes. - Keep custom MapStruct logic outside generated mappers. Nested DTO mapping, conditional conversions, or custom value transformations belong in hand-written mapper classes that delegate to generated ones, not inside the generated interface.
Also note: JDL doesn't express complex business logic, custom queries, or security rules. Those live in your service/repository implementations. Switching frontend frameworks (Angular → React) later means regenerating the entire client from JDL—effectively a rewrite.
Closing: Adopt Incrementally
Start a new JHipster project, write a JDL for one bounded context (e.g., user management), run import-jdl, and inspect the output. Verify the join table, MapStruct mapper, and Angular multi-select exist. Then add a field, re-run, and confirm protected regions hold. Once the workflow feels solid, expand the JDL to cover more domains. The JDL Studio at start.jhipster.tech/jdl-studio lets you design and visualize (Mermaid ERD) before generating—useful for team reviews. JDL won't eliminate all boilerplate, but it makes the domain model the contract that keeps backend and frontend honest.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.