Skip to content

SpringBoot profile and canonical modeling

This page explains the CMN modeling and configuration contract of the SpringBoot platform.

Model and configuration changes drive replaceable output; first-cut files become manual after their initial creation. See Generated output, ownership and regeneration for the complete ownership matrix.

1. Layers and generic output routing

A root CMN package can have an arbitrary layer name:

cmn
shared package org.example.customer

import org.joinedworkz.facilities.profiles.springboot

platform SpringBoot

The layer is stored on the transformed model and inherited by its subpackages. It normally becomes the effective layer for generated output. A generator can assign a different effective layer to a particular file. The generic property form is:

properties
outlet.<outlet>.<layer>.directory=<directory>

Layer names do not form a closed registry. A project can use names such as core, backend, shared or its own domain-specific names.

core and backend are useful architecture conventions:

  • core commonly identifies domain-oriented models;
  • backend commonly identifies component or application models.

SpringBoot does not attach additional generation semantics to either name. Their technical effect is the same generic effective-layer routing available to every other layer name.

2. Layers with additional SpringBoot semantics

2.1 api

The api layer has additional SpringBoot semantics and is also selected as the effective layer by several SpringBoot generators:

  • DTO and mapper output from an api model normally retains effective layer api;
  • generated REST controllers and their API interfaces are explicitly assigned effective layer api, including when the component model itself has no declared layer;
  • a complex type declared in an api model can be mapped to a source type when its fields retain their source-field relationships.

An API resource does not require a separate API DTO. It can directly use an imported domain type:

cmn
api package org.example.customer.api

import org.example.customer
import org.joinedworkz.facilities.profiles.springboot
import org.joinedworkz.facilities.springboot.api

platform SpringBoot

resource /customers as Customer[] by id {
    queryEntities()
    createEntity()
    readEntity()
    updateEntity()
    deleteEntity()
}

Use a separate view only when the HTTP representation must differ from its source type:

cmn
type CustomerView {
    ...Customer exclude internalId
}

The inclusion keeps the source-field relationships needed by the generated MapStruct mapper. A distinct API type without a complete source mapping is not a usable runtime boundary; make the generated project compile and exercise the affected endpoint before relying on such a view.

2.2 mapping

For a model in the mapping layer, SpringBoot selects generatedMappingSource for generated DTOs and entity mappers. Other persistence output, such as entities, repositories and data access services, continues to use generatedJavaSource.

Both outlets use src/generated/java by default, so the distinction becomes visible only when their directories are overridden. For these DTO and mapper files, mapping remains the effective layer within generatedMappingSource in addition to selecting that outlet.

2.3 DTO naming and external

SpringBoot uses the Java facility's stable DTO naming contract. The default decorated mode adds both the .dto package segment and the Dto class-name suffix. The advanced plain mode omits both:

  • decorated: org.example.customer.dto.CustomerDto;
  • plain: org.example.customer.Customer.

Set the project default in joinedworkz.properties:

properties
java.dto.naming=plain

Or override it on a CMN package:

cmn
package org.example.customer skipDtoPostfix=true

skipDtoPostfix=true selects plain naming and false selects decorated naming. Nested package and subpackage blocks inherit the value, with the nearest declaration taking precedence. The external layer implicitly selects plain naming when neither the concrete package nor a parent declares skipDtoPostfix:

cmn
external package org.example.partner

The complete precedence is concrete package, nearest parent, external layer, java.dto.naming, then the decorated default. An explicit skipDtoPostfix=false therefore selects decorated naming even for an external package.

This naming decision is shared by DTO declarations and generated references, including SpringBoot mapper and persistence output. If plain naming creates a same-simple-name collision between a DTO and an entity, JoinedWorkz emits a qualified Java reference where needed. Clean and regenerate all affected replaceable Java output after changing the mode.

See Java facility: DTO naming for the inherited contract and the joinedworkz.properties reference for value parsing.

3. From an entity to a provided REST resource

A persistent domain type starts with the entity stereotype:

cmn
core package org.example.customer

import org.joinedworkz.facilities.common.base
import org.joinedworkz.facilities.profiles.springboot

platform SpringBoot

type<entity> Customer {
    id**:       Id
    firstName*: Name
    lastName*:  Name
    email:      String(255)
}

The entity drives the generated DTO, JPA entity, MapStruct mapper, Spring Data repository and data access service. A resource describes the HTTP operations; a component that provides that resource causes SpringBoot to generate the API interface and controller:

cmn
package org.example.customer.backend

import org.example.customer.api
import org.joinedworkz.facilities.profiles.springboot

platform SpringBoot

component CustomerBackend
    componentNamespace="org.example.customer.webapp" {

    provide /customers
        namespaceSuffix="customers.v1"
        controller="CustomerV1Controller"
}

The component package does not need a backend layer. Add that layer only when its output should use the route for effective layer backend.

SpringBoot resolves the target namespace for each provided boundary as follows:

text
component target = componentNamespace
                   or, when absent, the component model's CMN namespace

provided target  = component target
                   + optional "." + namespaceSuffix

It then writes the related Java artifacts below that target:

ArtifactTarget name for the example
API interfaceorg.example.customer.webapp.customers.v1.api.CustomerV1Api
Controllerorg.example.customer.webapp.customers.v1.controller.CustomerV1Controller
Default handler packageorg.example.customer.webapp.customers.v1.handler

componentNamespace and namespaceSuffix organize generated component artifacts; they do not change the CMN model namespace or HTTP path. The previous component property basePackage and provide property subPackage remain deprecated aliases for compatibility. Use only the canonical names in new models.

When controller is omitted, SpringBoot derives the class name from the provided boundary's final resource: a named resource is singularized and converted to upper camel case before Controller is appended; an unnamed resource falls back to its representation name. A controller name ending in Controller maps to an API name with that suffix replaced by Api; other controller names receive an appended Api.

Nested provided boundaries can split one resource tree into several controllers. The deepest matching boundary owns its subtree:

cmn
component CatalogBackend
    componentNamespace='com.example.catalog.runtime' {

    provide /catalog/products
        namespaceSuffix='products.v1'
        controller='ProductController'

    provide /catalog/products/reviews
        namespaceSuffix='reviews.v1'
        controller='ReviewController'
}

ProductController receives the product operations but not the operations under the more specific reviews boundary. ReviewController receives that nested subtree. Each boundary must resolve to its own controller and API identity; repeating one final class identity is not a supported controller merge declaration. The platform-independent resource-selector and OpenAPI rules are documented under Components and applications.

3.1 Integer-coded enumeration persistence

A scalar persistent field can use an explicitly coded enumeration:

cmn
enum<integer> SetupType {
    NONE: 10
    NP:   20
    JP:   30
}

type<entity> Customer {
    id**: Id
    setupType: SetupType
}

The generated DTO and HTTP contract use the names NONE, NP, and JP. The Base OpenAPI document consequently contains a string enumeration. The JPA entity instead uses a generated attribute converter and stores 10, 20, or 30 in the database column.

The converter preserves null in both directions. Reading a database code for which the enumeration declares no value fails with an IllegalArgumentException; JoinedWorkz does not select an ordinal or another fallback value. Ordinary CMN enumerations continue to use name-based JPA persistence. The integer-code contract applies only to enum<integer>.

This converter contract is supported for ordinary scalar entity fields. Collections, dictionaries, keys, versions, references, relations, and calculated fields are rejected during validation instead of falling back to implicit ordinal persistence.

A SpringBoot-generated integer enum implements the package-overridable org.iworkz.core.enumeration.MappedEnum<Integer> contract. Its generated JPA converter extends the package-overridable org.iworkz.spring.persistence.converter.AbstractMappedEnumAttributeConverter. An integer enum imported from a Java-platform model remains usable because the converter delegates explicitly to its generated toMappedValue and fromMappedValue methods. The default glue contracts and their replacement workflow are described in Facilities and platforms.

3.2 Custom repository statements

Operations inside a persistent entity can define advanced JPQL or native SQL repository statements. SpringBoot supports direct statements as well as named statement parts composed through augments or the SpringBoot-specific commandTemplate property.

This is separate from resource-method modeling: an entity operation generates a persistence/data-access operation, not an HTTP endpoint. The complete language selection, template syntax, result mapping and composition contract is documented in Repository statements and composition.

3.3 ID generation strategies

The ** marker identifies an entity key; it does not select how a missing key is created. SpringBoot adds the case-sensitive generation property for that separate decision. Omit it for an assigned ID, or use one of CUSTOM, UUID, AUTO, IDENTITY, SEQUENCE and TABLE on the effective JPA key field.

Prefer a reusable simple ID type when several entities share a strategy. A property declared directly on one key field overrides the propagated value:

cmn
type GeneratedId specialization of Id generation='CUSTOM'

type<entity> CustomRecord {
    id**: GeneratedId
}

type<entity> JpaUuidRecord {
    id**: GeneratedId generation='UUID'
}

CUSTOM has a generated data-access lifecycle, while the other values select JPA provider strategies. Read SpringBoot ID generation strategies for supported key types, explicit-ID precedence, direct-save behavior, diagnostics and the database boundary.

4. CRUD methods and handlers

The opinionated CRUD resource-method types are:

  • createEntity
  • readEntity
  • updateEntity
  • queryEntities
  • deleteEntity

With an entity representation, their generated controllers delegate to the corresponding data access service operations. A resource operation can instead select a handler through its handler or handlerMethod modeling property. For a generated custom handler, JoinedWorkz creates a replaceable interface and an initial first-cut implementation. The implementation becomes manual source after its first creation and must be completed by the application developer.

4.1 Controller and handler composition

A generated controller is scoped by one provide boundary, but it does not require one handler class for the complete resource subtree. Each resource operation resolves its own handler. One controller can therefore inject and delegate to several handler classes when nested resources or individual operations select different application behavior.

For string-valued handler metadata, SpringBoot resolves the class and method parts independently in this order:

  1. An effective handlerClass or handlerMethod on the resource operation supplies that explicit part.
  2. handler='com.example.catalog.handler.SearchHandler.search' supplies any class or method part that is still missing. Explicit handlerClass and handlerMethod values therefore override the corresponding part of this combined form.
  3. If the class is still missing, the nearest handlerClass on the containing resource is used. The lookup continues through parent resources, so a local nested-resource declaration overrides an ancestor declaration for its subtree.
  4. A method without a class selects a generated default handler class below the provided boundary's .handler package. A class without a method uses the effective operationName, or the final segment of operationId when no operation name is available.

When handler resolves to a modeled service operation instead of a string, SpringBoot delegates directly to that operation's generated data-access service and uses the modeled operation name. That reference form is a separate contract; the string-part precedence above is not applied to it.

An unqualified handlerClass, such as SearchHandler, is relative to the provided boundary's default handler package. A fully qualified class name is used as modeled and can therefore place the handler independently of componentNamespace and namespaceSuffix.

Properties inherited from a method type are already part of the effective resource-operation properties before this resolution runs. A declaration on the concrete resource operation can override that inherited value according to the normal profile-property propagation contract.

For each custom handler class selected by at least one operation, JoinedWorkz generates:

  • a replaceable handler interface containing the required operation signatures; and
  • an initial <HandlerName>Impl first-cut implementation whose methods still fail until application behavior is implemented.

The interface follows the model and is regenerated. The implementation is created only when absent and becomes manually maintained application source. Do not edit the generated interface to add business logic, and do not treat the generated controller as the manual extension point. See Generated output and ownership.

This contract does not promise that several separate provide declarations with the same controller class name are merged. Use a single broader boundary and per-operation or per-resource handler selection when one controller should delegate to several handlers.

4.2 Request headers and handler signatures

SpringBoot realizes the Base consumeHeaders OpenAPI contract as individual optional request-header parameters. It additionally provides consumeAllHeaders for a custom handler that must receive the complete request-header map:

cmn
package com.example.customer.api

import org.joinedworkz.facilities.profiles.springboot

platform SpringBoot

methodtype inspectRequestHeaders GET
    success=204
    consumeHeaders='x-correlation-id','x-tenant-id'
    consumeAllHeaders=true
    handler='com.example.customer.handler.RequestHeaderHandler.inspectRequestHeaders'

resource /request-headers {
    inspectRequestHeaders()
}

The generated controller and handler contracts are:

  • consumeHeaders='x-correlation-id' generates the optional controller parameter @RequestHeader(value = "x-correlation-id", required = false) String xCorrelationId and the handler parameter String xCorrelationId. Without the property, no named-header parameter is generated.
  • consumeAllHeaders=true generates @RequestHeader Map<String, String> headers in the controller and Map<String, String> headers in the handler. A missing value or false generates no complete-header map.

Both properties can be declared on a method type and inherited by its concrete resource methods. A local consumeAllHeaders=false disables an inherited true; a local consumeHeaders value replaces the inherited list. When both properties are active, the individual String parameters precede the headers map in the generated handler signature:

java
void inspectRequestHeaders(
    String xCorrelationId,
    String xTenantId,
    Map<String, String> headers
)

The complete map normally has the Java parameter name headers. If a selected header would use the same Java name, the generator chooses a collision-free name such as requestHeaders for the complete map. It is a single-value Map<String, String>, not a multi-value header representation. Because its incoming names are not a finite modeled set, consumeAllHeaders=true does not add arbitrary header parameters to the static OpenAPI document. Only the names declared through consumeHeaders are listed there.

Treat forwarded request headers as untrusted input

The complete map can contain credentials, cookies, proxy metadata, tracing information, and other sensitive values. Do not log, persist, or forward it without an application-specific allowlist.

These are CMN resource-method properties, not joinedworkz.properties keys. Use them with the custom-handler path: the generated handler interface is replaceable output, while its initial implementation becomes manual first-cut source. See SpringBoot target platform design.

4.3 Dictionary request and response bodies

SpringBoot supports a dictionary whose CMN key is string-based and whose effective Java type is Java String as a JSON request or response body. This includes string specializations that do not select a different javaType; a string specialization mapped to java.util.UUID, for example, is rejected as a REST dictionary key. Declare the value type before the brackets and the key type inside them:

cmn
methodtype echoDictionary PUT
    consumes=String[String]
    produces=String[String]
    success=200
    operationName='echoDictionary'
    handler='com.example.dictionary.handler.DictionaryHandler.echoDictionary'

resource /dictionary-values {
    echoDictionary()
}

The generated API, controller and custom-handler interface use Map<String, String> for both the request body and return value. The HTTP representation is one JSON object, not an array and not an object with a generated wrapper property:

text
{
  "en": "Hello",
  "de": "Hallo"
}

The initial handler implementation is first-cut source and must implement the application behavior. The generated interface and controller remain replaceable output. A DTO that contains a dictionary field uses the inherited Java Map<K, V> field mapping. The corresponding Base OpenAPI schema is an object with additionalProperties.

This public REST contract is deliberately narrower than the general CMN grammar:

  • use a CMN string-based key type that maps to Java String for JSON request and response bodies;
  • direct dictionary resource-method parameters are not supported as Spring MVC path or query parameters and are rejected during generation; and
  • model several known path or query values as separate scalar parameters, or move a dynamic key/value set into a JSON request body.

consumeAllHeaders=true is a separate header-binding feature. Its Map<String, String> represents incoming HTTP headers and does not make a CMN dictionary parameter valid as a path or query parameter.

5. legacy and modern generation flavors

Select the flavor in joinedworkz.properties:

properties
platform.springboot.flavor=modern

The accepted values are lowercase legacy and modern. Surrounding whitespace is ignored. A missing property selects legacy; a blank or unknown value stops generation with a configuration error.

The flavors differ in generated defaults:

  • legacy is the compatibility default. Table names are lowercase snake case and pluralized. Default DTO names for a reference such as customer use customerid; a collection uses customerids.
  • modern uses singular lowercase snake-case table names. Common reserved table names receive _tbl, and common reserved column names receive _col. Reference names use Java-style customerId and customerIds.

Explicit model configuration such as tableName and columnName takes precedence over the corresponding database-name defaults.

The flavor affects generated Java APIs and database identifiers. Treat a change as a model and schema migration, not as formatting:

  1. Commit or otherwise preserve the current model, configuration and database migration history.
  2. Change the flavor once for the complete generated application.
  3. Clean only replaceable generated output; preserve first-cut sources and existing Flyway migrations.
  4. Regenerate all affected models and review DTO names, JPA mappings, mapper code and schema changes together.
  5. Add a new migration for every required database rename or structural change.
  6. Compile the complete application and run its persistence and REST tests before deploying it.

Never combine generated files from different flavors in one application.

6. REST and MIME configuration

Vendor-specific JSON media types are disabled by default. Enable them globally for resources generated with the active platform:

properties
rest.useVendorSpecificMimeType=true

A CMN package can override that default for its resources:

cmn
api package org.example.customer.api vendorSpecificMimeType=true

The package value has precedence over joinedworkz.properties, including an explicit package value of false.

For a represented type, the generated vendor media type follows this form:

text
application/vnd.<representation-namespace>.<type-name>+json

The type-name part is lowercase and dash-separated; collection representations use its plural form. The supported operation-level override is contentType.

The precise media-type set depends on the operation shape. Controller mappings, OpenAPI request bodies and OpenAPI responses do not necessarily list identical combinations of application/json and the vendor media type. When the exact HTTP contract matters, review both the generated controller/API annotations and the generated OpenAPI document.

6.1 OpenAPI documentation metadata

SpringBoot generates two related but distinct OpenAPI inputs:

  1. the static OpenAPI YAML inherited from the Base cartridge;
  2. Java annotations that SpringDoc can inspect at application runtime.

The canonical CMN documentation is reused for both paths where an equivalent Spring annotation exists:

CMN metadataGenerated Spring annotation
operation description@Operation(description = ...)
operation @summary@Operation(summary = ...)
@request description@RequestBody(description = ...)
@response for the primary CMN result, or status-result description@ApiResponse(description = ...)

Generated Java string literals preserve quotes, backslashes and line breaks. Descriptions therefore remain source-compatible instead of being inserted as unescaped annotation text.

Modeled Examples are emitted by the static Base OpenAPI generator; SpringBoot does not duplicate them as @ExampleObject annotations. Schema and field descriptions are likewise documented by the static document rather than promised as SpringDoc annotation metadata. Treat the Base YAML as the complete generated metadata contract and the annotations as a runtime-facing subset.

The primary CMN result is normally emitted under a concrete success status. It must not be confused with OpenAPI's separate default: error fallback.

The authoring syntax, OpenAPI placement and precedence are documented under Base: OpenAPI documentation and examples.

7. Outlet routing

Generic effective-layer routes and SpringBoot's additional api and mapping semantics can be combined:

properties
outlet.generatedJavaSource.core.directory=../domain/src/generated/java
outlet.generatedJavaSource.api.directory=../webapp/src/generated/java
outlet.generatedMappingSource.mapping.directory=../mapping/src/generated/java
outlet.generatedJavaSource.shared.directory=../shared/src/generated/java

The API route addresses files whose effective layer is api. A custom facility can define further outlets, and its generators can preserve the CMN model layer or assign another effective layer to particular output.

  1. Model persistent and reusable types.
  2. Model resources, using direct domain representations or deliberately mapped API views.
  3. Add custom entity statements only when a derived Spring Data operation is insufficient; keep their JPQL or SQL explicit and tested.
  4. Provide resources from components to obtain controllers and API interfaces.
  5. Add custom handlers only for operations that need application-specific behavior.
  6. Choose one SpringBoot flavor and keep it consistent.
  7. Add effective-layer routes only where the physical module layout requires them.
  8. Generate, compile and test the complete application.

See SpringBoot target platform design for the resulting artifact and runtime flow.