This project was generated by Pisa from specs. The code is derived from the files in
spec/, so change the specs and runpisa generateagain rather than editing the generated code.The approach is Domain & Spec Driven Development (DSDD): a domain model (
.dml) is the center, and further specs (forms, use cases) refer to it by name. The generator is deterministic; a language model may only propose what the model allows. The specs, not the generated code, are the source of truth. What each spec file is and how to write it:spec/README.md.
Spring Boot 4.1.1 project (Java 25, Maven, H2 database), created with Pisa.
This file has everything to build the project from its specs, run it and test it. Contents: Tools · Build it · Run · Use it · Database · Tests · Change it · Layout · Problems
| Tool | Version | Check | Needed for |
|---|---|---|---|
| JDK | 25 (this project's java.version; a newer JDK works too) |
java -version |
everything |
| Maven | 3.9 or newer | mvn -v |
build, run, test |
| Pisa | the version that made this project (the end of this file names it) | pisa --version |
pisa generate, pisa replay: not needed to run or test what is already generated |
| A browser for the tests | Chromium, downloaded by Playwright on the first mvn test -Pui |
– | the browser tests |
| Pisa plugin (DML, ADL) for IntelliJ or VS Code, PlantUML | optional | – | editing the specs comfortably |
You can start from three points:
A. This folder as it is (code and specs are all here, e.g. cloned from git): there is nothing to install, Maven fetches what it needs. Go on with Run. The first build downloads dependencies, among them Playwright (about 200 MB).
B. Only the specs (spec/*.dml, .ui.md, .uc.md, .adl and .pisa/): let Pisa rebuild the project from its recipe in a
new folder. It re-runs the recorded steps and checks every generated file against the hash in .pisa/recipe.json:
pisa replay --from . --out ../adressen-spring-copyC. Nothing yet (an empty folder): create the project, write the model, generate.
pisa init --target spring-boot --out . --group ch.example --artifact adressen-spring --package ch.example.adressen --java 25
# add --adl spec/architecture.adl to take style, database and base package from an architecture description
# write the domain model in spec/<name>.dml (see spec/README.md); optional: forms spec/<entity>.ui.md, use cases spec/*.uc.md
# pisa propose-specs --out . derives forms and use cases from the model (no LLM)
pisa generate --file spec/<name>.dml --out . # checks the model first and writes nothing if it is wrongPer entity generate writes a JPA class, a repository, a REST controller and a page, plus the tests and a class diagram
(spec/domain-model.md). It rewrites them on every run, so never edit them by hand.
mvn spring-boot:run # starts on http://localhost:8080You should see Spring's banner and Started AdressenSpringApplication. Stop it with Ctrl+C. Another port: SERVER_PORT=8090 mvn spring-boot:run.
The architecture description has a docker block, so the project has a Dockerfile and a .dockerignore. Build the image and run it
(the application is then on http://localhost:8080):
docker build -t adressen-spring .
docker run --rm --name adressen-spring -p 8080:8080 adressen-springH2 keeps its data in memory: it is gone when the container stops, and so are the passwords users chose (they start again with the password of their name). The roles and users (security.yml) are part of the jar: change them, then build again.
Open http://localhost:8080/ in a browser, through the running server (not the files in src/main/resources/static: Alpine.js 3.17.4
comes from the WebJar in pom.xml, served by the application). Every entity of the models in spec/*.dml has a page to list, create,
edit and delete it, and is a REST resource:
GET /api/<plural> |
all rows (for choosing one in another entity's form) |
GET /api/<plural>?page=0&size=10 |
one page, sorted by id (size up to 100) |
GET /api/<plural>/{id} |
one row, 404 if there is none |
GET /api/<plural>/{id}/<collection>?page=0&size=10 |
one page of a collection another entity keeps (an author's books) |
POST /api/<plural> |
create a row (a generated id in the body is ignored) |
PUT /api/<plural>/{id} |
replace the row |
DELETE /api/<plural>/{id} |
delete the row and its composed parts |
An entity that lists fewer operations in @operations(...) in its .dml answers the missing ones with 405 (and an Allow header); without @operations all of them are there.
A click on a row of the list shows the entry read-only (all fields locked, no Save button); whoever may change it finds an Edit button there, so an entity that may only be read (@operations, or a role without update) can still be looked at in full.
A value that breaks a rule of the model is answered with 400 and the message per field, a duplicate in a unique column with 409:
{"error":"Some values are not valid.","fields":{"address.zip":"must match \"[0-9]{4}\""}}Grouping, order, labels and tables of a form come from the Salt mockup in its form description spec/<entity>.ui.md
(drawn by hand, or proposed with pisa propose-layout).
This application has a login (authentication session in spec/architecture.adl). Every page and every /api call needs a signed-in user; users, their roles
and what each role may do are in src/main/resources/security.yml (read when the application starts: change it and restart, no pisa generate).
| Roles and what they may do | the block roles { … } of the backend in spec/architecture.adl (a role without a block may do everything the entities allow, a rule such as Company: read, update limits it to those operations; without the block: ADMIN and USER, both may do everything). pisa init wrote security.yml from it; change the rights later there (restart) or in the .adl (delete security.yml, pisa init … --adl … --force) |
| Development users | one per role, with the password of their name: admin / admin for ADMIN, user / user for USER, any other role its name in lower case (security.yml, users:). Nothing secret is in the file: a user starts with their name as password unless password: gives a bcrypt hash |
| First sign-in | every user of security.yml (mustChangePassword, the default) has to choose their own password: after signing in a page asks for the new one twice (at least 8 characters, at most 72 bytes, not the user name). Until then every other request is answered with 403 password change required |
| Where the new password goes | as a bcrypt hash in the table usr_password of the application's database (name, hash); security.yml stays as it is. Delete a user's row there to give them the starting password again. With H2 in memory the table is empty after every start |
| Change it later | Change password in the menu (☰, top right) on every page |
| Not signed in | the API answers 401, a page leads to login.html; signed in without the right: 403; what the entity doesn't allow at all (@operations): 405 |
| Who am I | GET /api/me: name, roles, permissions per entity |
Passwords exist as bcrypt hashes only, nowhere as plain text. A user with a password of your own: pisa hash-password (asks twice without echo, or --stdin) prints the hash for
password: in security.yml (the password the user starts with). pisa generate and the application refuse a file with a plain text password, a role that isn't defined under roles, or an entity that isn't in the model.
admin/admin and user/user are public knowledge: use them to get started, never let the application run anywhere others can reach it before they chose their own. The password travels as plain text
over HTTP at the sign-in: put the application behind HTTPS before real use.
The application uses an in-memory H2 database: it lives in the running process and is empty at every start.
While the application runs you can look into it, with the settings already in src/main/resources/application.yml:
| JDBC URL | jdbc:h2:mem:adressen-spring |
| User | sa |
| Password | empty (H2's defaults, written out in application.yml under spring.datasource) |
- In the browser: http://localhost:8080/h2-console - enter the JDBC URL, user
sa, empty password. With the login you must be signed in with the role ADMIN first (other roles get 403); the console's own sign-in comes after that. - From IntelliJ (or DBeaver): Database tool window, +, Data Source, H2, URL
jdbc:h2:tcp://localhost:9092/mem:adressen-spring, usersa, empty password. An in-memory database can only be reached through the TCP server thatconfig/H2TcpServerConfigstarts inside the application; it listens on this machine only. Pick an H2 driver of the same major version as the application's (2.x).
The data is gone when the application stops. Console and TCP server are for development: set
spring.h2.console.enabled and h2.tcp.enabled to false in application.yml (or remove
H2TcpServerConfig) before the application runs anywhere others can reach it. The tests do not use these
settings (src/test/resources/application.properties): each test context gets its own database.
pisa generate writes the tests under src/test/java/ and rewrites them on every run (put your own tests into other files):
| Command | What it runs | Needs | You should see |
|---|---|---|---|
mvn test |
a repository test and a REST controller test per entity (create, read, update, delete, paging, required and unique values, every rule of the model broken once, the pages of a collection), a service test per aggregate root, ArchitectureTest (ArchUnit: the layering, naming and annotation rules), and a test that the application starts. Fast, no browser |
JDK 25, Maven | BUILD SUCCESS |
mvn test -Pui |
the same, plus the browser tests (Playwright for Java): fill in, save, edit and delete through each page, a server error marks the field, the menu, one test per use case of spec/*.uc.md (class UC1UiTest, …, tagged UC1) |
the same, and the first run downloads Chromium | BUILD SUCCESS |
Without -Pui the browser tests are compiled but skipped (tag ui). Single tests:
mvn -Dtest=BookControllerTest test # one class (replace BookControllerTest)
mvn -Dtest=UC1UiTest -Pui test # one use case in the browserWith the login, every test signs in first as tester, a test user who may do what the entities allow; the users of the tests are in src/test/resources/security-test.yml (written by pisa generate, only hashes), so
they don't depend on your security.yml. SecurityTest checks 401, 403, the sign-in form, /api/me and the first sign-in that has to choose a password (and that only a hash is kept); RolesTest signs in as as-<role> for every role of the .adl and checks, per entity and operation, what the role may do (not 403) and what not (403); a use case whose actor is a role runs in the browser as that role; PasswordChangeUiTest goes
through the first sign-in in the browser.
Maven's summary line is Tests run: N, Failures: 0, Errors: 0. The sources are formatted with Spotless (Palantir Java Format) on each build.
ArchitectureTest is written by pisa generate from the package names in .pisa/architecture.plan.json; put your own rules into another class.
Change the specs, not the code, then generate again and test:
# edit spec/<name>.dml (a new field also needs a line in the Salt mockup of spec/<entity>.ui.md: every field appears once)
pisa generate --file spec/<name>.dml --out .
mvn testA change of the architecture (spec/architecture.adl: runtime, components crud|layered, the database engine, versions) is applied once, by init;
until then generate stops and says so. Repeat the options you gave the first time (else Pisa uses its defaults):
pisa init --target spring-boot --out . --group ch.example --artifact adressen-spring --package ch.example.adressen --java 25 --adl spec/architecture.adl --force
pisa generate --file spec/<name>.dml --out .Files an earlier plan wrote and the new one no longer wants are deleted by init and generate if you have not changed them (Info: deleted, no longer used …); the ones you changed stay and are listed with the command to delete them (Delete them with: rm …).
They can break the build (the H2 TCP class after a switch to PostgreSQL, dto/, mapper/ and service/ after a switch from layered to crud): delete them.
The packages and the versions of everything the build pulls in are in .pisa/architecture.plan.json.
controller -> service -> repository -> model. The controller never touches the repository or an entity:
it takes and returns DTOs (dto, Lombok @Data, the same shape as the entity, so the pages need no change),
the service (service, @Transactional) maps with the MapStruct mappers (mapper) and talks to the
repository. ArchitectureTest checks these layers. The style is style in .pisa/architecture.plan.json.
| Path | What | Written by |
|---|---|---|
spec/ |
the specs: .dml, .ui.md, .uc.md, .adl, the class diagram |
you (the diagram by Pisa) |
doc/ |
the user guide and both tutorials (pisa play <name> plays one in a new folder) |
init |
.pisa/ |
recipe.json (the steps), architecture.plan.json (style, packages, database, versions), template overrides |
Pisa |
src/main/java/ch/example/adressen/ |
model, repository, controller, service, dto, mapper, validation per entity; config; the application class |
generate (config and the application class: init) |
src/main/resources/static/ |
the pages (*.html, app.js, app.css) |
generate |
src/main/resources/application.yml |
port, database, H2 console, app.title (the name on the home page: change it and restart) |
init |
src/main/resources/security.yml |
the roles and users (yours once written; no passwords in it); the passwords users chose are in the table usr_password of the database |
init, the application |
config/SecurityStore, SecurityConfig, EntityAccess, controller/SecurityController, static/login.html, static/change-password.html |
the login | init (EntityAccess: generate) |
src/test/java/… |
the tests (ui/ holds the browser tests) |
generate |
src/test/resources/application.properties |
what the tests change (own database per context) | init |
pom.xml |
the build; every version outside the Boot parent is a property at the top | init |
| What you see | What it means |
|---|---|
| javac complains about the release or target version | The JDK is older than 25. Install JDK 25 (or newer) and check java -version and JAVA_HOME. |
Port 8080 was already in use |
SERVER_PORT=8090 mvn spring-boot:run. |
| The first build is slow | Maven fetches the dependencies, Playwright included (about 200 MB); the first -Pui run also downloads Chromium. |
mvn test -Pui fails to start a browser |
Chromium could not be downloaded (network, proxy). Run it once with network access. |
| A browser page is empty or its buttons have no text | The page was opened as a file. Open http://localhost:8080/ through the running server. |
generate stops: The Salt mockup in spec/<entity>.ui.md can't be used: … missing "x" |
A field of the model is not in the form's mockup. Add a line Label | "x" to the mockup; generate writes nothing before that. |
403 password change required from the API |
The user has not chosen their own password yet: sign in and fill in the page change-password.html. |
generate stops: security.yml is not valid: … password must be a bcrypt hash |
A password in security.yml is plain text. Make a hash with pisa hash-password. The same message stops the application when it starts. |
| The application stops at start: the role "X" is not defined | A user in security.yml has a role that is not under roles. Fix the name (or add the role). |
| The H2 console shows nothing or is refused | With the login the console is for the role ADMIN only: sign in as an admin first. |
generate stops: spec/architecture.adl no longer matches the project |
The architecture description changed after init. Run the init … --force of Change it. |
The build fails with Package org.h2.tools or lombok / org.mapstruct is missing after a change of database or style |
Files of the earlier plan are still there. init and generate list them (Info: no longer used … - delete); delete them. |
not supported yet for composed |
Only a single oneToOne … composed is supported; lists of parts follow later. |
Every Pisa step below was run from the project root, in this order. The machine-readable recipe
(exact arguments, inputs and outputs with SHA-256) is .pisa/recipe.json; this section is generated
from it, so edit this file outside the markers only. Keep spec/ and .pisa/ in the project: they
hold the model and the template overrides the steps read. Only Pisa steps are recorded - manual
edits to generated files are not part of the recipe, keep them in version control.
pisa init --target spring-boot --out . --group ch.example --artifact adressen-spring --package ch.example.adressen --java 25 --adl spec/architecture.adlReads: spec/architecture.adl (sha256 2af6ec9ab183)
Writes 19 file(s): .dockerignore, .gitignore, .pisa/architecture.plan.json, Dockerfile, pom.xml, project.spec.json, spec/README.md, src/main/java/ch/example/adressen/AdressenSpringApplication.java, and 11 more (all with hashes in .pisa/recipe.json)
pisa generate --target spring-boot --file spec/adressen-spring.dml --out .Reads: .pisa/architecture.plan.json (sha256 172e71217224), spec/adressen-spring.dml (sha256 39b184800a06), spec/architecture.adl (sha256 2af6ec9ab183), spec/company.ui.md (sha256 b9017b2221ba), spec/person.ui.md (sha256 797db759cb7e), spec/use-cases.uc.md (sha256 1f6f172721f3)
Writes 50 file(s): spec/domain-model.md, spec/domain-model.puml, src/main/java/ch/example/adressen/config/EntityAccess.java, src/main/java/ch/example/adressen/controller/AppController.java, src/main/java/ch/example/adressen/controller/CompanyController.java, src/main/java/ch/example/adressen/controller/PageResponse.java, src/main/java/ch/example/adressen/controller/PersonController.java, src/main/java/ch/example/adressen/dto/AddressDto.java, and 42 more (all with hashes in .pisa/recipe.json)
pisa init --target spring-boot --out . --group ch.example --artifact adressen-spring --package ch.example.adressen --java 25 --adl spec/architecture.adl --forceReads: spec/architecture.adl (sha256 532a5d0987ca)
Writes 16 file(s): .github/workflows/ci.yml, .gitignore, .pisa/architecture.plan.json, pom.xml, project.spec.json, src/main/java/ch/example/adressen/AdressenSpringApplication.java, src/main/java/ch/example/adressen/config/H2TcpServerConfig.java, src/main/java/ch/example/adressen/config/SecurityConfig.java, and 8 more (all with hashes in .pisa/recipe.json)
pisa init --target spring-boot --out . --group ch.example --artifact adressen-spring --package ch.example.adressen --java 25 --adl spec/architecture.adl --forceReads: spec/architecture.adl (sha256 a66b9be8f5ee)
Writes 18 file(s): .github/workflows/cd.yml, .github/workflows/release.yml, .gitignore, .pisa/architecture.plan.json, pom.xml, project.spec.json, render.yml, src/main/java/ch/example/adressen/AdressenSpringApplication.java, and 10 more (all with hashes in .pisa/recipe.json)
Last step run with pisa 0.13.0 (development run, no build timestamp).