Spring Boot applications have two options for calling the TaxID API: `RestTemplate` for synchronous, blocking calls in traditional MVC applications, and `WebClient` for reactive, non-blocking calls in Spring WebFlux projects. This guide covers both, along with Spring Cache integration to avoid redundant API calls and a JUnit 5 test suite. For the use-case guide with wiring steps, see the Java Spring Boot VAT validation use case.
Response DTO
package com.example.vat.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
public record VatResponse(
boolean valid,
String status,
String vat,
@JsonProperty("country_code") String countryCode,
@JsonProperty("company_name") String companyName,
String address,
boolean cached,
@JsonProperty("request_id") String requestId
) {}VatValidationService with RestTemplate
package com.example.vat.service;
import com.example.vat.dto.VatResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class VatValidationService {
private final RestTemplate restTemplate;
private final String apiKey;
private static final String BASE_URL = "https://taxid.dev/api/v1";
public VatValidationService(
RestTemplate restTemplate,
@Value("${taxid.api-key}") String apiKey) {
this.restTemplate = restTemplate;
this.apiKey = apiKey;
}
@Cacheable(value = "vatValidations", key = "#vatNumber.toUpperCase()",
unless = "#result.status().equals('service_unavailable')")
public VatResponse validate(String vatNumber) {
String normalised = vatNumber.replaceAll("\\s", "").toUpperCase();
String country = normalised.substring(0, 2);
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(apiKey);
HttpEntity<Void> entity = new HttpEntity<>(headers);
try {
ResponseEntity<VatResponse> response = restTemplate.exchange(
BASE_URL + "/validate/" + country + "/" + normalised,
HttpMethod.GET,
entity,
VatResponse.class
);
return response.getBody();
} catch (Exception e) {
return new VatResponse(false, "service_unavailable", normalised, country,
null, null, false, null);
}
}
}Spring Cache Configuration
package com.example.vat.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.*;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager("vatValidations");
manager.setCaffeine(
Caffeine.newBuilder()
.expireAfterWrite(23, TimeUnit.HOURS)
.maximumSize(10_000)
);
return manager;
}
}REST Controller
package com.example.vat.controller;
import com.example.vat.dto.VatResponse;
import com.example.vat.service.VatValidationService;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class VatController {
private final VatValidationService vatService;
public VatController(VatValidationService vatService) {
this.vatService = vatService;
}
@PostMapping("/validate-vat")
public ResponseEntity<?> validateVat(@RequestBody ValidateVatRequest request) {
if (request.vatNumber() == null || request.vatNumber().isBlank()) {
return ResponseEntity.badRequest().body(new ErrorResponse("vat_required"));
}
VatResponse result = vatService.validate(request.vatNumber());
return switch (result.status()) {
case "active" -> ResponseEntity.ok(new ValidResult(result.companyName(), result.address()));
case "inactive" -> ResponseEntity.unprocessableEntity().body(new ErrorResponse("vat_inactive"));
case "format_invalid" -> ResponseEntity.unprocessableEntity().body(new ErrorResponse("format_invalid"));
default -> ResponseEntity.ok(new UnavailableResult());
};
}
record ValidateVatRequest(String vatNumber) {}
record ValidResult(String companyName, String address) {}
record ErrorResponse(String error) {}
record UnavailableResult(boolean unavailable) { UnavailableResult() { this(true); } }
}JUnit 5 Tests with MockRestServiceServer
@SpringBootTest
class VatValidationServiceTest {
@Autowired private VatValidationService vatService;
@Autowired private RestTemplate restTemplate;
private MockRestServiceServer mockServer;
@BeforeEach
void setUp() {
mockServer = MockRestServiceServer.createServer(restTemplate);
}
@Test
void returnsValidForActiveNumber() throws Exception {
mockServer.expect(requestTo(containsString("/validate/DE/DE123456789")))
.andExpect(method(HttpMethod.GET))
.andRespond(withSuccess(
"{\"valid\":true,\"status\":\"active\",\"company_name\":\"Acme GmbH\",\"request_id\":\"r1\"}",
MediaType.APPLICATION_JSON
));
VatResponse result = vatService.validate("DE123456789");
assertThat(result.valid()).isTrue();
assertThat(result.status()).isEqualTo("active");
assertThat(result.companyName()).isEqualTo("Acme GmbH");
}
}Related guides
Start validating EU VAT numbers
Free plan — 100 validations/month. No credit card required.