RevenueCat is a powerful subscription and in-app purchase infrastructure that simplifies monetization across iOS, Android, and web platforms. For Java developers building Android applications or backend services, RevenueCat provides a unified API to manage subscriptions, validate receipts, and analyze revenue data. Let's explore how to integrate RevenueCat into Java-based mobile ecosystems.
Why RevenueCat for Java Mobile Development?
Benefits for Java developers:
- Cross-platform Consistency - Unified subscription logic across iOS and Android
- Server-side Validation - Secure receipt validation from Java backend
- Real-time Analytics - Live subscription status and revenue tracking
- Scalability - Handles millions of users with proven infrastructure
- Reduced Complexity - Abstracts away platform-specific purchase flows
Android Integration
1. Dependencies Setup
// app/build.gradle dependencies { implementation 'com.revenuecat.purchases:purchases:5.10.0' implementation 'com.android.billingclient:billing:6.0.1' implementation 'com.squareup.okhttp3:okhttp:4.11.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' } 2. RevenueCat Configuration Service
@Service public class RevenueCatService { private final Purchases purchases; private final ObjectMapper objectMapper; @Inject public RevenueCatService(Application context) { this.objectMapper = new ObjectMapper(); configureRevenueCat(context); this.purchases = Purchases.getSharedInstance(); } private void configureRevenueCat(Context context) { PurchasesConfiguration.Builder config = new PurchasesConfiguration.Builder(context, "your_public_api_key") .enableAdServicesTokenCollection(true) .observerMode(false); Purchases.configure(config.build()); // Set up listeners purchases.setUpdatedCustomerInfoListener(this::onCustomerInfoUpdated); purchases.setLogHandler(this::onRevenueCatLog); } // Purchase products public CompletableFuture<PurchaseResult> purchaseProduct(Activity activity, String productId) { CompletableFuture<PurchaseResult> future = new CompletableFuture<>(); purchases.getOfferings(new ReceiveOfferingsCallback() { @Override public void onReceived(@NonNull Offerings offerings) { Package packageToPurchase = offerings.getCurrent().getPackage(productId); if (packageToPurchase != null) { makePurchase(activity, packageToPurchase, future); } else { future.completeExceptionally(new ProductNotFoundException("Product not found: " + productId)); } } @Override public void onError(@NonNull PurchasesError error) { future.completeExceptionally(new RevenueCatException("Failed to get offerings: " + error.getMessage())); } }); return future; } private void makePurchase(Activity activity, Package packageToPurchase, CompletableFuture<PurchaseResult> future) { purchases.purchasePackage(activity, packageToPurchase, new MakePurchaseCallback() { @Override public void onCompleted(@NonNull StoreTransaction storeTransaction, @NonNull CustomerInfo customerInfo) { PurchaseResult result = new PurchaseResult( true, "Purchase successful", customerInfo, storeTransaction ); future.complete(result); } @Override public void onError(@NonNull PurchasesError error, Boolean userCancelled) { PurchaseResult result = new PurchaseResult( false, error.getMessage(), null, null ); future.complete(result); } }); } // Restore purchases public CompletableFuture<CustomerInfo> restorePurchases() { CompletableFuture<CustomerInfo> future = new CompletableFuture<>(); purchases.restorePurchases(new ReceiveCustomerInfoCallback() { @Override public void onReceived(@NonNull CustomerInfo customerInfo) { future.complete(customerInfo); } @Override public void onError(@NonNull PurchasesError error) { future.completeExceptionally(new RevenueCatException("Restore failed: " + error.getMessage())); } }); return future; } // Get customer info public CompletableFuture<CustomerInfo> getCustomerInfo() { CompletableFuture<CustomerInfo> future = new CompletableFuture<>(); purchases.getCustomerInfo(new ReceiveCustomerInfoCallback() { @Override public void onReceived(@NonNull CustomerInfo customerInfo) { future.complete(customerInfo); } @Override public void onError(@NonNull PurchasesError error) { future.completeExceptionally(new RevenueCatException("Failed to get customer info: " + error.getMessage())); } }); return future; } // Get available products public CompletableFuture<List<RevenueCatProduct>> getProducts() { CompletableFuture<List<RevenueCatProduct>> future = new CompletableFuture<>(); purchases.getOfferings(new ReceiveOfferingsCallback() { @Override public void onReceived(@NonNull Offerings offerings) { List<RevenueCatProduct> products = new ArrayList<>(); Offering currentOffering = offerings.getCurrent(); if (currentOffering != null) { for (Package pkg : currentOffering.getAvailablePackages()) { RevenueCatProduct product = new RevenueCatProduct( pkg.getIdentifier(), pkg.getProduct().getTitle(), pkg.getProduct().getDescription(), pkg.getProduct().getPrice(), pkg.getProduct().getPriceCurrencyCode(), pkg.getPackageType().name() ); products.add(product); } } future.complete(products); } @Override public void onError(@NonNull PurchasesError error) { future.completeExceptionally(new RevenueCatException("Failed to get products: " + error.getMessage())); } }); return future; } // Check subscription status public CompletableFuture<SubscriptionStatus> getSubscriptionStatus() { return getCustomerInfo().thenApply(customerInfo -> { EntitlementInfos entitlements = customerInfo.getEntitlements(); for (EntitlementInfo entitlement : entitlements.getAll().values()) { if (entitlement.isActive()) { return new SubscriptionStatus( true, entitlement.getIdentifier(), entitlement.getPeriodType(), entitlement.getLatestPurchaseDate(), entitlement.getExpirationDate(), entitlement.getStore() ); } } return new SubscriptionStatus(false, null, null, null, null, null); }); } // Event listeners private void onCustomerInfoUpdated(@NonNull CustomerInfo customerInfo) { // Handle customer info updates (subscription changes, etc.) Log.d("RevenueCat", "Customer info updated: " + customerInfo.getOriginalAppUserId()); // Notify subscribers EventBus.getDefault().post(new CustomerInfoUpdatedEvent(customerInfo)); } private void onRevenueCatLog(LogHandler.Loggable loggable) { Log.d("RevenueCat", loggable.getMessage()); } // Data models @Data public static class PurchaseResult { private final boolean success; private final String message; private final CustomerInfo customerInfo; private final StoreTransaction transaction; } @Data public static class RevenueCatProduct { private final String id; private final String title; private final String description; private final String price; private final String currency; private final String packageType; } @Data public static class SubscriptionStatus { private final boolean isActive; private final String entitlement; private final String periodType; private final Date latestPurchaseDate; private final Date expirationDate; private final String store; } } Backend Server Integration
1. RevenueCat Webhook Handler
@RestController @RequestMapping("/api/revenuecat") @Slf4j public class RevenueCatWebhookController { private final RevenueCatWebhookService webhookService; private final SubscriptionService subscriptionService; private final ObjectMapper objectMapper; public RevenueCatWebhookController(RevenueCatWebhookService webhookService, SubscriptionService subscriptionService) { this.webhookService = webhookService; this.subscriptionService = subscriptionService; this.objectMapper = new ObjectMapper(); this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } @PostMapping("/webhook") public ResponseEntity<String> handleWebhook( @RequestBody String payload, @RequestHeader("RevenueCat-Signature") String signature) { try { // Verify webhook signature if (!webhookService.verifySignature(payload, signature)) { log.warn("Invalid webhook signature"); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } RevenueCatWebhookEvent event = objectMapper.readValue(payload, RevenueCatWebhookEvent.class); switch (event.getType()) { case "INITIAL_PURCHASE": handleInitialPurchase(event); break; case "RENEWAL": handleRenewal(event); break; case "CANCELLATION": handleCancellation(event); break; case "EXPIRATION": handleExpiration(event); break; case "BILLING_ISSUE": handleBillingIssue(event); break; case "PRODUCT_CHANGE": handleProductChange(event); break; default: log.info("Unhandled webhook type: {}", event.getType()); } return ResponseEntity.ok("Webhook processed"); } catch (Exception e) { log.error("Failed to process RevenueCat webhook", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } private void handleInitialPurchase(RevenueCatWebhookEvent event) { String appUserId = event.getAppUserId(); String entitlement = event.getEntitlementId(); log.info("Initial purchase for user: {}, entitlement: {}", appUserId, entitlement); // Update user subscription status in your database subscriptionService.activateSubscription(appUserId, entitlement, event.getEventTimestamp()); // Send welcome email or trigger onboarding notificationService.sendWelcomeEmail(appUserId); } private void handleRenewal(RevenueCatWebhookEvent event) { String appUserId = event.getAppUserId(); log.info("Subscription renewed for user: {}", appUserId); subscriptionService.renewSubscription(appUserId, event.getEventTimestamp()); } private void handleCancellation(RevenueCatWebhookEvent event) { String appUserId = event.getAppUserId(); log.info("Subscription cancelled for user: {}", appUserId); subscriptionService.cancelSubscription(appUserId, event.getEventTimestamp()); // Send cancellation survey or retention offer notificationService.sendCancellationSurvey(appUserId); } private void handleExpiration(RevenueCatWebhookEvent event) { String appUserId = event.getAppUserId(); log.info("Subscription expired for user: {}", appUserId); subscriptionService.expireSubscription(appUserId, event.getEventTimestamp()); // Send re-engagement email notificationService.sendExpirationNotification(appUserId); } private void handleBillingIssue(RevenueCatWebhookEvent event) { String appUserId = event.getAppUserId(); log.warn("Billing issue for user: {}", appUserId); subscriptionService.flagBillingIssue(appUserId); // Notify user about billing issue notificationService.sendBillingIssueNotification(appUserId); } private void handleProductChange(RevenueCatWebhookEvent event) { String appUserId = event.getAppUserId(); String newEntitlement = event.getNewProductId(); log.info("Product change for user: {}, new product: {}", appUserId, newEntitlement); subscriptionService.changeSubscriptionPlan(appUserId, newEntitlement, event.getEventTimestamp()); } } 2. RevenueCat API Client for Backend
@Service @Slf4j public class RevenueCatApiClient { private final OkHttpClient httpClient; private final ObjectMapper objectMapper; private final RevenueCatConfig config; public RevenueCatApiClient(RevenueCatConfig config) { this.config = config; this.objectMapper = new ObjectMapper(); this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); this.httpClient = new OkHttpClient.Builder() .addInterceptor(new RevenueCatAuthInterceptor(config)) .addInterceptor(new HttpLoggingInterceptor(log::info).setLevel(HttpLoggingInterceptor.Level.BASIC)) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); } // Get subscriber information public CompletableFuture<Subscriber> getSubscriber(String appUserId) { return CompletableFuture.supplyAsync(() -> { try { Request request = new Request.Builder() .url(config.getBaseUrl() + "/v1/subscribers/" + appUserId) .get() .build(); Response response = httpClient.newCall(request).execute(); if (!response.isSuccessful()) { handleErrorResponse(response, "Failed to get subscriber: " + appUserId); } String responseBody = response.body().string(); return objectMapper.readValue(responseBody, Subscriber.class); } catch (Exception e) { throw new RevenueCatApiException("Failed to get subscriber: " + appUserId, e); } }); } // Grant promotional entitlement public CompletableFuture<Void> grantPromotionalEntitlement(String appUserId, String entitlementId, int durationDays) { return CompletableFuture.runAsync(() -> { try { Map<String, Object> body = Map.of( "entitlement_id", entitlementId, "duration", Map.of( "value", durationDays, "unit", "days" ) ); String jsonBody = objectMapper.writeValueAsString(body); Request request = new Request.Builder() .url(config.getBaseUrl() + "/v1/subscribers/" + appUserId + "/entitlements") .post(RequestBody.create(jsonBody, MediaType.parse("application/json"))) .build(); Response response = httpClient.newCall(request).execute(); if (!response.isSuccessful()) { handleErrorResponse(response, "Failed to grant entitlement"); } } catch (Exception e) { throw new RevenueCatApiException("Failed to grant promotional entitlement", e); } }); } // Revoke promotional entitlement public CompletableFuture<Void> revokePromotionalEntitlement(String appUserId, String entitlementId) { return CompletableFuture.runAsync(() -> { try { Request request = new Request.Builder() .url(config.getBaseUrl() + "/v1/subscribers/" + appUserId + "/entitlements/" + entitlementId) .delete() .build(); Response response = httpClient.newCall(request).execute(); if (!response.isSuccessful()) { handleErrorResponse(response, "Failed to revoke entitlement"); } } catch (Exception e) { throw new RevenueCatApiException("Failed to revoke promotional entitlement", e); } }); } // Create subscriber (if they don't exist) public CompletableFuture<Subscriber> createSubscriber(String appUserId) { return CompletableFuture.supplyAsync(() -> { try { Map<String, Object> body = Map.of( "app_user_id", appUserId ); String jsonBody = objectMapper.writeValueAsString(body); Request request = new Request.Builder() .url(config.getBaseUrl() + "/v1/subscribers") .post(RequestBody.create(jsonBody, MediaType.parse("application/json"))) .build(); Response response = httpClient.newCall(request).execute(); if (!response.isSuccessful()) { handleErrorResponse(response, "Failed to create subscriber"); } String responseBody = response.body().string(); return objectMapper.readValue(responseBody, Subscriber.class); } catch (Exception e) { throw new RevenueCatApiException("Failed to create subscriber", e); } }); } // Delete subscriber public CompletableFuture<Void> deleteSubscriber(String appUserId) { return CompletableFuture.runAsync(() -> { try { Request request = new Request.Builder() .url(config.getBaseUrl() + "/v1/subscribers/" + appUserId) .delete() .build(); Response response = httpClient.newCall(request).execute(); if (!response.isSuccessful()) { handleErrorResponse(response, "Failed to delete subscriber"); } } catch (Exception e) { throw new RevenueCatApiException("Failed to delete subscriber", e); } }); } // Get subscription analytics public CompletableFuture<SubscriptionAnalytics> getSubscriptionAnalytics(LocalDate startDate, LocalDate endDate) { return CompletableFuture.supplyAsync(() -> { try { HttpUrl url = HttpUrl.parse(config.getBaseUrl() + "/v1/analytics/subscriptions") .newBuilder() .addQueryParameter("start_date", startDate.toString()) .addQueryParameter("end_date", endDate.toString()) .build(); Request request = new Request.Builder() .url(url) .get() .build(); Response response = httpClient.newCall(request).execute(); if (!response.isSuccessful()) { handleErrorResponse(response, "Failed to get analytics"); } String responseBody = response.body().string(); return objectMapper.readValue(responseBody, SubscriptionAnalytics.class); } catch (Exception e) { throw new RevenueCatApiException("Failed to get subscription analytics", e); } }); } private void handleErrorResponse(Response response, String context) throws IOException { int code = response.code(); String body = response.body() != null ? response.body().string() : "No response body"; log.error("RevenueCat API error - Status: {}, Context: {}, Body: {}", code, context, body); switch (code) { case 400: throw new RevenueCatBadRequestException(context + ": " + body); case 401: throw new RevenueCatAuthenticationException("Authentication failed"); case 404: throw new RevenueCatNotFoundException("Resource not found"); case 429: throw new RevenueCatRateLimitException("Rate limit exceeded"); default: throw new RevenueCatApiException("API error - Status: " + code + ", Context: " + context); } } // Authentication interceptor private static class RevenueCatAuthInterceptor implements Interceptor { private final RevenueCatConfig config; public RevenueCatAuthInterceptor(RevenueCatConfig config) { this.config = config; } @Override public Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); Request authenticatedRequest = originalRequest.newBuilder() .header("Authorization", "Bearer " + config.getSecretKey()) .header("Content-Type", "application/json") .header("Accept", "application/json") .build(); return chain.proceed(authenticatedRequest); } } } 3. Subscription Management Service
@Service @Slf4j public class SubscriptionService { private final RevenueCatApiClient revenueCatClient; private final SubscriptionRepository subscriptionRepository; private final UserRepository userRepository; public SubscriptionService(RevenueCatApiClient revenueCatClient, SubscriptionRepository subscriptionRepository, UserRepository userRepository) { this.revenueCatClient = revenueCatClient; this.subscriptionRepository = subscriptionRepository; this.userRepository = userRepository; } @Transactional public void syncUserSubscription(String appUserId) { try { Subscriber subscriber = revenueCatClient.getSubscriber(appUserId).get(); UserSubscription subscription = subscriptionRepository.findByAppUserId(appUserId) .orElse(new UserSubscription(appUserId)); // Update subscription status from RevenueCat data updateSubscriptionFromSubscriber(subscription, subscriber); subscriptionRepository.save(subscription); // Update user access levels updateUserAccessLevels(appUserId, subscription); } catch (Exception e) { log.error("Failed to sync subscription for user: {}", appUserId, e); throw new SubscriptionSyncException("Failed to sync subscription", e); } } @Transactional public void activateSubscription(String appUserId, String entitlementId, Instant activatedAt) { UserSubscription subscription = subscriptionRepository.findByAppUserId(appUserId) .orElse(new UserSubscription(appUserId)); subscription.setActive(true); subscription.setEntitlementId(entitlementId); subscription.setActivatedAt(activatedAt); subscription.setLastSyncedAt(Instant.now()); subscriptionRepository.save(subscription); // Grant access to premium features grantPremiumAccess(appUserId); log.info("Subscription activated for user: {}, entitlement: {}", appUserId, entitlementId); } @Transactional public void cancelSubscription(String appUserId, Instant cancelledAt) { UserSubscription subscription = subscriptionRepository.findByAppUserId(appUserId) .orElseThrow(() -> new SubscriptionNotFoundException(appUserId)); subscription.setActive(false); subscription.setCancelledAt(cancelledAt); subscription.setLastSyncedAt(Instant.now()); subscriptionRepository.save(subscription); // Revoke premium access (with grace period) schedulePremiumAccessRevocation(appUserId); log.info("Subscription cancelled for user: {}", appUserId); } @Transactional public void expireSubscription(String appUserId, Instant expiredAt) { UserSubscription subscription = subscriptionRepository.findByAppUserId(appUserId) .orElseThrow(() -> new SubscriptionNotFoundException(appUserId)); subscription.setActive(false); subscription.setExpiredAt(expiredAt); subscription.setLastSyncedAt(Instant.now()); subscriptionRepository.save(subscription); // Immediately revoke premium access revokePremiumAccess(appUserId); log.info("Subscription expired for user: {}", appUserId); } public boolean hasActiveSubscription(String appUserId) { return subscriptionRepository.findByAppUserId(appUserId) .map(UserSubscription::isActive) .orElse(false); } public Optional<String> getActiveEntitlement(String appUserId) { return subscriptionRepository.findByAppUserId(appUserId) .filter(UserSubscription::isActive) .map(UserSubscription::getEntitlementId); } public List<UserSubscription> findExpiringSubscriptions(Instant withinDays) { return subscriptionRepository.findByActiveTrueAndExpiresAtBefore(withinDays); } public List<UserSubscription> findTrialsExpiringSoon(Instant withinDays) { return subscriptionRepository.findByIsTrialTrueAndTrialEndsAtBefore(withinDays); } private void updateSubscriptionFromSubscriber(UserSubscription subscription, Subscriber subscriber) { // Implementation to map RevenueCat subscriber data to local subscription model Entitlements entitlements = subscriber.getSubscriber().getEntitlements(); boolean hasActiveEntitlement = entitlements.values().stream() .anyMatch(Entitlement::getIsActive); subscription.setActive(hasActiveEntitlement); subscription.setLastSyncedAt(Instant.now()); // Update other fields from subscriber data if (hasActiveEntitlement) { entitlements.values().stream() .filter(Entitlement::getIsActive) .findFirst() .ifPresent(activeEntitlement -> { subscription.setEntitlementId(activeEntitlement.getProductIdentifier()); subscription.setExpiresAt(activeEntitlement.getExpiresDate()); subscription.setStore(activeEntitlement.getStore()); }); } } private void updateUserAccessLevels(String appUserId, UserSubscription subscription) { User user = userRepository.findByAppUserId(appUserId) .orElseThrow(() -> new UserNotFoundException(appUserId)); user.setPremiumAccess(subscription.isActive()); user.setPremiumFeatures(getPremiumFeaturesForEntitlement(subscription.getEntitlementId())); userRepository.save(user); } private void grantPremiumAccess(String appUserId) { User user = userRepository.findByAppUserId(appUserId) .orElseThrow(() -> new UserNotFoundException(appUserId)); user.setPremiumAccess(true); userRepository.save(user); // Trigger any post-activation workflows eventPublisher.publishEvent(new PremiumAccessGrantedEvent(appUserId)); } private void revokePremiumAccess(String appUserId) { User user = userRepository.findByAppUserId(appUserId) .orElseThrow(() -> new UserNotFoundException(appUserId)); user.setPremiumAccess(false); userRepository.save(user); // Trigger any post-revocation workflows eventPublisher.publishEvent(new PremiumAccessRevokedEvent(appUserId)); } private void schedulePremiumAccessRevocation(String appUserId) { // Schedule revocation for end of billing period scheduler.schedule(() -> revokePremiumAccess(appUserId), calculateRevocationDelay(appUserId)); } private List<String> getPremiumFeaturesForEntitlement(String entitlementId) { // Map entitlement IDs to specific feature sets return featureMappingService.getFeaturesForEntitlement(entitlementId); } } Data Models
1. RevenueCat Webhook Event Model
@Data public class RevenueCatWebhookEvent { private String type; private String id; private String appId; private String appUserId; private String entitlementId; private String productId; private String newProductId; private Instant eventTimestamp; private Map<String, Object> data; public enum EventType { INITIAL_PURCHASE, RENEWAL, CANCELLATION, EXPIRATION, BILLING_ISSUE, PRODUCT_CHANGE, SUBSCRIBER_ALIAS } } @Data public class Subscriber { private SubscriberData subscriber; private Request request; @Data public static class SubscriberData { private String originalAppUserId; private String originalApplicationVersion; private FirstSeenInfo firstSeen; private Map<String, Entitlement> entitlements; private Map<String, Subscription> subscriptions; private Map<String, NonSubscription> nonSubscriptions; } @Data public static class Entitlement { private String productIdentifier; private Boolean isActive; private Instant expiresDate; private Instant purchaseDate; private String store; private String unsubscribeDetectedAt; private String billingIssueDetectedAt; } } @Data public class UserSubscription { private String id; private String appUserId; private boolean isActive; private String entitlementId; private String store; private Instant activatedAt; private Instant cancelledAt; private Instant expiredAt; private Instant expiresAt; private boolean isTrial; private Instant trialEndsAt; private Instant lastSyncedAt; private Map<String, Object> metadata; public UserSubscription() {} public UserSubscription(String appUserId) { this.appUserId = appUserId; this.isActive = false; this.lastSyncedAt = Instant.now(); } } Configuration
1. RevenueCat Configuration
@Configuration @ConfigurationProperties(prefix = "revenuecat") @Data public class RevenueCatConfig { private String publicKey; private String secretKey; private String baseUrl = "https://api.revenuecat.com"; private String webhookSecret; @Bean public RevenueCatApiClient revenueCatApiClient() { return new RevenueCatApiClient(this); } } 2. Application Properties
# application.yml revenuecat: public-key: ${REVENUECAT_PUBLIC_KEY} secret-key: ${REVENUECAT_SECRET_KEY} webhook-secret: ${REVENUECAT_WEBHOOK_SECRET} base-url: https://api.revenuecat.com Testing
1. Unit Tests
@ExtendWith(MockitoExtension.class) class SubscriptionServiceTest { @Mock private RevenueCatApiClient revenueCatClient; @Mock private SubscriptionRepository subscriptionRepository; @InjectMocks private SubscriptionService subscriptionService; @Test void shouldActivateSubscriptionSuccessfully() { // Given String appUserId = "user123"; String entitlementId = "premium_monthly"; Instant activatedAt = Instant.now(); when(subscriptionRepository.findByAppUserId(appUserId)) .thenReturn(Optional.empty()); // When subscriptionService.activateSubscription(appUserId, entitlementId, activatedAt); // Then verify(subscriptionRepository).save(argThat(subscription -> subscription.getAppUserId().equals(appUserId) && subscription.isActive() && subscription.getEntitlementId().equals(entitlementId) )); } @Test void shouldSyncSubscriptionFromRevenueCat() { // Given String appUserId = "user123"; Subscriber mockSubscriber = createMockSubscriber(); when(revenueCatClient.getSubscriber(appUserId)) .thenReturn(CompletableFuture.completedFuture(mockSubscriber)); when(subscriptionRepository.findByAppUserId(appUserId)) .thenReturn(Optional.of(new UserSubscription(appUserId))); // When subscriptionService.syncUserSubscription(appUserId); // Then verify(subscriptionRepository).save(argThat(subscription -> subscription.isActive() && subscription.getLastSyncedAt() != null )); } } Best Practices
- Error Handling - Implement comprehensive error handling for network failures
- Retry Logic - Add retry with exponential backoff for API calls
- Caching - Cache subscriber data to reduce API calls
- Security - Validate webhook signatures and use secure API key storage
- Monitoring - Track subscription metrics and API usage
- Testing - Mock RevenueCat API for reliable testing
- Grace Periods - Implement grace periods for subscription expirations
Conclusion
Integrating RevenueCat with Java applications provides a robust foundation for mobile subscription management. By implementing the patterns shown here, Java developers can:
- Simplify Subscription Logic - Abstract away platform-specific purchase flows
- Ensure Data Consistency - Keep local subscription state in sync with RevenueCat
- Automate Business Processes - Trigger workflows based on subscription events
- Gain Insights - Leverage RevenueCat's analytics for business intelligence
- Scale Confidently - Rely on RevenueCat's infrastructure for global scale
The combination of RevenueCat's powerful subscription platform with Java's enterprise capabilities creates a comprehensive solution for mobile monetization that can scale from startup to enterprise-level applications.