Chargebee Subscription Management in Java: Complete Integration Guide

Chargebee is a recurring billing and subscription management platform. This guide covers complete integration with Chargebee's subscription system using Java.


Setup and Dependencies

Maven Dependencies
<properties>
<chargebee.version>2.9.4</chargebee.version>
<spring-boot.version>3.1.0</spring-boot.version>
</properties>
<dependencies>
<!-- Chargebee Java SDK -->
<dependency>
<groupId>com.chargebee</groupId>
<artifactId>chargebee-java</artifactId>
<version>${chargebee.version}</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
Configuration
@Configuration
public class ChargebeeConfig {
@Value("${chargebee.site:your-site}")
private String chargebeeSite;
@Value("${chargebee.api-key:your-api-key}")
private String chargebeeApiKey;
@PostConstruct
public void init() {
// Configure Chargebee environment
Chargebee.configure(chargebeeSite, chargebeeApiKey);
}
}
# application.yml
chargebee:
site: your-site-test
api-key: test_xxxxxxxxxxxxxxxxxxxxxxxx
webhook-secret: your-webhook-secret
app:
base-url: https://yourapp.com

Domain Models

1. Subscription Plans Enum
public enum SubscriptionPlan {
STARTER("starter-monthly", "Starter Monthly", "month"),
PROFESSIONAL("professional-monthly", "Professional Monthly", "month"),
ENTERPRISE("enterprise-yearly", "Enterprise Yearly", "year"),
STARTER_YEARLY("starter-yearly", "Starter Yearly", "year");
private final String id;
private final String name;
private final String billingPeriod;
SubscriptionPlan(String id, String name, String billingPeriod) {
this.id = id;
this.name = name;
this.billingPeriod = billingPeriod;
}
// Getters
public String getId() { return id; }
public String getName() { return name; }
public String getBillingPeriod() { return billingPeriod; }
public static SubscriptionPlan fromId(String id) {
return Arrays.stream(values())
.filter(plan -> plan.getId().equals(id))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unknown plan: " + id));
}
}
2. JPA Entities
@Entity
@Table(name = "customers")
public class Customer {
@Id
private String id;
@Column(nullable = false)
private String email;
private String firstName;
private String lastName;
private String phone;
@Column(name = "chargebee_id")
private String chargebeeId;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
private List<Subscription> subscriptions = new ArrayList<>();
// constructors, getters, setters
}
@Entity
@Table(name = "subscriptions")
public class Subscription {
@Id
private String id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
@Enumerated(EnumType.STRING)
@Column(name = "plan_id")
private SubscriptionPlan plan;
@Column(name = "chargebee_id")
private String chargebeeId;
@Enumerated(EnumType.STRING)
private SubscriptionStatus status;
@Column(name = "current_term_start")
private LocalDateTime currentTermStart;
@Column(name = "current_term_end")
private LocalDateTime currentTermEnd;
@Column(name = "trial_end")
private LocalDateTime trialEnd;
private boolean hasScheduledChanges;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
// constructors, getters, setters
}
public enum SubscriptionStatus {
ACTIVE,
CANCELLED,
FUTURE,
IN_TRIAL,
NON_RENEWING,
PAUSED
}

Core Service Implementation

1. Customer Management Service
@Service
@Transactional
public class CustomerService {
private static final Logger logger = LoggerFactory.getLogger(CustomerService.class);
private final CustomerRepository customerRepository;
public CustomerService(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
public Customer createCustomer(CreateCustomerRequest request) {
try {
// Create customer in Chargebee
Result result = Chargebee.getInstance()
.customer()
.create()
.firstName(request.getFirstName())
.lastName(request.getLastName())
.email(request.getEmail())
.phone(request.getPhone())
.request();
com.chargebee.models.Customer chargebeeCustomer = result.customer();
// Save to local database
Customer customer = new Customer();
customer.setId(UUID.randomUUID().toString());
customer.setEmail(chargebeeCustomer.email());
customer.setFirstName(chargebeeCustomer.firstName());
customer.setLastName(chargebeeCustomer.lastName());
customer.setPhone(chargebeeCustomer.phone());
customer.setChargebeeId(chargebeeCustomer.id());
Customer savedCustomer = customerRepository.save(customer);
logger.info("Created customer: {} with Chargebee ID: {}", 
savedCustomer.getId(), chargebeeCustomer.id());
return savedCustomer;
} catch (Exception e) {
logger.error("Failed to create customer: {}", e.getMessage(), e);
throw new SubscriptionException("Failed to create customer", e);
}
}
public Customer getCustomer(String customerId) {
return customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException("Customer not found: " + customerId));
}
public Customer getCustomerByChargebeeId(String chargebeeId) {
return customerRepository.findByChargebeeId(chargebeeId)
.orElseThrow(() -> new CustomerNotFoundException("Customer not found with Chargebee ID: " + chargebeeId));
}
public Customer updateCustomer(String customerId, UpdateCustomerRequest request) {
Customer customer = getCustomer(customerId);
try {
// Update customer in Chargebee
Result result = Chargebee.getInstance()
.customer()
.update(customer.getChargebeeId())
.firstName(request.getFirstName())
.lastName(request.getLastName())
.phone(request.getPhone())
.request();
com.chargebee.models.Customer chargebeeCustomer = result.customer();
// Update local database
customer.setFirstName(chargebeeCustomer.firstName());
customer.setLastName(chargebeeCustomer.lastName());
customer.setPhone(chargebeeCustomer.phone());
return customerRepository.save(customer);
} catch (Exception e) {
logger.error("Failed to update customer {}: {}", customerId, e.getMessage(), e);
throw new SubscriptionException("Failed to update customer", e);
}
}
}
2. Subscription Management Service
@Service
@Transactional
public class SubscriptionService {
private static final Logger logger = LoggerFactory.getLogger(SubscriptionService.class);
private final SubscriptionRepository subscriptionRepository;
private final CustomerRepository customerRepository;
public SubscriptionService(SubscriptionRepository subscriptionRepository, 
CustomerRepository customerRepository) {
this.subscriptionRepository = subscriptionRepository;
this.customerRepository = customerRepository;
}
public Subscription createSubscription(CreateSubscriptionRequest request) {
Customer customer = customerRepository.findById(request.getCustomerId())
.orElseThrow(() -> new CustomerNotFoundException("Customer not found: " + request.getCustomerId()));
try {
// Create subscription in Chargebee
SubscriptionRequest subscriptionRequest = Chargebee.getInstance()
.subscription()
.create()
.customerId(customer.getChargebeeId())
.planId(request.getPlanId())
.billingCycles(request.getBillingCycles())
.couponIds(request.getCouponIds());
// Add trial end if specified
if (request.getTrialEnd() != null) {
subscriptionRequest.trialEnd(Instant.ofEpochSecond(request.getTrialEnd()).getEpochSecond());
}
// Add addons if any
if (request.getAddons() != null) {
for (AddonRequest addon : request.getAddons()) {
subscriptionRequest.addon(addon.getId(), addon.getQuantity());
}
}
Result result = subscriptionRequest.request();
com.chargebee.models.Subscription chargebeeSubscription = result.subscription();
com.chargebee.models.Customer chargebeeCustomer = result.customer();
// Save to local database
Subscription subscription = new Subscription();
subscription.setId(UUID.randomUUID().toString());
subscription.setCustomer(customer);
subscription.setPlan(SubscriptionPlan.fromId(chargebeeSubscription.planId()));
subscription.setChargebeeId(chargebeeSubscription.id());
subscription.setStatus(SubscriptionStatus.valueOf(chargebeeSubscription.status().name()));
subscription.setCurrentTermStart(toLocalDateTime(chargebeeSubscription.currentTermStart()));
subscription.setCurrentTermEnd(toLocalDateTime(chargebeeSubscription.currentTermEnd()));
subscription.setTrialEnd(toLocalDateTime(chargebeeSubscription.trialEnd()));
subscription.setHasScheduledChanges(chargebeeSubscription.hasScheduledChanges());
Subscription savedSubscription = subscriptionRepository.save(subscription);
logger.info("Created subscription: {} for customer: {}", 
savedSubscription.getId(), customer.getId());
return savedSubscription;
} catch (Exception e) {
logger.error("Failed to create subscription for customer {}: {}", 
customer.getId(), e.getMessage(), e);
throw new SubscriptionException("Failed to create subscription", e);
}
}
public Subscription getSubscription(String subscriptionId) {
return subscriptionRepository.findById(subscriptionId)
.orElseThrow(() -> new SubscriptionNotFoundException("Subscription not found: " + subscriptionId));
}
public List<Subscription> getCustomerSubscriptions(String customerId) {
return subscriptionRepository.findByCustomerId(customerId);
}
public Subscription changePlan(String subscriptionId, ChangePlanRequest request) {
Subscription subscription = getSubscription(subscriptionId);
try {
// Update subscription in Chargebee
Result result = Chargebee.getInstance()
.subscription()
.update(subscription.getChargebeeId())
.planId(request.getNewPlanId())
.billingCycles(request.getBillingCycles())
.prorate(request.isProrate())
.invoiceImmediately(request.isInvoiceImmediately())
.request();
com.chargebee.models.Subscription chargebeeSubscription = result.subscription();
// Update local database
subscription.setPlan(SubscriptionPlan.fromId(chargebeeSubscription.planId()));
subscription.setCurrentTermStart(toLocalDateTime(chargebeeSubscription.currentTermStart()));
subscription.setCurrentTermEnd(toLocalDateTime(chargebeeSubscription.currentTermEnd()));
subscription.setHasScheduledChanges(chargebeeSubscription.hasScheduledChanges());
return subscriptionRepository.save(subscription);
} catch (Exception e) {
logger.error("Failed to change plan for subscription {}: {}", 
subscriptionId, e.getMessage(), e);
throw new SubscriptionException("Failed to change subscription plan", e);
}
}
public Subscription cancelSubscription(String subscriptionId, CancelSubscriptionRequest request) {
Subscription subscription = getSubscription(subscriptionId);
try {
// Cancel subscription in Chargebee
SubscriptionRequest cancelRequest = Chargebee.getInstance()
.subscription()
.cancel(subscription.getChargebeeId())
.endOfTerm(request.isEndOfTerm());
if (request.getCreditOptionForCurrentTermCharges() != null) {
cancelRequest.creditOptionForCurrentTermCharges(
request.getCreditOptionForCurrentTermCharges());
}
if (request.getUnbilledChargesOption() != null) {
cancelRequest.unbilledChargesOption(request.getUnbilledChargesOption());
}
Result result = cancelRequest.request();
com.chargebee.models.Subscription chargebeeSubscription = result.subscription();
// Update local database
subscription.setStatus(SubscriptionStatus.valueOf(chargebeeSubscription.status().name()));
subscription.setHasScheduledChanges(chargebeeSubscription.hasScheduledChanges());
return subscriptionRepository.save(subscription);
} catch (Exception e) {
logger.error("Failed to cancel subscription {}: {}", subscriptionId, e.getMessage(), e);
throw new SubscriptionException("Failed to cancel subscription", e);
}
}
public Subscription reactivateSubscription(String subscriptionId) {
Subscription subscription = getSubscription(subscriptionId);
try {
// Reactivate subscription in Chargebee
Result result = Chargebee.getInstance()
.subscription()
.reactivate(subscription.getChargebeeId())
.request();
com.chargebee.models.Subscription chargebeeSubscription = result.subscription();
// Update local database
subscription.setStatus(SubscriptionStatus.valueOf(chargebeeSubscription.status().name()));
subscription.setHasScheduledChanges(chargebeeSubscription.hasScheduledChanges());
return subscriptionRepository.save(subscription);
} catch (Exception e) {
logger.error("Failed to reactivate subscription {}: {}", subscriptionId, e.getMessage(), e);
throw new SubscriptionException("Failed to reactivate subscription", e);
}
}
public Estimate estimateSubscription(EstimateRequest request) {
try {
EstimateRequest estimateRequest = Chargebee.getInstance()
.estimate()
.createSubscription()
.planId(request.getPlanId())
.customerEmail(request.getCustomerEmail());
if (request.getCustomerId() != null) {
estimateRequest.customerId(request.getCustomerId());
}
if (request.getCouponIds() != null) {
estimateRequest.couponIds(request.getCouponIds());
}
if (request.getAddons() != null) {
for (AddonRequest addon : request.getAddons()) {
estimateRequest.addon(addon.getId(), addon.getQuantity());
}
}
Result result = estimateRequest.request();
com.chargebee.models.Estimate chargebeeEstimate = result.estimate();
return convertToEstimate(chargebeeEstimate);
} catch (Exception e) {
logger.error("Failed to create estimate: {}", e.getMessage(), e);
throw new SubscriptionException("Failed to create estimate", e);
}
}
private LocalDateTime toLocalDateTime(Long epochSeconds) {
if (epochSeconds == null) return null;
return LocalDateTime.ofInstant(Instant.ofEpochSecond(epochSeconds), ZoneOffset.UTC);
}
private Estimate convertToEstimate(com.chargebee.models.Estimate chargebeeEstimate) {
Estimate estimate = new Estimate();
estimate.setAmount(chargebeeEstimate.estimate().total());
estimate.setCurrencyCode(chargebeeEstimate.estimate().currencyCode());
estimate.setSubTotal(chargebeeEstimate.estimate().subTotal());
estimate.setTax(chargebeeEstimate.estimate().tax());
return estimate;
}
}
3. Payment Method Service
@Service
@Transactional
public class PaymentMethodService {
private static final Logger logger = LoggerFactory.getLogger(PaymentMethodService.class);
private final CustomerRepository customerRepository;
public PaymentMethodService(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
public PaymentMethod addPaymentMethod(String customerId, AddPaymentMethodRequest request) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException("Customer not found: " + customerId));
try {
// Create payment method in Chargebee
Result result = Chargebee.getInstance()
.paymentSource()
.createCard()
.customerId(customer.getChargebeeId())
.cardNumber(request.getCardNumber())
.expiryMonth(request.getExpiryMonth())
.expiryYear(request.getExpiryYear())
.cvv(request.getCvv())
.request();
com.chargebee.models.PaymentSource paymentSource = result.paymentSource();
logger.info("Added payment method for customer: {}", customerId);
return convertToPaymentMethod(paymentSource);
} catch (Exception e) {
logger.error("Failed to add payment method for customer {}: {}", customerId, e.getMessage(), e);
throw new SubscriptionException("Failed to add payment method", e);
}
}
public List<PaymentMethod> getCustomerPaymentMethods(String customerId) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException("Customer not found: " + customerId));
try {
Result result = Chargebee.getInstance()
.paymentSource()
.list()
.customerIdIs(customer.getChargebeeId())
.request();
List<PaymentMethod> paymentMethods = new ArrayList<>();
for (com.chargebee.models.PaymentSource paymentSource : result.paymentSources()) {
paymentMethods.add(convertToPaymentMethod(paymentSource));
}
return paymentMethods;
} catch (Exception e) {
logger.error("Failed to get payment methods for customer {}: {}", customerId, e.getMessage(), e);
throw new SubscriptionException("Failed to get payment methods", e);
}
}
public void removePaymentMethod(String customerId, String paymentMethodId) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException("Customer not found: " + customerId));
try {
Chargebee.getInstance()
.paymentSource()
.delete(paymentMethodId)
.request();
logger.info("Removed payment method: {} for customer: {}", paymentMethodId, customerId);
} catch (Exception e) {
logger.error("Failed to remove payment method {} for customer {}: {}", 
paymentMethodId, customerId, e.getMessage(), e);
throw new SubscriptionException("Failed to remove payment method", e);
}
}
private PaymentMethod convertToPaymentMethod(com.chargebee.models.PaymentSource paymentSource) {
PaymentMethod method = new PaymentMethod();
method.setId(paymentSource.id());
method.setType(paymentSource.type().name());
method.setStatus(paymentSource.status().name());
if (paymentSource.type() == com.chargebee.models.PaymentSource.Type.CARD) {
method.setLast4(paymentSource.card().last4());
method.setBrand(paymentSource.card().brand());
method.setExpiryMonth(paymentSource.card().expiryMonth());
method.setExpiryYear(paymentSource.card().expiryYear());
}
return method;
}
}

REST Controllers

1. Subscription Controller
@RestController
@RequestMapping("/api/subscriptions")
@Validated
public class SubscriptionController {
private final SubscriptionService subscriptionService;
public SubscriptionController(SubscriptionService subscriptionService) {
this.subscriptionService = subscriptionService;
}
@PostMapping
public ResponseEntity<Subscription> createSubscription(
@Valid @RequestBody CreateSubscriptionRequest request) {
Subscription subscription = subscriptionService.createSubscription(request);
return ResponseEntity.status(HttpStatus.CREATED).body(subscription);
}
@GetMapping("/{subscriptionId}")
public ResponseEntity<Subscription> getSubscription(@PathVariable String subscriptionId) {
Subscription subscription = subscriptionService.getSubscription(subscriptionId);
return ResponseEntity.ok(subscription);
}
@GetMapping("/customer/{customerId}")
public ResponseEntity<List<Subscription>> getCustomerSubscriptions(
@PathVariable String customerId) {
List<Subscription> subscriptions = subscriptionService.getCustomerSubscriptions(customerId);
return ResponseEntity.ok(subscriptions);
}
@PutMapping("/{subscriptionId}/plan")
public ResponseEntity<Subscription> changePlan(
@PathVariable String subscriptionId,
@Valid @RequestBody ChangePlanRequest request) {
Subscription subscription = subscriptionService.changePlan(subscriptionId, request);
return ResponseEntity.ok(subscription);
}
@PostMapping("/{subscriptionId}/cancel")
public ResponseEntity<Subscription> cancelSubscription(
@PathVariable String subscriptionId,
@Valid @RequestBody CancelSubscriptionRequest request) {
Subscription subscription = subscriptionService.cancelSubscription(subscriptionId, request);
return ResponseEntity.ok(subscription);
}
@PostMapping("/{subscriptionId}/reactivate")
public ResponseEntity<Subscription> reactivateSubscription(
@PathVariable String subscriptionId) {
Subscription subscription = subscriptionService.reactivateSubscription(subscriptionId);
return ResponseEntity.ok(subscription);
}
@PostMapping("/estimate")
public ResponseEntity<Estimate> estimateSubscription(
@Valid @RequestBody EstimateRequest request) {
Estimate estimate = subscriptionService.estimateSubscription(request);
return ResponseEntity.ok(estimate);
}
}
2. Customer Controller
@RestController
@RequestMapping("/api/customers")
@Validated
public class CustomerController {
private final CustomerService customerService;
private final PaymentMethodService paymentMethodService;
public CustomerController(CustomerService customerService, 
PaymentMethodService paymentMethodService) {
this.customerService = customerService;
this.paymentMethodService = paymentMethodService;
}
@PostMapping
public ResponseEntity<Customer> createCustomer(
@Valid @RequestBody CreateCustomerRequest request) {
Customer customer = customerService.createCustomer(request);
return ResponseEntity.status(HttpStatus.CREATED).body(customer);
}
@GetMapping("/{customerId}")
public ResponseEntity<Customer> getCustomer(@PathVariable String customerId) {
Customer customer = customerService.getCustomer(customerId);
return ResponseEntity.ok(customer);
}
@PutMapping("/{customerId}")
public ResponseEntity<Customer> updateCustomer(
@PathVariable String customerId,
@Valid @RequestBody UpdateCustomerRequest request) {
Customer customer = customerService.updateCustomer(customerId, request);
return ResponseEntity.ok(customer);
}
@PostMapping("/{customerId}/payment-methods")
public ResponseEntity<PaymentMethod> addPaymentMethod(
@PathVariable String customerId,
@Valid @RequestBody AddPaymentMethodRequest request) {
PaymentMethod method = paymentMethodService.addPaymentMethod(customerId, request);
return ResponseEntity.status(HttpStatus.CREATED).body(method);
}
@GetMapping("/{customerId}/payment-methods")
public ResponseEntity<List<PaymentMethod>> getPaymentMethods(
@PathVariable String customerId) {
List<PaymentMethod> methods = paymentMethodService.getCustomerPaymentMethods(customerId);
return ResponseEntity.ok(methods);
}
@DeleteMapping("/{customerId}/payment-methods/{paymentMethodId}")
public ResponseEntity<Void> removePaymentMethod(
@PathVariable String customerId,
@PathVariable String paymentMethodId) {
paymentMethodService.removePaymentMethod(customerId, paymentMethodId);
return ResponseEntity.noContent().build();
}
}

Webhook Handler

@Service
public class ChargebeeWebhookService {
private static final Logger logger = LoggerFactory.getLogger(ChargebeeWebhookService.class);
private final SubscriptionRepository subscriptionRepository;
private final CustomerRepository customerRepository;
public ChargebeeWebhookService(SubscriptionRepository subscriptionRepository,
CustomerRepository customerRepository) {
this.subscriptionRepository = subscriptionRepository;
this.customerRepository = customerRepository;
}
public void handleWebhook(ChargebeeEvent event) {
String eventType = event.eventType();
String content = event.content();
logger.info("Processing Chargebee webhook: {}", eventType);
try {
switch (eventType) {
case "subscription_created":
case "subscription_updated":
handleSubscriptionEvent(content);
break;
case "subscription_cancelled":
handleSubscriptionCancelled(content);
break;
case "subscription_reactivated":
handleSubscriptionReactivated(content);
break;
case "payment_succeeded":
handlePaymentSucceeded(content);
break;
case "payment_failed":
handlePaymentFailed(content);
break;
case "invoice_generated":
handleInvoiceGenerated(content);
break;
default:
logger.debug("Unhandled webhook event type: {}", eventType);
}
} catch (Exception e) {
logger.error("Failed to process webhook event {}: {}", eventType, e.getMessage(), e);
throw new WebhookProcessingException("Failed to process webhook", e);
}
}
private void handleSubscriptionEvent(String content) throws Exception {
Result result = Chargebee.getInstance().eventDeserialize(content);
com.chargebee.models.Subscription chargebeeSubscription = result.subscription();
// Update local subscription
subscriptionRepository.findByChargebeeId(chargebeeSubscription.id())
.ifPresent(subscription -> {
subscription.setStatus(SubscriptionStatus.valueOf(chargebeeSubscription.status().name()));
subscription.setCurrentTermStart(toLocalDateTime(chargebeeSubscription.currentTermStart()));
subscription.setCurrentTermEnd(toLocalDateTime(chargebeeSubscription.currentTermEnd()));
subscription.setTrialEnd(toLocalDateTime(chargebeeSubscription.trialEnd()));
subscription.setHasScheduledChanges(chargebeeSubscription.hasScheduledChanges());
subscriptionRepository.save(subscription);
logger.info("Updated subscription from webhook: {}", subscription.getId());
});
}
private void handleSubscriptionCancelled(String content) throws Exception {
Result result = Chargebee.getInstance().eventDeserialize(content);
com.chargebee.models.Subscription chargebeeSubscription = result.subscription();
subscriptionRepository.findByChargebeeId(chargebeeSubscription.id())
.ifPresent(subscription -> {
subscription.setStatus(SubscriptionStatus.CANCELLED);
subscriptionRepository.save(subscription);
logger.info("Cancelled subscription from webhook: {}", subscription.getId());
});
}
private void handlePaymentFailed(String content) throws Exception {
Result result = Chargebee.getInstance().eventDeserialize(content);
com.chargebee.models.Transaction transaction = result.transaction();
// Notify customer about payment failure
// Implement your notification logic here
logger.warn("Payment failed for transaction: {}", transaction.id());
}
// Other webhook handlers...
}
@RestController
@RequestMapping("/webhooks/chargebee")
public class ChargebeeWebhookController {
private final ChargebeeWebhookService webhookService;
public ChargebeeWebhookController(ChargebeeWebhookService webhookService) {
this.webhookService = webhookService;
}
@PostMapping
public ResponseEntity<String> handleWebhook(
@RequestBody String payload,
@RequestHeader("chargebee-signature") String signature) {
// Verify webhook signature (implementation depends on your security setup)
if (!isValidSignature(payload, signature)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
try {
ChargebeeEvent event = ChargebeeEvent.create(payload);
webhookService.handleWebhook(event);
return ResponseEntity.ok("Webhook processed successfully");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to process webhook");
}
}
private boolean isValidSignature(String payload, String signature) {
// Implement signature verification based on Chargebee docs
return true; // Simplified for example
}
}

Exception Handling

@ControllerAdvice
public class SubscriptionExceptionHandler {
@ExceptionHandler(SubscriptionException.class)
public ResponseEntity<ErrorResponse> handleSubscriptionException(SubscriptionException ex) {
ErrorResponse error = new ErrorResponse("SUBSCRIPTION_ERROR", ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
@ExceptionHandler(CustomerNotFoundException.class)
public ResponseEntity<ErrorResponse> handleCustomerNotFound(CustomerNotFoundException ex) {
ErrorResponse error = new ErrorResponse("CUSTOMER_NOT_FOUND", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(SubscriptionNotFoundException.class)
public ResponseEntity<ErrorResponse> handleSubscriptionNotFound(SubscriptionNotFoundException ex) {
ErrorResponse error = new ErrorResponse("SUBSCRIPTION_NOT_FOUND", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
public class SubscriptionException extends RuntimeException {
public SubscriptionException(String message) {
super(message);
}
public SubscriptionException(String message, Throwable cause) {
super(message, cause);
}
}
public class CustomerNotFoundException extends RuntimeException {
public CustomerNotFoundException(String message) {
super(message);
}
}

Testing

1. Unit Tests
@ExtendWith(MockitoExtension.class)
class SubscriptionServiceTest {
@Mock
private SubscriptionRepository subscriptionRepository;
@Mock
private CustomerRepository customerRepository;
@InjectMocks
private SubscriptionService subscriptionService;
@Test
void shouldCreateSubscription() {
// Given
CreateSubscriptionRequest request = new CreateSubscriptionRequest();
request.setCustomerId("customer-123");
request.setPlanId("professional-monthly");
Customer customer = new Customer();
customer.setId("customer-123");
customer.setChargebeeId("cb-customer-123");
when(customerRepository.findById("customer-123")).thenReturn(Optional.of(customer));
// When/Then
assertThatThrownBy(() -> subscriptionService.createSubscription(request))
.isInstanceOf(SubscriptionException.class);
}
}
2. Integration Test
@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.properties")
class SubscriptionIntegrationTest {
@Autowired
private SubscriptionService subscriptionService;
@Autowired
private CustomerService customerService;
@Test
@Disabled("For actual Chargebee integration testing")
void shouldCreateSubscriptionInChargebee() {
// This test would actually call Chargebee test environment
// Implement with test customer and plan setup
}
}

Best Practices

  1. Use Test Environment: Always use Chargebee test site for development
  2. Idempotency: Implement idempotent operations for webhooks
  3. Error Handling: Comprehensive error handling for Chargebee API calls
  4. Webhook Security: Always verify webhook signatures
  5. Data Consistency: Keep local database in sync with Chargebee state
  6. Monitoring: Log all Chargebee interactions for debugging
@Component
public class ChargebeeHealthCheck {
@Scheduled(fixedRate = 300000) // 5 minutes
public void checkChargebeeConnectivity() {
try {
Result result = Chargebee.getInstance()
.plan()
.list()
.limit(1)
.request();
logger.debug("Chargebee connectivity check passed");
} catch (Exception e) {
logger.error("Chargebee connectivity check failed: {}", e.getMessage());
// Alert monitoring system
}
}
}

Conclusion

This comprehensive Chargebee integration provides:

  • Complete subscription lifecycle management
  • Customer and payment method management
  • Webhook handling for real-time updates
  • Robust error handling and monitoring
  • Scalable architecture for production use

The implementation follows best practices for SaaS subscription management and can be extended with additional features like metered billing, coupon management, and advanced analytics based on your business requirements.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper