Engineering

How to Add License Key Validation to Your Spring Boot Application

TOT
Traffic Orchestrator Team
Engineering
April 10, 2026 7 min read 611 words
Share

How to Add License Key Validation to Your Spring Boot Application

Spring Boot is the de facto standard for enterprise Java applications. When you need to monetize your Spring Boot APIs, microservices, or SaaS products, license key validation provides a clean, scalable solution. This guide walks you through a complete integration with Traffic Orchestrator.

Why License Keys in Spring Boot?

Spring Boot powers enterprise-grade applications that need:

  • Per-tenant licensing in multi-tenant architectures
  • Feature-flagged access based on subscription tier
  • API monetization with usage-based or seat-based pricing
  • Offline verification for on-premise deployments

Step 1: Add Dependencies

Add the required dependencies to your pom.xml:

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

Step 2: Create the License Client Service

Use WebClient for non-blocking license validation:

// LicenseService.java
@Service
public class LicenseService {
    private final WebClient webClient;

public LicenseService(@Value("${to.base-url:https://api.trafficorchestrator.com/api/v1}") String baseUrl) { this.webClient = WebClient.builder() .baseUrl(baseUrl) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); }

public Mono<LicenseResult> validate(String key, String domain) { return webClient.post() .uri("/validate") .bodyValue(Map.of("key", key, "domain", domain)) .retrieve() .bodyToMono(LicenseResult.class) .onErrorReturn(new LicenseResult(false, null, List.of(), "Service unavailable")); } }

public record LicenseResult( boolean valid, String key, List<String> features, String error ) {} ```

Step 3: Create a License Validation Filter

A servlet filter enforces licensing across all requests:

// LicenseFilter.java
@Component
@Order(1)
public class LicenseFilter extends OncePerRequestFilter {
    private final LicenseService licenseService;

public LicenseFilter(LicenseService licenseService) { this.licenseService = licenseService; }

@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String licenseKey = request.getHeader("X-License-Key");

if (licenseKey == null || licenseKey.isBlank()) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); response.getWriter().write("{\"error\": \"License key required\"}"); return; }

LicenseResult result = licenseService.validate(licenseKey, request.getServerName()).block();

if (result == null || !result.valid()) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json"); response.getWriter().write("{\"error\": \"Invalid license\"}"); return; }

request.setAttribute("license", result); chain.doFilter(request, response); }

@Override protected boolean shouldNotFilter(HttpServletRequest request) { // Skip validation for public endpoints String path = request.getRequestURI(); return path.startsWith("/public") || path.equals("/health"); } } ```

Step 4: Custom Annotation for Feature Gating

Create a custom annotation to protect specific endpoints:

// RequireLicense.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireLicense {
    String feature() default "";
}

// LicenseAspect.java @Aspect @Component public class LicenseAspect { @Around("@annotation(requireLicense)") public Object checkLicense(ProceedingJoinPoint joinPoint, RequireLicense requireLicense) throws Throwable { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); LicenseResult license = (LicenseResult) request.getAttribute("license");

if (license == null || !license.valid()) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Valid license required"); }

String feature = requireLicense.feature(); if (!feature.isEmpty() && !license.features().contains(feature)) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Feature not included in your plan"); }

return joinPoint.proceed(); } } ```

Use it on controller methods:

@RestController
@RequestMapping("/api")
public class ApiController {

@GetMapping("/data") @RequireLicense public ResponseEntity<Map<String, Object>> getData(HttpServletRequest request) { LicenseResult license = (LicenseResult) request.getAttribute("license"); return ResponseEntity.ok(Map.of("data", "premium content", "plan", license.key())); }

@GetMapping("/analytics") @RequireLicense(feature = "analytics") public ResponseEntity<Map<String, Object>> getAnalytics() { return ResponseEntity.ok(Map.of("analytics", Map.of("views", 12345))); } } ```

Step 5: Webhook Handler

Process real-time license events:

// WebhookController.java
@RestController
@RequestMapping("/webhooks")
public class WebhookController {

@Value("${to.webhook-secret}") private String webhookSecret;

@PostMapping("/license") public ResponseEntity<Map<String, Boolean>> handleWebhook( @RequestHeader("X-TO-Signature") String signature, @RequestHeader("X-TO-Timestamp") String timestamp, @RequestBody String body) {

// Reject stale webhooks long ts = Long.parseLong(timestamp); if (System.currentTimeMillis() / 1000 - ts > 300) { return ResponseEntity.status(401).body(Map.of("received", false)); }

// Verify HMAC String expected = HmacUtils.hmacSha256Hex(webhookSecret, timestamp + "." + body); if (!MessageDigest.isEqual(expected.getBytes(), signature.getBytes())) { return ResponseEntity.status(401).body(Map.of("received", false)); }

// Process event return ResponseEntity.ok(Map.of("received", true)); } } ```

Step 6: Configuration

Add configuration to application.yml:

# application.yml
to:
  base-url: https://api.trafficorchestrator.com/api/v1
  api-key: ${TO_API_KEY}
  webhook-secret: ${TO_WEBHOOK_SECRET}

Next Steps

Traffic Orchestrator's REST API is framework-agnostic — it works with Spring Boot, Quarkus, Micronaut, or any Java HTTP client. Edge validation completes in under 10ms, adding negligible latency to your API responses.

TOT
Traffic Orchestrator Team
Engineering

The engineering team behind Traffic Orchestrator, building enterprise-grade software licensing infrastructure used by developers worldwide.

Was this article helpful?
Get licensing insights delivered

Engineering deep-dives, security advisories, and product updates. Unsubscribe anytime.

Share this article
Free Plan Available

Ship licensing in your next release

5 licenses, 500 validations/month, full API access. Set up in under 5 minutes — no credit card required.

2-minute setup No credit card Cancel anytime