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
- The payment provider settings bind from `payments.provider.*` into one typed, immutable object: `base-url` (required, an absolute `https` URL), `api-key` (required, not blank; prod sets it through the `PAYMENTS_PROVIDER_API_KEY` environment variable), and `connect-timeout` / `read-timeout` (Spring duration strings such as `2s` or `1500ms`, each from 1 to 60 seconds, defaulting to 2 s and 10 s).
- A missing or invalid setting stops the application at startup with Spring Boot's binding-validation error, which names the failing setting (for the https rule, the `httpsBaseUrl` check and its message), so a client is never built from a bad config.
- A `RestClient` bean, `paymentsRestClient`, calls the provider at `base-url` with `Authorization: Bearer <api-key>` on every request and both timeouts applied. The charge and refund clients that inject it come in the next PR.
- The API key must not appear when the settings object is logged or printed.
- Tests cover the defaults, a missing key, a plain `http` URL and an out-of-range timeout.
Files touched
- src/main/java/com/acme/billing/payments/PaymentsProperties.java
- src/main/java/com/acme/billing/payments/PaymentsClientConfig.java
- src/test/java/com/acme/billing/payments/PaymentsPropertiesTest.java
--- 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