Spring Boot example: CRUD, controller composition, and advanced contracts
The canonical example-spring-boot module demonstrates a generated Spring Boot application with a small, manually maintained application shell and runtime tests. The CMN models define the domain, composed repository statements, REST API including a focused request-header endpoint, composed resource fragments, backend components and application. JoinedWorkz generates the persistence, mapping, data-access and web layers.
Use this page as the advanced example and generated-contract reference. It catalogues the canonical module's models, generated layers, configuration and executable contract tests. For a guided modeling sequence, begin with Model a SpringBoot REST API step by step. For one focused end-to-end CRUD walkthrough, use Build a CRUD API with the Spring Boot facility.
The complete source is the example-spring-boot module on release/1.3.81.
1. Generate, build and run
Requirements:
- Java 21
- Maven 3.9 or newer
Clone and verify the release example:
git clone --branch release/1.3.81 --single-branch \
https://gitlab.com/joinedworkz/joinedworkz-examples.git
cd joinedworkz-examples/example-spring-boot
mvn clean verifyThe build removes previous generator output, generates the application and compiles the generated and manual Java sources. It runs nine runtime tests against Spring Boot and the in-memory H2 database plus five focused generated- source contract tests. CustomerCrudRuntimeTest starts Spring Boot on a random port and exercises the generated REST API through real HTTP requests. Three CustomerRepositoryStatementRuntimeTest cases execute composed JPQL and a native SQL projection through the generated data-access service. Three IdGenerationRuntimeTest cases cover application-side CUSTOM and provider-managed JPA UUID generation. RequestHeaderRuntimeTest calls the generated header endpoint with selected and additional request headers. DictionaryRuntimeTest adds one real dictionary HTTP roundtrip and one generated Java/OpenAPI contract check. The four ControllerCompositionGenerationTest cases check namespace placement, nested controller ownership, path identifiers and handler selection.
To create and start the executable application:
mvn clean package
java -jar target/example-spring-boot-1.3.81.jarThe application then listens on http://localhost:8080.
2. Models and generated layers
The domain model declares the entity, an enum and a value type:
core package org.joinedworkz.examples.customer
import org.joinedworkz.facilities.common.base
import org.joinedworkz.facilities.common.profiles.java
import org.joinedworkz.facilities.profiles.springboot
platform SpringBoot
enum<integer> SetupType {
NONE: 10
NP: 20
JP: 30
}
type Address {
street*: String(200)
zipCode: String(10)
city*: String(100)
country: String(2)
}
type CustomerSummary {
id: Id
firstName: Name
lastName: Name
}
type<entity> Customer {
id**: Id
firstName*: Name
lastName*: Name
email: String(255)
setupType: SetupType
mainAddress: Address
abstract customerQueryTemplate(): Customer[] =>
SELECT customer
FROM Customer customer
@{ criteria before='WHERE ', separator=' AND ' };;
abstract customerByLastNameCondition() augments customerQueryTemplate
criteria => customer.lastName = ?1;;
<find> loadByLastNameAndSetupType(
lastName*: Name,
setupType*: SetupType
): Customer[] augments customerByLastNameCondition
criteria => customer.setupType = ?2;;
<findOne> loadByEmailFromTemplate(email*: String): Customer
commandTemplate=customerQueryTemplate
criteria => customer.email = ?1;;
<findOne> loadSummaryByEmail(email*: String): CustomerSummary
language='sql' =>
SELECT
customer.id AS "id",
customer.first_name AS "firstName",
customer.last_name AS "lastName"
FROM customer customer
WHERE customer.email = ?1;;
}The API model applies all five SpringBoot CRUD method types:
api package org.joinedworkz.examples.customer.api vendorSpecificMimeType=true
import org.joinedworkz.examples.customer
import org.joinedworkz.facilities.profiles.springboot
import org.joinedworkz.facilities.springboot.api
platform SpringBoot
methodtype inspectRequestHeaders GET
success=204
consumeHeaders='x-correlation-id','x-tenant-id'
consumeAllHeaders=true
handler='org.joinedworkz.examples.customer.webapp.requestheaders.v1.handler.RequestHeaderHandler.inspectRequestHeaders'
resource /customers as Customer[] by id {
queryEntities()
createEntity()
readEntity()
updateEntity()
deleteEntity()
}
resource /request-headers {
inspectRequestHeaders()
}The backend component provides the resource and thereby establishes the controller boundary:
package org.joinedworkz.examples.backend
import org.joinedworkz.examples.customer.api
import org.joinedworkz.facilities.profiles.springboot
platform SpringBoot
component CustomerBackend
componentNamespace='org.joinedworkz.examples.customer.webapp' {
provide /customers
namespaceSuffix='customers.v1'
controller="CustomerV1Controller" {
}
provide /request-headers
namespaceSuffix='requestheaders.v1'
controller="RequestHeaderController" {
}
}Generation produces these cooperating layers:
SetupTypeexposes the public names and their explicit numeric mapping.SetupTypeAttributeConvertermaps those names to database codes.CustomerDtoandAddressDtoare the REST representations.Customeris the JPA persistence entity.CustomerMappermaps DTOs to entities and entities to DTOs. Its update method changes the existing entity while preserving its identifier.CustomerRepositoryis the Spring Data repository.ExamplesCustomerTupleConvertermaps the native result intoCustomerSummaryDtothrough the example-owned runtime glue.CustomerDataAccessServiceconnects mapping, repository access and query execution.CustomerV1Apideclares the generated HTTP contract.CustomerV1Controllerimplements that contract and delegates directly toCustomerDataAccessService.RequestHeaderApiandRequestHeaderControllerexpose the focused header contract.RequestHeaderHandleris a replaceable generated interface; its initialRequestHeaderHandlerImplis manual first-cut source.DictionaryDocumentDtomaps its dictionary field toMap<String, String>.DictionaryApi,DictionaryController, andDictionaryHandlerexpose the neutral JSON dictionary endpoint;DictionaryHandlerImplis manual first-cut source.
For an incoming request the call direction is controller → data-access service → mapper/repository. A returned entity travels through the mapper to a DTO and then through the data-access service and controller into the HTTP response.
3. The five CRUD method types
The imported SpringBoot API model supplies five reusable method types. In this example they produce the following contract:
createEntity()generatesPOST /customers. It maps the request DTO to an entity, persists it and returns the resulting DTO with status201 Created.readEntity()generatesGET /customers/{id}. It reads the entity by UUID and returns its DTO with status200 OK.updateEntity()generatesPUT /customers/{id}. It updates the entity identified by the path while preserving that identifier and returns the updated DTO with status200 OK.queryEntities()generatesGET /customers. It accepts page, page-size, filter and sort parameters and returns a query result with status200 OK.deleteEntity()generatesDELETE /customers/{id}. It removes the entity and returns status204 No Content.
The Customer model deliberately declares the UUID as the key without selecting an ID generation strategy. It remains an assigned-ID example, so a create request must provide an id. The separate synthetic ID-generation model in section 5 does not change this Customer contract.
4. Runtime CRUD test
The manually maintained CustomerCrudRuntimeTest is the executable runtime contract for this example. In one HTTP test it:
- creates Ada Lovelace with a supplied UUID and
setupTypeNP, checks201and every returned customer field, and confirms that H2 stores code20; - reads Ada by UUID and checks
200and the response content; - creates Grace Hopper;
- queries the collection with paging and ascending first-name sorting, then checks both result counts and the order
Ada,Grace; - updates Ada through the path UUID to
setupTypeJP, checks200, the changed fields and the preserved identifier, and confirms that H2 now stores code30; - reads the updated customer again;
- deletes Ada and checks
204; - queries again and checks that only Grace remains.
This test is manual source under src/test/**; it is not generator output. The profile keeps the generated integration-test cartridge disabled by default, and the example states that choice explicitly. The manual test remains the example's complete CRUD contract. An opt-in cartridge generates only eligible flat Create-to-Read tests; update, delete and query remain manual test responsibilities.
5. ID generation runtime tests
The separate model/id-generation.cmn is a focused synthetic model. It keeps the Customer CRUD flow unchanged while demonstrating a reusable ID type and a local key-field override:
core package org.joinedworkz.examples.customer.idgeneration
import org.joinedworkz.facilities.common.base
import org.joinedworkz.facilities.profiles.springboot
platform SpringBoot
type DefaultGeneratedId specialization of Id generation='CUSTOM'
type<entity> CustomGeneratedRecord {
id**: DefaultGeneratedId
label*: String(100)
}
type<entity> JpaGeneratedUuidRecord {
id**: DefaultGeneratedId generation='UUID'
label*: String(100)
}DefaultGeneratedId is a local name in this synthetic fixture, not a SpringBoot default. The facility default remains to omit generation and use an assigned ID, as the Customer model does.
The three manually maintained IdGenerationRuntimeTest cases call the generated data-access create(dto) methods and verify with H2 that:
CUSTOMcreates a UUID when the DTO has no ID;CUSTOMpreserves an explicit UUID with the example's reference glue; and- JPA
UUIDcreates an ID during persistence.
These cases do not establish runtime support for CUSTOM with a String key or for AUTO, IDENTITY, SEQUENCE and TABLE. See SpringBoot ID generation strategies for the complete modeling, lifecycle, provider and database boundaries.
6. Repository statement runtime tests
The three manually maintained CustomerRepositoryStatementRuntimeTest cases create synthetic customer rows and invoke generated data-access methods. They verify:
- two
criteriaparts composed in base-to-derived order throughaugments; - selection of the same root statement through
commandTemplate; and - a native SQL
Tuplemapped intoCustomerSummaryDtoby the selected column aliases.
The statement operations use descriptive load... names and explicit <find> or <findOne> stereotypes. They intentionally do not match the Java profile's separate findBy* or findOneBy* naming conventions. See Repository statements and composition for the complete advanced contract.
7. Request-header runtime test
The API model declares the two selected headers on the reusable inspectRequestHeaders method type and enables the SpringBoot complete-header map on the same method type. The concrete resource method inherits both properties. The resulting custom-handler contract is:
void inspectRequestHeaders(
String xCorrelationId,
String xTenantId,
Map<String, String> headers
)The manually maintained RequestHeaderRuntimeTest sends the two selected headers and an additional x-example-context header through a real HTTP request. It checks the 204 No Content response and verifies that the selected values and the additional map entry reach the first-cut handler. The handler deliberately neither logs nor persists the complete map.
Only the two modeled names appear as static OpenAPI parameters. The additional header demonstrates the separate SpringBoot runtime map; it does not make an unbounded header set part of the OpenAPI document. See the Base OpenAPI request-header contract and SpringBoot handler binding.
8. Dictionary field and REST body
The neutral dictionary domain model demonstrates the generic CMN order ValueType[KeyType] without tying it to the customer domain:
core package org.joinedworkz.examples.dictionary
import org.joinedworkz.facilities.common.base
import org.joinedworkz.facilities.common.profiles.java
import org.joinedworkz.facilities.profiles.springboot
platform SpringBoot
type DictionaryDocument {
entries: String[String]
}The Java DTO generator turns entries into Map<String, String>. A separate API model uses the same dictionary shape as both request and response body:
api package org.joinedworkz.examples.dictionary.api
import org.joinedworkz.examples.dictionary
import org.joinedworkz.facilities.profiles.springboot
platform SpringBoot
methodtype echoDictionary PUT
consumes=String[String]
produces=String[String]
success=200
operationName='echoDictionary'
operationId='dictionary.echo'
handler='org.joinedworkz.examples.dictionary.webapp.dictionary.v1.handler.DictionaryHandler.echoDictionary'
resource /dictionary-values as DictionaryDocument {
echoDictionary()
}The component provides /dictionary-values through DictionaryController. The generated controller and handler interface use Map<String, String> for the body and result. The generated OpenAPI request and response schemas both use type: object with additionalProperties: { type: string }.
DictionaryRuntimeTest sends {"en":"Hello","de":"Hallo"} to the generated PUT endpoint on a random HTTP port and checks the returned map. Its second test checks the generated handler signature, controller body/result types, and both OpenAPI additionalProperties declarations. The handler implementation is the manually completed first-cut source; the controller, handler interface, DTO, and OpenAPI document remain replaceable output.
The public REST example deliberately uses a direct String key. The contract also accepts a CMN string specialization whose effective Java type remains Java String, but rejects keys mapped to another Java type. Direct dictionary path or query parameters are not a supported SpringBoot contract; generation rejects them. Use the generic CMN cardinality reference, Java field mapping, Base OpenAPI mapping, and SpringBoot REST mapping for the separated contracts.
9. Controller-composition contract tests
The neutral model/controller-composition-*.cmn files compose independently modeled resource fragments below one public resource tree. Their component uses componentNamespace and three provide declarations. Two controller classes share the same effective namespace without being merged, while a third uses another namespaceSuffix.
A more specific boundary for /composition/v1/assets/events removes the event operations from the broader asset controller and assigns them to a separate controller. The modeled assetId does not appear in that provide selector, but it does appear as {assetId} in the generated request mapping. Within the asset boundary, a resource-level handlerClass is inherited by metadata operations and overridden by the nested summary resource.
The four contract tests verify that all generated controller, API and handler types compile and that the emitted files contain the expected operations and paths. They do not invoke the generated first-cut handler implementations; those are deliberately application-owned skeletons whose methods still fail until implemented. See Components and applications for the generic model contract and SpringBoot controller and handler composition for the Java mapping.
10. Configuration
The example has three effective JoinedWorkz settings:
platform.springboot.flavor=modern
override-package.org.iworkz.core.converter=org.joinedworkz.examples.glue.converter
cartridge.IntegrationTestCartridge.enabled=falseThe explicit modern flavor selects the current SpringBoot naming strategies, including singular table names and safe suffixes for reserved table and column names. It is selected explicitly; it is not inferred from the example structure. See SpringBoot profile and modeling for the configuration reference.
The explicit false matches the profile default and records that this example uses only its manually maintained runtime test. Other projects can opt in with cartridge.IntegrationTestCartridge.enabled=true; see the Spring Boot facility for the supported Create-to-Read shape.
The narrow package override routes only the generated native-result converter to the manual Spring bean under org.joinedworkz.examples.glue.converter. That example-owned implementation handles the scalar and binary UUID values used by this H2 scenario. Other org.iworkz.core imports remain provided by the selected reference glue; the override is deliberately not broadened.
SetupType deliberately separates the public enum names from the database codes. HTTP requests and responses use "NONE", "NP", and "JP". The JPA converter stores 10, 20, and 30. The runtime test verifies both sides of this contract through real HTTP requests and direct reads of the H2 column.
The example declares org.iworkz:genesis-spring:1.0.77 as a compact reference glue implementation. No direct genesis-core dependency is required because genesis-spring brings it transitively. This reference dependency is intended for examples, demos, and prototypes. Production projects can replace both glue package prefixes as described in Facilities and platforms.
No outlet override is required in this single-module example.
11. Generated output and ownership
Edit and commit the model and manual project sources:
model/*.cmnpom.xmljoinedworkz.propertiessrc/main/**src/test/**
Do not edit src/generated/**. It is replaceable generator output and is removed and regenerated by the example's clean build. Whether a project commits reviewed generated output or ignores it is a repository policy, not a change of ownership. The directory contains the generated Java layers and three OpenAPI documents:
src/generated/resources/openapi/org.joinedworkz.examples.customer.api.yamlsrc/generated/resources/openapi/org.joinedworkz.examples.backend_customerbackend.yamlsrc/generated/resources/openapi/org.joinedworkz.examples.controllercomposition.backend_controllercompositionbackend.yaml
The first document describes the non-abstract resources of the API model. The second aggregates all endpoints provided by CustomerBackend. The third is the component aggregate for ControllerCompositionBackend and contains its three provided controller boundaries. The application composition does not create another OpenAPI aggregate.
The manual application shell is src/main/java/org/joinedworkz/examples/SpringBootExampleApplication.java; its runtime configuration is src/main/resources/application.properties. General regeneration and takeover rules are documented in Generated output, ownership and regeneration.
12. Continue from the example
Use the example as a baseline for project-specific services, additional API views, more components or multi-module outlet routing. Continue with:
