Paddle Billing represents the next generation of payment processing, offering a comprehensive platform for handling subscriptions, one-time payments, and complex billing scenarios. For Java developers, integrating with Paddle Billing provides a robust foundation for building scalable subscription-based applications with minimal compliance overhead. This guide explores practical patterns for integrating Paddle's modern billing API with Java applications.
Understanding Paddle Billing Architecture
Paddle Billing operates on a webhook-driven, API-first architecture:
- RESTful API for all billing operations
- Webhook events for real-time notifications
- Product and price management with flexible pricing models
- Subscription lifecycle management with automated billing
- Customer and business management with tax handling
Core Integration Patterns
1. Paddle API Client Implementation
Create a robust HTTP client to interact with Paddle Billing API.
Base Paddle Client Configuration:
@Configuration
public class PaddleBillingConfig {
@Value("${paddle.api.key}")
private String apiKey;
@Value("${paddle.environment:sandbox}")
private String environment;
@Bean
public WebClient paddleWebClient() {
String baseUrl = environment.equals("production")
? "https://api.paddle.com"
: "https://api.sandbox.paddle.com";
return WebClient.builder()
.baseUrl(baseUrl)
.defaultHeader("Authorization", "Bearer " + apiKey)
.defaultHeader("Content-Type", "application/json")
.build();
}
}
@Service
public class PaddleBillingClient {
private final WebClient webClient;
public PaddleBillingClient(WebClient paddleWebClient) {
this.webClient = paddleWebClient;
}
public Mono<Customer> createCustomer(CreateCustomerRequest request) {
return webClient.post()
.uri("/customers")
.bodyValue(request)
.retrieve()
.bodyToMono(Customer.class);
}
public Mono<Subscription> createSubscription(CreateSubscriptionRequest request) {
return webClient.post()
.uri("/subscriptions")
.bodyValue(request)
.retrieve()
.bodyToMono(Subscription.class);
}
public Mono<Transaction> getTransaction(String transactionId) {
return webClient.get()
.uri("/transactions/{id}", transactionId)
.retrieve()
.bodyToMono(Transaction.class);
}
}
2. Domain Models and DTOs
Create Java models that map to Paddle's API structures.
Core Domain Models:
@Data
public class CreateCustomerRequest {
private String email;
private String name;
private String locale;
private Map<String, Object> customData;
public CreateCustomerRequest(String email, String name) {
this.email = email;
this.name = name;
this.locale = "en";
this.customData = new HashMap<>();
}
}
@Data
public class Customer {
private String id;
private String email;
private String name;
private String locale;
private Instant createdAt;
private Instant updatedAt;
private Map<String, Object> customData;
private String status;
}
@Data
public class CreateSubscriptionRequest {
private String customerId;
private String addressId;
private String businessId;
private String currencyCode;
private List<SubscriptionItem> items;
private Map<String, Object> customData;
@Data
public static class SubscriptionItem {
private String priceId;
private Integer quantity;
}
}
@Data
public class Subscription {
private String id;
private String status;
private String customerId;
private String addressId;
private String businessId;
private String currencyCode;
private Instant createdAt;
private Instant updatedAt;
private Instant startedAt;
private Instant firstBilledAt;
private Instant nextBilledAt;
private Instant canceledAt;
private CollectionMode collectionMode;
private BillingDetails billingDetails;
private List<SubscriptionItem> items;
private Map<String, Object> customData;
public enum CollectionMode {
AUTOMATIC, MANUAL
}
}
3. Subscription Management Service
Implement business logic for subscription lifecycle management.
Subscription Service:
@Service
@Transactional
public class SubscriptionService {
private final PaddleBillingClient paddleClient;
private final CustomerRepository customerRepository;
private final SubscriptionRepository subscriptionRepository;
public SubscriptionService(PaddleBillingClient paddleClient,
CustomerRepository customerRepository,
SubscriptionRepository subscriptionRepository) {
this.paddleClient = paddleClient;
this.customerRepository = customerRepository;
this.subscriptionRepository = subscriptionRepository;
}
public Mono<Subscription> createSubscription(String email, String name, String priceId) {
// Check if customer exists or create new one
return findOrCreateCustomer(email, name)
.flatMap(customer -> {
CreateSubscriptionRequest request = new CreateSubscriptionRequest();
request.setCustomerId(customer.getId());
request.setCurrencyCode("USD");
request.setItems(List.of(
new CreateSubscriptionRequest.SubscriptionItem(priceId, 1)
));
request.setCollectionMode(CreateSubscriptionRequest.CollectionMode.AUTOMATIC);
return paddleClient.createSubscription(request);
})
.flatMap(subscription -> {
// Store subscription in local database
return subscriptionRepository.save(mapToEntity(subscription))
.map(this::mapToDomain);
});
}
public Mono<Customer> findOrCreateCustomer(String email, String name) {
return customerRepository.findByEmail(email)
.switchIfEmpty(
paddleClient.createCustomer(new CreateCustomerRequest(email, name))
.flatMap(customer -> customerRepository.save(mapToEntity(customer)))
)
.map(this::mapToDomain);
}
public Mono<Subscription> cancelSubscription(String subscriptionId) {
return webClient.patch()
.uri("/subscriptions/{id}", subscriptionId)
.bodyValue(Map.of("status", "canceled"))
.retrieve()
.bodyToMono(Subscription.class)
.flatMap(subscription ->
subscriptionRepository.updateStatus(subscriptionId, "canceled")
);
}
public Flux<Subscription> getActiveSubscriptions(String customerId) {
return subscriptionRepository.findByCustomerIdAndStatus(customerId, "active");
}
}
4. Webhook Handling for Real-time Events
Process Paddle webhooks to keep local state synchronized.
Webhook Controller and Security:
@RestController
@RequestMapping("/webhooks/paddle")
public class PaddleWebhookController {
private final SubscriptionService subscriptionService;
private final NotificationService notificationService;
@Value("${paddle.webhook.secret}")
private String webhookSecret;
@PostMapping
public ResponseEntity<String> handleWebhook(
@RequestBody String payload,
@RequestHeader("Paddle-Signature") String signature) {
// Verify webhook signature
if (!verifySignature(payload, signature)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
PaddleWebhookEvent event = parseWebhookPayload(payload);
processWebhookEvent(event).subscribe();
return ResponseEntity.accepted().build();
}
private Mono<Void> processWebhookEvent(PaddleWebhookEvent event) {
return Mono.fromRunnable(() -> {
switch (event.getEventType()) {
case "subscription.created":
handleSubscriptionCreated(event);
break;
case "subscription.activated":
handleSubscriptionActivated(event);
break;
case "subscription.canceled":
handleSubscriptionCanceled(event);
break;
case "transaction.completed":
handleTransactionCompleted(event);
break;
case "transaction.payment_failed":
handlePaymentFailed(event);
break;
}
});
}
private void handleSubscriptionActivated(PaddleWebhookEvent event) {
String subscriptionId = event.getData().getSubscriptionId();
String customerId = event.getData().getCustomerId();
subscriptionService.activateSubscription(subscriptionId)
.flatMap(subscription ->
notificationService.sendWelcomeEmail(customerId, subscription)
)
.subscribe();
}
private void handlePaymentFailed(PaddleWebhookEvent event) {
String customerId = event.getData().getCustomerId();
String subscriptionId = event.getData().getSubscriptionId();
subscriptionService.markPaymentFailed(subscriptionId)
.flatMap(subscription ->
notificationService.sendPaymentFailedEmail(customerId, subscription)
)
.subscribe();
}
private boolean verifySignature(String payload, String signature) {
// Implement Paddle webhook signature verification
// Refer to: https://developer.paddle.com/webhook-reference/verifying-webhooks
try {
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(webhookSecret.getBytes(), "HmacSHA256");
sha256Hmac.init(secretKey);
String computedSignature = Base64.getEncoder()
.encodeToString(sha256Hmac.doFinal(payload.getBytes()));
return computedSignature.equals(signature);
} catch (Exception e) {
return false;
}
}
}
5. Price and Product Management
Manage products and pricing plans.
Product Catalog Service:
@Service
public class ProductCatalogService {
private final PaddleBillingClient paddleClient;
public ProductCatalogService(PaddleBillingClient paddleClient) {
this.paddleClient = paddleClient;
}
public Flux<Product> getAvailableProducts() {
return webClient.get()
.uri("/products?status=active")
.retrieve()
.bodyToFlux(Product.class);
}
public Flux<Price> getProductPrices(String productId) {
return webClient.get()
.uri("/prices?product_id={productId}&status=active", productId)
.retrieve()
.bodyToFlux(Price.class);
}
public Mono<Price> createRecurringPrice(String productId, String description,
BigDecimal unitPrice, String interval) {
CreatePriceRequest request = new CreatePriceRequest();
request.setDescription(description);
request.setProductId(productId);
request.setUnitPrice(createMoney(unitPrice, "USD"));
request.setBillingCycle(new Interval(interval, 1));
return paddleClient.createPrice(request);
}
private Map<String, Object> createMoney(BigDecimal amount, String currencyCode) {
return Map.of(
"amount", amount.toString(),
"currency_code", currencyCode
);
}
}
6. Reporting and Analytics
Generate business insights from billing data.
Billing Analytics Service:
@Service
public class BillingAnalyticsService {
private final SubscriptionRepository subscriptionRepository;
private final TransactionRepository transactionRepository;
public BillingAnalyticsService(SubscriptionRepository subscriptionRepository,
TransactionRepository transactionRepository) {
this.subscriptionRepository = subscriptionRepository;
this.transactionRepository = transactionRepository;
}
public Mono<RevenueReport> getMonthlyRecurringRevenue(LocalDate from, LocalDate to) {
return subscriptionRepository.calculateMRR(from, to)
.map(mrr -> new RevenueReport("MRR", mrr, from, to));
}
public Mono<ChurnReport> getChurnRate(LocalDate from, LocalDate to) {
return subscriptionRepository.calculateChurnRate(from, to)
.map(churnRate -> new ChurnReport(churnRate, from, to));
}
public Flux<SubscriptionMetrics> getSubscriptionMetrics() {
return subscriptionRepository.getActiveSubscriptionsCount()
.zipWith(subscriptionRepository.getTrialSubscriptionsCount())
.flatMapMany(tuple ->
Flux.just(new SubscriptionMetrics(tuple.getT1(), tuple.getT2()))
);
}
}
Best Practices for Paddle Billing Integration
- Idempotency Keys: Use idempotency keys for all POST requests to prevent duplicate operations
- Error Handling: Implement comprehensive error handling for API failures
- Webhook Security: Always verify webhook signatures to prevent spoofing
- Data Consistency: Maintain local database state synchronized with Paddle via webhooks
- Rate Limiting: Respect Paddle's rate limits with proper backoff strategies
- Testing: Use Paddle's sandbox environment for comprehensive testing
- Monitoring: Implement health checks and monitoring for billing operations
Conclusion: Streamlined Subscription Management
Integrating Paddle Billing with Java applications provides a robust foundation for building modern subscription-based businesses. By leveraging Paddle's comprehensive billing platform and Java's enterprise capabilities, developers can focus on building core product features while outsourcing complex billing operations.
This integration demonstrates that modern subscription management doesn't require building complex billing infrastructure—it's about creating seamless connections between your Java application and specialized billing platforms like Paddle, enabling you to scale with confidence while maintaining compliance and reducing operational overhead.