Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
### v4.3.0 (2026-02-20)
* * *

### New Features:
* Added async API support for all resource operations via `CompletableFuture`-based async methods (e.g., `createAsync`, `listAsync`, `retrieveAsync`).

### v4.2.0 (2026-02-16)
* * *

Expand Down
50 changes: 41 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -516,20 +516,33 @@ try {
```

##### Async exception handling

> **Important:** When using async methods, exceptions are wrapped in a `java.util.concurrent.CompletionException`.
> Unlike sync methods that throw `ChargebeeException` directly, async methods deliver errors through
> `CompletableFuture`'s `.exceptionally()` or `.handle()` callbacks, where the original exception is
> available via `throwable.getCause()`. Always unwrap the `CompletionException` to access the
> underlying `ChargebeeException` (e.g., `InvalidRequestException`, `APIException`).

```java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;

CompletableFuture<CustomerCreateResponse> futureCustomer = customers.create(params);
CompletableFuture<CustomerCreateResponse> futureCustomer = customers.createAsync(params);

futureCustomer
.thenAccept(customer -> {
System.out.println("Customer created: " + customer.getCustomer().getId());
.thenAccept(response -> {
System.out.println("Customer created: " + response.getCustomer().getId());
})
.exceptionally(throwable -> {
if (throwable.getCause() instanceof InvalidRequestException) {
InvalidRequestException e = (InvalidRequestException) throwable.getCause();
// Unwrap CompletionException to get the actual ChargebeeException
Throwable cause = throwable instanceof CompletionException
? throwable.getCause()
: throwable;

if (cause instanceof InvalidRequestException) {
InvalidRequestException e = (InvalidRequestException) cause;
ApiErrorCode errorCode = e.getApiErrorCode();

if (errorCode instanceof BadRequestApiErrorCode) {
BadRequestApiErrorCode code = (BadRequestApiErrorCode) errorCode;
if (code == BadRequestApiErrorCode.DUPLICATE_ENTRY) {
Expand All @@ -538,16 +551,35 @@ futureCustomer
} else {
System.err.println("Validation error: " + e.getMessage());
}
} else if (throwable.getCause() instanceof APIException) {
APIException e = (APIException) throwable.getCause();
} else if (cause instanceof APIException) {
APIException e = (APIException) cause;
System.err.println("API error: " + e.getApiErrorCodeRaw());
} else {
System.err.println("Unexpected error: " + throwable.getMessage());
System.err.println("Unexpected error: " + cause.getMessage());
}
return null;
});
```

If you prefer blocking on the result, use a try-catch around `.join()` or `.get()`:

```java
try {
CustomerCreateResponse response = customers.createAsync(params).join();
System.out.println("Customer created: " + response.getCustomer().getId());
} catch (CompletionException e) {
// Unwrap to get the original ChargebeeException
Throwable cause = e.getCause();
if (cause instanceof InvalidRequestException) {
System.err.println("Validation error: " + cause.getMessage());
} else if (cause instanceof APIException) {
System.err.println("API error: " + ((APIException) cause).getApiErrorCodeRaw());
} else {
throw e; // Re-throw unexpected errors
}
}
```

### Retry Handling

Chargebee's SDK includes built-in retry logic to handle temporary network issues and server-side errors. This feature is **disabled by default** but can be **enabled when needed**.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.2.0
4.3.0
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
}

group = "com.chargebee"
version = "4.2.0"
version = "4.3.0"
description = "Java client library for ChargeBee"

// Project metadata
Expand Down
58 changes: 36 additions & 22 deletions src/main/java/com/chargebee/v4/client/ChargebeeClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

/**
* Immutable, thread-safe Chargebee API client with pluggable transport.
Expand All @@ -25,7 +28,7 @@
* .build();
* }</pre>
*/
public final class ChargebeeClient extends ClientMethodsImpl {
public final class ChargebeeClient extends ClientMethodsImpl implements AutoCloseable {
private final String apiKey;
private final String siteName;
private final String endpoint;
Expand All @@ -37,7 +40,8 @@ public final class ChargebeeClient extends ClientMethodsImpl {
private final String protocol;
private final RequestInterceptor requestInterceptor;
private final RequestContext clientHeaders;

private final ScheduledExecutorService retryScheduler;

// Auto-generated service registry for lazy loading
private final ServiceRegistry serviceRegistry;

Expand All @@ -53,6 +57,11 @@ private ChargebeeClient(Builder builder) {
this.protocol = builder.protocol;
this.requestInterceptor = builder.requestInterceptor;
this.clientHeaders = new RequestContext(builder.clientHeaders.getHeaders());
this.retryScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "chargebee-retry-scheduler");
t.setDaemon(true);
return t;
});
this.serviceRegistry = new ServiceRegistry(this);
}

Expand Down Expand Up @@ -83,7 +92,19 @@ public static Builder builder(String apiKey, String siteName) {
public String getProtocol() { return protocol; }
public RequestInterceptor getRequestInterceptor() { return requestInterceptor; }
public RequestContext getClientHeaders() { return clientHeaders; }


@Override
public void close() {
retryScheduler.shutdown();
if (transport instanceof AutoCloseable) {
try {
((AutoCloseable) transport).close();
} catch (Exception e) {
// best-effort cleanup
}
}
}

// (Header decoration removed from public API)

// Resource Services - Auto-generated via ClientMethodsImpl
Expand Down Expand Up @@ -477,25 +498,18 @@ private CompletableFuture<Response> sendWithRetryAsyncInternal(Request request,

private CompletableFuture<Response> delayAndRetry(Request request, int nextAttempt, long delayMs, int maxRetries) {
CompletableFuture<Response> delayedRetry = new CompletableFuture<>();

// Use a separate thread for the delay to avoid blocking
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(delayMs);
sendWithRetryAsyncInternal(request, nextAttempt, maxRetries)
.whenComplete((response, throwable) -> {
if (throwable != null) {
delayedRetry.completeExceptionally(throwable);
} else {
delayedRetry.complete(response);
}
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
delayedRetry.completeExceptionally(new RuntimeException("Interrupted during retry delay", e));
}
});


retryScheduler.schedule(() -> {
sendWithRetryAsyncInternal(request, nextAttempt, maxRetries)
.whenComplete((response, throwable) -> {
if (throwable != null) {
delayedRetry.completeExceptionally(throwable);
} else {
delayedRetry.complete(response);
}
});
}, delayMs, TimeUnit.MILLISECONDS);

return delayedRetry;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.chargebee.v4.client.request.RequestOptions;
import com.chargebee.v4.exceptions.ChargebeeException;
import com.chargebee.v4.transport.Response;
import java.util.concurrent.CompletableFuture;

import com.chargebee.v4.models.additionalBillingLogiq.params.AdditionalBillingLogiqRetrieveParams;

Expand Down Expand Up @@ -85,9 +86,30 @@ public AdditionalBillingLogiqRetrieveResponse retrieve(
return AdditionalBillingLogiqRetrieveResponse.fromJson(response.getBodyAsString(), response);
}

/** Async variant of retrieve for additionalBillingLogiq with params. */
public CompletableFuture<AdditionalBillingLogiqRetrieveResponse> retrieveAsync(
AdditionalBillingLogiqRetrieveParams params) {

return getAsync("/additional_billing_logiqs", params != null ? params.toQueryParams() : null)
.thenApply(
response ->
AdditionalBillingLogiqRetrieveResponse.fromJson(
response.getBodyAsString(), response));
}

public AdditionalBillingLogiqRetrieveResponse retrieve() throws ChargebeeException {
Response response = retrieveRaw();

return AdditionalBillingLogiqRetrieveResponse.fromJson(response.getBodyAsString(), response);
}

/** Async variant of retrieve for additionalBillingLogiq without params. */
public CompletableFuture<AdditionalBillingLogiqRetrieveResponse> retrieveAsync() {

return getAsync("/additional_billing_logiqs", null)
.thenApply(
response ->
AdditionalBillingLogiqRetrieveResponse.fromJson(
response.getBodyAsString(), response));
}
}
75 changes: 75 additions & 0 deletions src/main/java/com/chargebee/v4/services/AddonService.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.chargebee.v4.client.request.RequestOptions;
import com.chargebee.v4.exceptions.ChargebeeException;
import com.chargebee.v4.transport.Response;
import java.util.concurrent.CompletableFuture;

import com.chargebee.v4.models.addon.params.AddonCopyParams;

Expand Down Expand Up @@ -86,6 +87,13 @@ public AddonCopyResponse copy(AddonCopyParams params) throws ChargebeeException
return AddonCopyResponse.fromJson(response.getBodyAsString(), response);
}

/** Async variant of copy for addon with params. */
public CompletableFuture<AddonCopyResponse> copyAsync(AddonCopyParams params) {

return postAsync("/addons/copy", params != null ? params.toFormData() : null)
.thenApply(response -> AddonCopyResponse.fromJson(response.getBodyAsString(), response));
}

/** unarchive a addon (executes immediately) - returns raw Response. */
Response unarchiveRaw(String addonId) throws ChargebeeException {
String path = buildPathWithParams("/addons/{addon-id}/unarchive", "addon-id", addonId);
Expand All @@ -98,6 +106,15 @@ public AddonUnarchiveResponse unarchive(String addonId) throws ChargebeeExceptio
return AddonUnarchiveResponse.fromJson(response.getBodyAsString(), response);
}

/** Async variant of unarchive for addon without params. */
public CompletableFuture<AddonUnarchiveResponse> unarchiveAsync(String addonId) {
String path = buildPathWithParams("/addons/{addon-id}/unarchive", "addon-id", addonId);

return postAsync(path, null)
.thenApply(
response -> AddonUnarchiveResponse.fromJson(response.getBodyAsString(), response));
}

/** retrieve a addon (executes immediately) - returns raw Response. */
Response retrieveRaw(String addonId) throws ChargebeeException {
String path = buildPathWithParams("/addons/{addon-id}", "addon-id", addonId);
Expand All @@ -110,6 +127,15 @@ public AddonRetrieveResponse retrieve(String addonId) throws ChargebeeException
return AddonRetrieveResponse.fromJson(response.getBodyAsString(), response);
}

/** Async variant of retrieve for addon without params. */
public CompletableFuture<AddonRetrieveResponse> retrieveAsync(String addonId) {
String path = buildPathWithParams("/addons/{addon-id}", "addon-id", addonId);

return getAsync(path, null)
.thenApply(
response -> AddonRetrieveResponse.fromJson(response.getBodyAsString(), response));
}

/** update a addon (executes immediately) - returns raw Response. */
Response updateRaw(String addonId) throws ChargebeeException {
String path = buildPathWithParams("/addons/{addon-id}", "addon-id", addonId);
Expand All @@ -135,11 +161,27 @@ public AddonUpdateResponse update(String addonId, AddonUpdateParams params)
return AddonUpdateResponse.fromJson(response.getBodyAsString(), response);
}

/** Async variant of update for addon with params. */
public CompletableFuture<AddonUpdateResponse> updateAsync(
String addonId, AddonUpdateParams params) {
String path = buildPathWithParams("/addons/{addon-id}", "addon-id", addonId);
return postAsync(path, params.toFormData())
.thenApply(response -> AddonUpdateResponse.fromJson(response.getBodyAsString(), response));
}

public AddonUpdateResponse update(String addonId) throws ChargebeeException {
Response response = updateRaw(addonId);
return AddonUpdateResponse.fromJson(response.getBodyAsString(), response);
}

/** Async variant of update for addon without params. */
public CompletableFuture<AddonUpdateResponse> updateAsync(String addonId) {
String path = buildPathWithParams("/addons/{addon-id}", "addon-id", addonId);

return postAsync(path, null)
.thenApply(response -> AddonUpdateResponse.fromJson(response.getBodyAsString(), response));
}

/** list a addon using immutable params (executes immediately) - returns raw Response. */
Response listRaw(AddonListParams params) throws ChargebeeException {

Expand All @@ -164,12 +206,30 @@ public AddonListResponse list(AddonListParams params) throws ChargebeeException
return AddonListResponse.fromJson(response.getBodyAsString(), this, params, response);
}

/** Async variant of list for addon with params. */
public CompletableFuture<AddonListResponse> listAsync(AddonListParams params) {

return getAsync("/addons", params != null ? params.toQueryParams() : null)
.thenApply(
response ->
AddonListResponse.fromJson(response.getBodyAsString(), this, params, response));
}

public AddonListResponse list() throws ChargebeeException {
Response response = listRaw();

return AddonListResponse.fromJson(response.getBodyAsString(), this, null, response);
}

/** Async variant of list for addon without params. */
public CompletableFuture<AddonListResponse> listAsync() {

return getAsync("/addons", null)
.thenApply(
response ->
AddonListResponse.fromJson(response.getBodyAsString(), this, null, response));
}

/** create a addon using immutable params (executes immediately) - returns raw Response. */
Response createRaw(AddonCreateParams params) throws ChargebeeException {

Expand All @@ -188,6 +248,13 @@ public AddonCreateResponse create(AddonCreateParams params) throws ChargebeeExce
return AddonCreateResponse.fromJson(response.getBodyAsString(), response);
}

/** Async variant of create for addon with params. */
public CompletableFuture<AddonCreateResponse> createAsync(AddonCreateParams params) {

return postAsync("/addons", params != null ? params.toFormData() : null)
.thenApply(response -> AddonCreateResponse.fromJson(response.getBodyAsString(), response));
}

/** delete a addon (executes immediately) - returns raw Response. */
Response deleteRaw(String addonId) throws ChargebeeException {
String path = buildPathWithParams("/addons/{addon-id}/delete", "addon-id", addonId);
Expand All @@ -199,4 +266,12 @@ public AddonDeleteResponse delete(String addonId) throws ChargebeeException {
Response response = deleteRaw(addonId);
return AddonDeleteResponse.fromJson(response.getBodyAsString(), response);
}

/** Async variant of delete for addon without params. */
public CompletableFuture<AddonDeleteResponse> deleteAsync(String addonId) {
String path = buildPathWithParams("/addons/{addon-id}/delete", "addon-id", addonId);

return postAsync(path, null)
.thenApply(response -> AddonDeleteResponse.fromJson(response.getBodyAsString(), response));
}
}
Loading