Skip to content
docuconf
docuconf on GitHub

Get started / Java (Spring Boot)

docuconf for Java (Spring Boot)

Your @ConfigurationProperties records, with the Bean Validation annotations you already use, marked @Docuconf. An annotation processor writes the contract at compile time, and an auto-configuration checks everything before any bean binds.

Builds on
Spring Boot 3 / 4 @ConfigurationProperties
Package
dev.docuconf:docuconf-spring (Maven)
Requires
Java 17
Status
v0.1 alpha, not yet released · conformance 134 of 134
Source
docuconf-java · the orders example

Not published yet

Every docuconf SDK is a v0.1 alpha and none is on a package registry yet, so step 1 installs from the main branch. After the first release it will be dev.docuconf:docuconf-spring:0.1.0 and dev.docuconf:docuconf-processor:0.1.0 from Maven Central.

The code below is the orders service, the example every SDK ships: six variables, one of them a secret, checked the same way in every language. Compare it across languages.

1.Install

The artifacts are not on Maven Central yet. Install them from the main branch into your local Maven repository (Java 17+), then add the starter and the annotation processor:

git clone https://github.com/Docuconf/docuconf-java ../docuconf-java
mvn -f ../docuconf-java install -DskipTests
pom.xml: dependencies
<!-- docuconf: from mvn install of the main branch, until the first release -->
<dependency>
  <groupId>dev.docuconf</groupId>
  <artifactId>docuconf-spring</artifactId>
  <version>0.1.0-SNAPSHOT</version>
</dependency>
pom.xml: maven-compiler-plugin configuration
<!-- docuconf: writes META-INF/docuconf/contract.cue at compile time -->
<annotationProcessorPaths>
  <path>
    <groupId>dev.docuconf</groupId>
    <artifactId>docuconf-processor</artifactId>
    <version>0.1.0-SNAPSHOT</version>
  </path>
</annotationProcessorPaths>
<!-- end docuconf -->

2.Declare your configuration

An ordinary @ConfigurationProperties record with Bean Validation annotations. docuconf adds @Docuconf, @Secret and @UrlSchemes. The first sentence of each Javadoc @param line is the description, and the rest the details. Spring binds orders.port from ORDERS_PORT.

OrdersProperties.java
package dev.docuconf.examples.orders;

import dev.docuconf.Docuconf;
import dev.docuconf.EnumCase;
import dev.docuconf.MaxLength;
import dev.docuconf.Redacted;
import dev.docuconf.Secret;
import dev.docuconf.UrlSchemes;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.net.URI;
import java.time.Duration;
import java.util.List;
import org.hibernate.validator.constraints.time.DurationMax;
import org.hibernate.validator.constraints.time.DurationMin;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.validation.annotation.Validated;

/**
 * Settings of the orders service. Each property is the environment variable Spring binds to it: {@code port} is
 * {@code ORDERS_PORT}, {@code databaseUrl} is {@code ORDERS_DATABASEURL}. The first sentence of each property's
 * Javadoc is the contract's description, and the rest is its details, longer docs for {@code docuconf docs}.
 *
 * @param port HTTP listen port
 * @param logLevel Minimum level of the log lines the service writes
 * @param databaseUrl Postgres connection URL for the orders database
 * @param allowedOrigins Origins allowed to call the API from a browser
 * @param requestTimeout Time allowed to answer one request
 * @param workerCount Background workers that process new orders
 *        <p>Each worker holds one connection from the pool of {@code databaseUrl}, so keep this below the
 *        database's connection limit.
 *        <ul>
 *          <li>Raise it when the order queue backs up.</li>
 *          <li>Lower it when the database is the bottleneck.</li>
 *        </ul>
 */
@Docuconf(service = "orders", enumCase = EnumCase.LOWER)
@Validated
@ConfigurationProperties("orders")
public record OrdersProperties(
        @Min(1) @Max(65535) @DefaultValue("8080") int port,
        @DefaultValue("INFO") LogLevel logLevel,
        // @Secret: the platform must supply it from a Secret, and docuconf never prints it. @MaxLength bounds the
        // URL in characters; a longer one fails startup with out_of_range.
        @NotNull @Secret @UrlSchemes("postgres") @MaxLength(2048) URI databaseUrl,
        @NotEmpty @DefaultValue("http://localhost:3000") List<String> allowedOrigins,
        @DurationMin(seconds = 1) @DurationMax(minutes = 5) @DefaultValue("30s") Duration requestTimeout,
        @Min(1) @Max(64) @DefaultValue("4") int workerCount) {

    /** Log levels. The contract spells them in lower case (enumCase); the app accepts any case, as Spring does. */
    public enum LogLevel { DEBUG, INFO, WARN, ERROR }

    /** Prints the secret as [redacted]; a record's generated toString() would print it. */
    @Override
    public String toString() {
        return Redacted.toString(this);
    }
}

3.Load it at boot

Nothing to call. The docuconf-spring auto-configuration checks the environment against the contract once it is complete and before any bean is created, so no properties bean ever binds a bad value.

OrdersApplication.java
@SpringBootApplication
@ConfigurationPropertiesScan
@RestController
public class OrdersApplication {

    private final OrdersProperties config;

    OrdersApplication(OrdersProperties config) {
        this.config = config;
    }

    /**
     * Starts the service.
     *
     * @param args command-line arguments
     */
    public static void main(String[] args) {
        SpringApplication.run(OrdersApplication.class, args);

4.Run it, and see an error

Run the example with a valid environment:

ORDERS_DATABASEURL=postgres://orders:pw@localhost:5432/orders java -jar target/orders.jar

With ORDERS_PORT=0 and no ORDERS_DATABASEURL, Spring Boot's failure analyzer prints every problem with its error code, after Spring's own startup lines, and the process exits 1. The same lines go to /dev/termination-log.

Terminal
$ ORDERS_PORT=0 java -jar target/orders.jar
...
***************************
APPLICATION FAILED TO START
***************************

Description:

docuconf: 2 configuration problems:

    [missing_required] ORDERS_DATABASEURL: is required (orders.database-url)
    [out_of_range] ORDERS_PORT: is below min 1 (got 0)

Action:

Set or fix the environment variables and files listed above. Codes are defined in the docuconf spec (section 11.2). For local runs, DOCUCONF_FILE_ROOT=./dev reads file inputs from ./dev; docuconf.enabled=false skips the check (for tests and build-time tasks).

5.Test your configuration

DocuconfTester checks an explicit environment against the contract without starting the app, so the test needs no environment variables and no web server. It returns every code, and the typo hints too.

OrdersConfigTest.java
class OrdersConfigTest {

    private static final String DB = "postgres://orders:secret@db.internal:5432/orders";

    @Test
    void aValidEnvironmentPasses() {
        var result = DocuconfTester.env(Map.of("ORDERS_DATABASEURL", DB, "ORDERS_LOGLEVEL", "warn")).check();
        assertTrue(result.ok(), result.violations().toString());
    }

    @Test
    void everyProblemIsReported() {
        var result = DocuconfTester.env(Map.of("ORDERS_PORT", "0", "ORDERS_REQUESTTIMEOUT", "1m30s")).check();
        assertEquals(List.of(
                "invalid_type ORDERS_REQUESTTIMEOUT",
                "missing_required ORDERS_DATABASEURL",
                "out_of_range ORDERS_PORT"), result.codes());
    }

    @Test
    void aTypoGetsAHint() {
        var result = DocuconfTester.env(Map.of("ORDERS_DATABASEURL", DB, "ORDERS_PROT", "9090")).check();
        assertEquals(List.of("ORDERS_PROT is set but not declared; did you mean ORDERS_PORT?"), result.warnings());
    }
}
mvn test

6.Export the contract

The annotation processor writes the contract on every compile, into target/classes and so into the jar. The docuconf-maven-plugin copies it next to the app with mvn docuconf:export, and its check goal fails mvn verify when the committed contract.cue is out of date.

mvn docuconf:export
contract.cue
// Code generated by docuconf. DO NOT EDIT.
package orders

import "docuconf.dev/contract"

contract.#Contract & {
	apiVersion: "docuconf.dev/v1alpha1"
	kind: "ConfigContract"
	metadata: {
		name: "orders"
		appVersion: "1.0.0"
		generator: {language: "java", sdk: "docuconf-spring", version: "0.1.0-SNAPSHOT"}
	}
	vars: {
		ORDERS_ALLOWEDORIGINS: {
			type: "list"
			description: "Origins allowed to call the API from a browser"
			configKey: "orders.allowed-origins"
			items: "string"
			encoding: "csv"
			minItems: 1
			default: ["http://localhost:3000"]
		}
		ORDERS_DATABASEURL: {
			type: "url"
			description: "Postgres connection URL for the orders database"
			required: true
			secret: true
			configKey: "orders.database-url"
			schemes: ["postgres"]
			maxLength: 2048
		}
		ORDERS_LOGLEVEL: {
			type: "enum"
			description: "Minimum level of the log lines the service writes"
			configKey: "orders.log-level"
			values: ["debug", "info", "warn", "error"]
			default: "info"
		}
		ORDERS_PORT: {
			type: "int"
			description: "HTTP listen port"
			configKey: "orders.port"
			min: 1
			max: 65535
			default: 8080
		}
		ORDERS_REQUESTTIMEOUT: {
			type: "duration"
			description: "Time allowed to answer one request"
			configKey: "orders.request-timeout"
			encoding: "iso8601"
			min: "1s"
			max: "5m"
			default: "30s"
		}
		ORDERS_WORKERCOUNT: {
			type: "int"
			description: "Background workers that process new orders"
			details: "Each worker holds one connection from the pool of `databaseUrl`, so keep this below the database's connection limit.\n\n- Raise it when the order queue backs up.\n- Lower it when the database is the bottleneck."
			configKey: "orders.worker-count"
			min: 1
			max: 64
			default: 4
		}
	}
}

The platform then checks each environment's values against contract.cue before it deploys, with docuconf vet, the Helm chart or CUE. See How it works.

7.Spring Boot

This SDK is the Spring Boot integration, for Spring Boot 3 and 4. Profiles in application-{profile}.yml are exported as contract profiles, and a platform-mounted application.yml overlay is supported. docuconf.enabled=false skips the check for build-time tasks. See the README.

Next

Every snippet on this page is compiled or run against docuconf-java's main branch in CI. Facts checked 2026-10-08.