Typed settings for the payment provider client

Java · Spring Boot · intermediate · greenfield

The new payment client gets its settings from one validated `@ConfigurationProperties` record instead of loose `@Value` strings. Bad config now fails at startup with a validation error that says which check failed, instead of at the first charge. Timeouts are real `Duration`s with defaults, `https` is enforced, and `toString` masks the key so the object is safe to log. The `RestClient` bean takes its base URL, auth header and timeouts from the record. `ApplicationContextRunner` tests cover the happy path and the three failure modes.

Billing service on Spring Boot 3.3 (Spring Framework 6.1, Java 21, Gradle with the Spring Boot plugin), with `spring-boot-starter-web` and `spring-boot-starter-validation`, so Boot auto-configures Hibernate Validator and a prototype-scoped `RestClient.Builder`. No other HTTP client library is on the classpath. The old `@Value` usages were removed in the previous PR along with the old client; nothing injects `paymentsRestClient` yet. `/actuator/configprops` is not exposed, and `org.springframework.web.client` logs at the default level. The provider's base URL includes the API version path (`https://api.pay.example/v1`).

Requirements

Files touched

--- src/main/java/com/acme/billing/payments/PaymentsProperties.java
+package com.acme.billing.payments;
+
+import java.net.URI;
+import java.time.Duration;
+
+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;
+
+import jakarta.validation.constraints.AssertTrue;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+
+@Validated
+@ConfigurationProperties(prefix = "payments.provider")
+public record PaymentsProperties(
+        @NotNull URI baseUrl,
+        @NotBlank String apiKey,
+        @DurationMin(seconds = 1) @DurationMax(seconds = 60) @DefaultValue("2s") Duration connectTimeout,
+        @DurationMin(seconds = 1) @DurationMax(seconds = 60) @DefaultValue("10s") Duration readTimeout) {
+
+    @AssertTrue(message = "must be an absolute https URL")
+    public boolean isHttpsBaseUrl() {
+        return baseUrl == null || ("https".equalsIgnoreCase(baseUrl.getScheme()) && baseUrl.getHost() != null);
+    }
+
+    @Override

Review this PR

Java practice