Spring Boot (`spring-boot`)
Generates a Spring Boot service in Java or Kotlin. It builds with Gradle (Kotlin DSL, the default) or
Maven, always through the wrapper. The app project holds your code. Every API contract and every database is a
module of its own. The platform module has error handling, ports, middleware and security.
The generated code uses Spring MVC, Bean Validation, JPA (Hibernate) or JdbcClient, HikariCP and Flyway. It does not
use Spring Security: tokens are verified in the platform with JDK cryptography. This gives Java and Kotlin, Gradle and
Maven, and Spring Boot 3.5 and 4.x the same code, with no extra dependency. All versions are pinned in one place:
gradle/libs.versions.toml with Gradle, or the root pom.xml (on spring-boot-starter-parent) with Maven.
Quick start
gnx create orders --type spring-boot --dir services/orders --language kotlin --build-tool maven --contract services/orders/api/orders.openapi.yaml --port 8080--type spring-boot-kotlin is a shortcut for --language kotlin. The component spec:
# services/orders/.genexy/component.yaml
type: spring-boot
language: kotlin
buildTool: maven
groupId: com.acme
packageName: com.acme.orders
javaVersion: '21'
springBootVersion: 4.1.1
exposes:
- name: api
port: 8080
contracts:
- $ref: api/orders.openapi.yaml
modules:
- name: main-db
type: postgresql-adapter
driver: jdbc
contract: { $ref: db/orders.yaml }Component spec
| Option | gnx create flag | Default | Notes |
|---|---|---|---|
language | --language | java | java or kotlin |
buildTool | --build-tool | gradle | gradle or maven. Both produce the same modules, sources and tests |
mavenVersion | 3.9.11 | The Maven the wrapper downloads (buildTool: maven) | |
groupId | --group | com.example | |
artifactId | the component id | ||
packageName | --package | <groupId>.<artifactId> | Base package of the generated code |
javaVersion | --java | 21 | 17 or newer |
springBootVersion | --boot | 4.1.1 | 4.x, or 3.4 and newer |
name, description, version | the id, –, 1.0.0 | ||
basePath | from the contract | Path prefix of every contract; otherwise taken from servers[0].url | |
dependencies, testDependencies | Extra libraries of app/ as group:artifact[:version] | ||
exposes | --port, --contract | one port, 8080 | Ports: see Ports, Middleware & Security |
exposes[].middleware, .validation, .security | Middleware, validation, authentication | ||
kind, parts | --parent | service | A part of a service in its own folder, see Parts |
modules | Databases and security modules, see below |
Modules
| Type | Contract | What you get |
|---|---|---|
postgresql-adapter | sqlschema | modules/db-<name>: rows, repositories, queries, transactions, Flyway migrations |
mysql-adapter (also mariadb-adapter) | sqlschema | The same for MySQL and MariaDB |
security | JWT / OIDC bearer tokens for the ports that use it. See Authentication |
Database modules take driver: jpa (default) or driver: jdbc. With several databases, mark one primary: true.
REST clients, Redis and messaging modules are not generated for Spring Boot yet. The generator skips them with a
warning.
What is generated
orders/
├─ .genexy/component.yaml the component spec (and generated.json)
├─ settings.gradle.kts, build.gradle.kts Gradle: generated
├─ gradlew, gradlew.bat, gradle/ Gradle: wrapper and libs.versions.toml
├─ build-logic/ Gradle: shared conventions
├─ pom.xml, mvnw, mvnw.cmd, .mvn/ Maven (instead of the Gradle files): root pom and wrapper
├─ compose.yaml generated with a database
├─ Dockerfile, README.md, gradle.properties yours (created once)
├─ app/
│ ├─ build.gradle.kts | pom.xml generated
│ └─ src/
│ ├─ main/kotlin/com/acme/orders/
│ │ ├─ OrdersApplication.kt generated
│ │ ├─ ports/ApiPort.kt generated: one configuration per port
│ │ └─ orders/OrdersServiceImpl.kt stub, one per OpenAPI tag
│ ├─ main/resources/
│ │ ├─ application.yml generated: the values of the spec
│ │ └─ config/application.yml yours: your settings
│ └─ test/kotlin/com/acme/orders/
│ ├─ ArchitectureTests.kt, PortsTests.kt, TestDatabases.kt generated
│ └─ OrdersApplicationTests.kt stub
└─ modules/ generated, never edit
├─ api-orders/ <Tag>Service interfaces, model/, internal/ controllers
├─ db-main-db/ repositories, queries, transactions, db/migration/main-db/V<n>__*.sql
└─ platform/ ApiException, UploadedFile, Principal, CurrentPrincipal, internal/ runtimeWith Java the sources are under src/main/java and end in .java; models are records in Java and data classes in
Kotlin.
The public packages for your code:
| Package | Types |
|---|---|
<package>.api.<contract> | <Tag>Service and model.* of each API contract |
<package>.db.<database> | <Row>Repository, <Db>Queries, <Db>Transactions, @<Db>Transactional, model.* rows |
<package>.platform | ApiException, UploadedFile, Principal, CurrentPrincipal, DataConflictException, RowNotFoundException, PortRegistry |
Code in app/ must not use the .internal packages of the modules, Spring Web or JPA directly. The generated ArchUnit
test ArchitectureTests fails if it does.
Writing the business logic
There is one <Tag>ServiceImpl per OpenAPI tag, a @Service that implements the generated interface. The
interface has default methods that answer 501, so the service builds and runs right away, and operations added to the
contract later answer 501 until you implement them.
package com.acme.orders.orders
import com.acme.orders.api.orders.OrdersService
import com.acme.orders.api.orders.model.Order
import com.acme.orders.db.maindb.MainDbTransactions
import com.acme.orders.db.maindb.OrderRepository
import com.acme.orders.platform.ApiException
import com.acme.orders.platform.CurrentPrincipal
import org.springframework.stereotype.Service
@Service
class OrdersServiceImpl(
private val orders: OrderRepository,
private val tx: MainDbTransactions,
) : OrdersService {
override fun getOrder(orderId: Long): Order {
val row = orders.findById(orderId).orElseThrow { ApiException.notFound("order $orderId not found") }
val caller = CurrentPrincipal.get()
if (caller != null && !caller.hasRole("admin") && row.owner != caller.subject) {
throw ApiException.forbidden("not your order")
}
return row.toApi()
}
}The same in Java:
@Service
public class OrdersServiceImpl implements OrdersService {
private final OrderRepository orders;
public OrdersServiceImpl(OrderRepository orders) {
this.orders = orders;
}
@Override
public Order getOrder(Long orderId) {
var row = orders.findById(orderId).orElseThrow(() -> ApiException.notFound("order " + orderId + " not found"));
CurrentPrincipal.get()
.filter(caller -> !caller.hasRole("admin") && !row.owner().equals(caller.subject()))
.ifPresent(caller -> { throw ApiException.forbidden("not your order"); });
return toApi(row);
}
}- Dependencies: constructor injection of the repositories, transactions and your own beans. Your own packages
and
@Configurationclasses go next to the stubs inapp/. - Parameters: path, query, header and cookie parameters are method parameters with their types; the body is
the last one. Uploaded files are
UploadedFile. - Errors: throw
ApiException(badRequest,notFound,conflict,forbidden,of(status, message)…), orApiException.withBody(status, message, body)to send the error model of the contract. Other exceptions become 500. See Errors on the wire. - Types:
dateisLocalDate,date-timeOffsetDateTime,uuidUUID,durationDuration,decimalBigDecimal; optional properties are nullable; defaults of the contract are applied.
Databases
Interface (<package>.db.<name>) | Methods |
|---|---|
<Row>Repository | findById, existsById, findAll, findAll(offset, limit), count, findBy<Column>, insert, update, deleteById |
<Db>Queries | One method per named query of the contract |
<Db>Transactions | inTransaction { … }, readOnly { … }; or annotate a method with @<Db>Transactional |
JPA and JDBC have the same interfaces; only the implementation in .internal differs, so you can switch the driver
without touching your code. Every database has its own HikariCP data source, transaction manager and Flyway.
A constraint violation throws DataConflictException (409 with the constraint name), a missing row
RowNotFoundException (404).
Migrations: the first generation writes V1__init.sql, every later change of the schema the next V<n>__*.sql
in the dialect of the database, from the difference to schema.snapshot.json. They are never rewritten; dropping
data gives a warning. Flyway applies them at startup. Your own migrations go into
app/src/main/resources/db/migration/<name>/.
Settings: genexy.databases.<name>.url, username, password, or <NAME>_URL, <NAME>_USERNAME,
<NAME>_PASSWORD. The defaults point to the database of the generated compose.yaml.
Parts
A part (kind: part) moves libraries of a service into their own folder, with their own Gradle or Maven build, so a
team can keep, say, the payments adapter in its own repository and build it alone. A Spring Boot part has the platform
and the database modules, no app/ and no ports.
# services/orders/adapters/payments/.genexy/component.yaml
name: payments
type: spring-boot
kind: part
packageName: com.acme.payments
modules:
- name: payments-db
type: postgresql-adapter
contract: { $ref: db/payments.yaml }The service lists it in parts: [{ $ref: adapters/payments }]. With Gradle its settings include the part’s build
(includeBuild) and app/ depends on it; with Maven the part is a module of the service’s pom. The application
imports the part’s database modules, and application.yml, compose.yaml and the test databases contain its
databases. The service reads only the part’s component.yaml, so the two can be generated in any order. Use the same
buildTool and its own packageName for the part.
With one port, the application is a plain Spring Boot application. With several, the root context holds your services and the databases, and every port is a child context with its own Tomcat, controllers, error handling, middleware and security. CORS, limits and tokens of one port never affect another.
- The middleware is the servlet filter
PortPolicyFilter: request id, access log, security headers, CORS, body limit and rate limit. Compression is Tomcat’s (server.compression.*). - Authentication is the MVC interceptor
PortSecurityInterceptor. Every controller method carries an@OperationSecurityannotation generated from the contract. - All options: Ports, Middleware & Security.
Two settings work differently in Spring Boot. middleware.requestTimeout is not enforced, because synchronous Spring
MVC controllers cannot be interrupted, so the generator only warns. validation.unknownProperties: reject applies to
the whole component, because Spring Boot has one JSON mapper for all ports. The generator warns when only some ports
ask for it. The body limit is checked with Content-Length, so chunked bodies are not limited yet.
Configuration
app/src/main/resources/application.yml(generated: the values of the spec)app/src/main/resources/config/application.yml(yours)- profiles, environment variables (
GENEXY_PORTS_API_PORT), command line (--genexy.ports.api.port=9090)
Read your own settings with @ConfigurationProperties or @Value in app/. Placeholders like
${AUTH_ISSUER:https://sso.example.com/realms/shop} are resolved by Spring.
Tests
| Test | Owner | Checks |
|---|---|---|
ArchitectureTests | generated | app/ uses only the public types of the modules |
PortsTests | generated | With several ports: every operation answers only on its own port |
TestDatabases | generated | Starts the databases with Testcontainers for the Spring tests |
<App>ApplicationTests | yours | contextLoads |
Start the application on free ports in your own tests and ask PortRegistry for the addresses:
ConfigurableApplicationContext context = OrdersApplication.run("--genexy.ports.api.port=0");
String base = context.getBean(PortRegistry.class).url("api");The database tests need Docker for Testcontainers; without Docker they are skipped and the build still passes. With
GENEXY_TEST_DATABASES=external they use a running database instead, for example from the generated compose.yaml.
Build and run
Needs a JDK of the chosen Java version. Gradle or Maven comes with the wrapper. In the component folder:
| Task | Gradle | Maven |
|---|---|---|
| Build + test | ./gradlew build | ./mvnw verify |
| Run | ./gradlew :app:bootRun | ./mvnw -pl app -am spring-boot:run |
| Jar | app/build/libs/*.jar | app/target/*.jar |
On Windows use gradlew.bat and mvnw.cmd. The generator owns every build.gradle.kts and pom.xml: add libraries
with dependencies in the component spec, not in the build files.
Not generated yet
- REST and WebSocket clients, MariaDB as its own dialect, PostGIS, Oracle, MongoDB, Redis, Kafka and NATS modules
oneOfand discriminators, response headers and several success statuses,readOnly/writeOnly, streaming/healthz, Actuator as a module,businessErrorsapiKeyandbasicsecurity schemes, Keycloak incompose.yaml- A generated Dockerfile (
docker.enabled) and Kubernetes manifests; today the Dockerfile is a starter you own