Skip to content

Commit 68838e9

Browse files
authored
Enable async api support (#88)
* Enable async api support * version bump-up and change log * add JavaDoc * Exception handling document * add configurable executor service * Version bump-up
1 parent 1aec9ed commit 68838e9

91 files changed

Lines changed: 8507 additions & 39 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
### v4.3.0 (2026-02-20)
2+
* * *
3+
4+
### New Features:
5+
* Added async API support for all resource operations via `CompletableFuture`-based async methods (e.g., `createAsync`, `listAsync`, `retrieveAsync`).
6+
17
### v4.2.0 (2026-02-16)
28
* * *
39

README.md

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -516,20 +516,33 @@ try {
516516
```
517517

518518
##### Async exception handling
519+
520+
> **Important:** When using async methods, exceptions are wrapped in a `java.util.concurrent.CompletionException`.
521+
> Unlike sync methods that throw `ChargebeeException` directly, async methods deliver errors through
522+
> `CompletableFuture`'s `.exceptionally()` or `.handle()` callbacks, where the original exception is
523+
> available via `throwable.getCause()`. Always unwrap the `CompletionException` to access the
524+
> underlying `ChargebeeException` (e.g., `InvalidRequestException`, `APIException`).
525+
519526
```java
520527
import java.util.concurrent.CompletableFuture;
528+
import java.util.concurrent.CompletionException;
521529

522-
CompletableFuture<CustomerCreateResponse> futureCustomer = customers.create(params);
530+
CompletableFuture<CustomerCreateResponse> futureCustomer = customers.createAsync(params);
523531

524532
futureCustomer
525-
.thenAccept(customer -> {
526-
System.out.println("Customer created: " + customer.getCustomer().getId());
533+
.thenAccept(response -> {
534+
System.out.println("Customer created: " + response.getCustomer().getId());
527535
})
528536
.exceptionally(throwable -> {
529-
if (throwable.getCause() instanceof InvalidRequestException) {
530-
InvalidRequestException e = (InvalidRequestException) throwable.getCause();
537+
// Unwrap CompletionException to get the actual ChargebeeException
538+
Throwable cause = throwable instanceof CompletionException
539+
? throwable.getCause()
540+
: throwable;
541+
542+
if (cause instanceof InvalidRequestException) {
543+
InvalidRequestException e = (InvalidRequestException) cause;
531544
ApiErrorCode errorCode = e.getApiErrorCode();
532-
545+
533546
if (errorCode instanceof BadRequestApiErrorCode) {
534547
BadRequestApiErrorCode code = (BadRequestApiErrorCode) errorCode;
535548
if (code == BadRequestApiErrorCode.DUPLICATE_ENTRY) {
@@ -538,16 +551,35 @@ futureCustomer
538551
} else {
539552
System.err.println("Validation error: " + e.getMessage());
540553
}
541-
} else if (throwable.getCause() instanceof APIException) {
542-
APIException e = (APIException) throwable.getCause();
554+
} else if (cause instanceof APIException) {
555+
APIException e = (APIException) cause;
543556
System.err.println("API error: " + e.getApiErrorCodeRaw());
544557
} else {
545-
System.err.println("Unexpected error: " + throwable.getMessage());
558+
System.err.println("Unexpected error: " + cause.getMessage());
546559
}
547560
return null;
548561
});
549562
```
550563

564+
If you prefer blocking on the result, use a try-catch around `.join()` or `.get()`:
565+
566+
```java
567+
try {
568+
CustomerCreateResponse response = customers.createAsync(params).join();
569+
System.out.println("Customer created: " + response.getCustomer().getId());
570+
} catch (CompletionException e) {
571+
// Unwrap to get the original ChargebeeException
572+
Throwable cause = e.getCause();
573+
if (cause instanceof InvalidRequestException) {
574+
System.err.println("Validation error: " + cause.getMessage());
575+
} else if (cause instanceof APIException) {
576+
System.err.println("API error: " + ((APIException) cause).getApiErrorCodeRaw());
577+
} else {
578+
throw e; // Re-throw unexpected errors
579+
}
580+
}
581+
```
582+
551583
### Retry Handling
552584

553585
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**.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
4.2.0
1+
4.3.0

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ plugins {
77
}
88

99
group = "com.chargebee"
10-
version = "4.2.0"
10+
version = "4.3.0"
1111
description = "Java client library for ChargeBee"
1212

1313
// Project metadata

src/main/java/com/chargebee/v4/client/ChargebeeClient.java

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414

1515
import java.util.*;
1616
import java.util.concurrent.CompletableFuture;
17+
import java.util.concurrent.Executors;
18+
import java.util.concurrent.ScheduledExecutorService;
1719
import java.util.concurrent.ThreadLocalRandom;
20+
import java.util.concurrent.TimeUnit;
1821

1922
/**
2023
* Immutable, thread-safe Chargebee API client with pluggable transport.
@@ -25,7 +28,7 @@
2528
* .build();
2629
* }</pre>
2730
*/
28-
public final class ChargebeeClient extends ClientMethodsImpl {
31+
public final class ChargebeeClient extends ClientMethodsImpl implements AutoCloseable {
2932
private final String apiKey;
3033
private final String siteName;
3134
private final String endpoint;
@@ -37,7 +40,8 @@ public final class ChargebeeClient extends ClientMethodsImpl {
3740
private final String protocol;
3841
private final RequestInterceptor requestInterceptor;
3942
private final RequestContext clientHeaders;
40-
43+
private final ScheduledExecutorService retryScheduler;
44+
4145
// Auto-generated service registry for lazy loading
4246
private final ServiceRegistry serviceRegistry;
4347

@@ -53,6 +57,11 @@ private ChargebeeClient(Builder builder) {
5357
this.protocol = builder.protocol;
5458
this.requestInterceptor = builder.requestInterceptor;
5559
this.clientHeaders = new RequestContext(builder.clientHeaders.getHeaders());
60+
this.retryScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
61+
Thread t = new Thread(r, "chargebee-retry-scheduler");
62+
t.setDaemon(true);
63+
return t;
64+
});
5665
this.serviceRegistry = new ServiceRegistry(this);
5766
}
5867

@@ -83,7 +92,19 @@ public static Builder builder(String apiKey, String siteName) {
8392
public String getProtocol() { return protocol; }
8493
public RequestInterceptor getRequestInterceptor() { return requestInterceptor; }
8594
public RequestContext getClientHeaders() { return clientHeaders; }
86-
95+
96+
@Override
97+
public void close() {
98+
retryScheduler.shutdown();
99+
if (transport instanceof AutoCloseable) {
100+
try {
101+
((AutoCloseable) transport).close();
102+
} catch (Exception e) {
103+
// best-effort cleanup
104+
}
105+
}
106+
}
107+
87108
// (Header decoration removed from public API)
88109

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

478499
private CompletableFuture<Response> delayAndRetry(Request request, int nextAttempt, long delayMs, int maxRetries) {
479500
CompletableFuture<Response> delayedRetry = new CompletableFuture<>();
480-
481-
// Use a separate thread for the delay to avoid blocking
482-
CompletableFuture.runAsync(() -> {
483-
try {
484-
Thread.sleep(delayMs);
485-
sendWithRetryAsyncInternal(request, nextAttempt, maxRetries)
486-
.whenComplete((response, throwable) -> {
487-
if (throwable != null) {
488-
delayedRetry.completeExceptionally(throwable);
489-
} else {
490-
delayedRetry.complete(response);
491-
}
492-
});
493-
} catch (InterruptedException e) {
494-
Thread.currentThread().interrupt();
495-
delayedRetry.completeExceptionally(new RuntimeException("Interrupted during retry delay", e));
496-
}
497-
});
498-
501+
502+
retryScheduler.schedule(() -> {
503+
sendWithRetryAsyncInternal(request, nextAttempt, maxRetries)
504+
.whenComplete((response, throwable) -> {
505+
if (throwable != null) {
506+
delayedRetry.completeExceptionally(throwable);
507+
} else {
508+
delayedRetry.complete(response);
509+
}
510+
});
511+
}, delayMs, TimeUnit.MILLISECONDS);
512+
499513
return delayedRetry;
500514
}
501515

src/main/java/com/chargebee/v4/services/AdditionalBillingLogiqService.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.chargebee.v4.client.request.RequestOptions;
1212
import com.chargebee.v4.exceptions.ChargebeeException;
1313
import com.chargebee.v4.transport.Response;
14+
import java.util.concurrent.CompletableFuture;
1415

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

@@ -85,9 +86,30 @@ public AdditionalBillingLogiqRetrieveResponse retrieve(
8586
return AdditionalBillingLogiqRetrieveResponse.fromJson(response.getBodyAsString(), response);
8687
}
8788

89+
/** Async variant of retrieve for additionalBillingLogiq with params. */
90+
public CompletableFuture<AdditionalBillingLogiqRetrieveResponse> retrieveAsync(
91+
AdditionalBillingLogiqRetrieveParams params) {
92+
93+
return getAsync("/additional_billing_logiqs", params != null ? params.toQueryParams() : null)
94+
.thenApply(
95+
response ->
96+
AdditionalBillingLogiqRetrieveResponse.fromJson(
97+
response.getBodyAsString(), response));
98+
}
99+
88100
public AdditionalBillingLogiqRetrieveResponse retrieve() throws ChargebeeException {
89101
Response response = retrieveRaw();
90102

91103
return AdditionalBillingLogiqRetrieveResponse.fromJson(response.getBodyAsString(), response);
92104
}
105+
106+
/** Async variant of retrieve for additionalBillingLogiq without params. */
107+
public CompletableFuture<AdditionalBillingLogiqRetrieveResponse> retrieveAsync() {
108+
109+
return getAsync("/additional_billing_logiqs", null)
110+
.thenApply(
111+
response ->
112+
AdditionalBillingLogiqRetrieveResponse.fromJson(
113+
response.getBodyAsString(), response));
114+
}
93115
}

src/main/java/com/chargebee/v4/services/AddonService.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.chargebee.v4.client.request.RequestOptions;
1212
import com.chargebee.v4.exceptions.ChargebeeException;
1313
import com.chargebee.v4.transport.Response;
14+
import java.util.concurrent.CompletableFuture;
1415

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

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

90+
/** Async variant of copy for addon with params. */
91+
public CompletableFuture<AddonCopyResponse> copyAsync(AddonCopyParams params) {
92+
93+
return postAsync("/addons/copy", params != null ? params.toFormData() : null)
94+
.thenApply(response -> AddonCopyResponse.fromJson(response.getBodyAsString(), response));
95+
}
96+
8997
/** unarchive a addon (executes immediately) - returns raw Response. */
9098
Response unarchiveRaw(String addonId) throws ChargebeeException {
9199
String path = buildPathWithParams("/addons/{addon-id}/unarchive", "addon-id", addonId);
@@ -98,6 +106,15 @@ public AddonUnarchiveResponse unarchive(String addonId) throws ChargebeeExceptio
98106
return AddonUnarchiveResponse.fromJson(response.getBodyAsString(), response);
99107
}
100108

109+
/** Async variant of unarchive for addon without params. */
110+
public CompletableFuture<AddonUnarchiveResponse> unarchiveAsync(String addonId) {
111+
String path = buildPathWithParams("/addons/{addon-id}/unarchive", "addon-id", addonId);
112+
113+
return postAsync(path, null)
114+
.thenApply(
115+
response -> AddonUnarchiveResponse.fromJson(response.getBodyAsString(), response));
116+
}
117+
101118
/** retrieve a addon (executes immediately) - returns raw Response. */
102119
Response retrieveRaw(String addonId) throws ChargebeeException {
103120
String path = buildPathWithParams("/addons/{addon-id}", "addon-id", addonId);
@@ -110,6 +127,15 @@ public AddonRetrieveResponse retrieve(String addonId) throws ChargebeeException
110127
return AddonRetrieveResponse.fromJson(response.getBodyAsString(), response);
111128
}
112129

130+
/** Async variant of retrieve for addon without params. */
131+
public CompletableFuture<AddonRetrieveResponse> retrieveAsync(String addonId) {
132+
String path = buildPathWithParams("/addons/{addon-id}", "addon-id", addonId);
133+
134+
return getAsync(path, null)
135+
.thenApply(
136+
response -> AddonRetrieveResponse.fromJson(response.getBodyAsString(), response));
137+
}
138+
113139
/** update a addon (executes immediately) - returns raw Response. */
114140
Response updateRaw(String addonId) throws ChargebeeException {
115141
String path = buildPathWithParams("/addons/{addon-id}", "addon-id", addonId);
@@ -135,11 +161,27 @@ public AddonUpdateResponse update(String addonId, AddonUpdateParams params)
135161
return AddonUpdateResponse.fromJson(response.getBodyAsString(), response);
136162
}
137163

164+
/** Async variant of update for addon with params. */
165+
public CompletableFuture<AddonUpdateResponse> updateAsync(
166+
String addonId, AddonUpdateParams params) {
167+
String path = buildPathWithParams("/addons/{addon-id}", "addon-id", addonId);
168+
return postAsync(path, params.toFormData())
169+
.thenApply(response -> AddonUpdateResponse.fromJson(response.getBodyAsString(), response));
170+
}
171+
138172
public AddonUpdateResponse update(String addonId) throws ChargebeeException {
139173
Response response = updateRaw(addonId);
140174
return AddonUpdateResponse.fromJson(response.getBodyAsString(), response);
141175
}
142176

177+
/** Async variant of update for addon without params. */
178+
public CompletableFuture<AddonUpdateResponse> updateAsync(String addonId) {
179+
String path = buildPathWithParams("/addons/{addon-id}", "addon-id", addonId);
180+
181+
return postAsync(path, null)
182+
.thenApply(response -> AddonUpdateResponse.fromJson(response.getBodyAsString(), response));
183+
}
184+
143185
/** list a addon using immutable params (executes immediately) - returns raw Response. */
144186
Response listRaw(AddonListParams params) throws ChargebeeException {
145187

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

209+
/** Async variant of list for addon with params. */
210+
public CompletableFuture<AddonListResponse> listAsync(AddonListParams params) {
211+
212+
return getAsync("/addons", params != null ? params.toQueryParams() : null)
213+
.thenApply(
214+
response ->
215+
AddonListResponse.fromJson(response.getBodyAsString(), this, params, response));
216+
}
217+
167218
public AddonListResponse list() throws ChargebeeException {
168219
Response response = listRaw();
169220

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

224+
/** Async variant of list for addon without params. */
225+
public CompletableFuture<AddonListResponse> listAsync() {
226+
227+
return getAsync("/addons", null)
228+
.thenApply(
229+
response ->
230+
AddonListResponse.fromJson(response.getBodyAsString(), this, null, response));
231+
}
232+
173233
/** create a addon using immutable params (executes immediately) - returns raw Response. */
174234
Response createRaw(AddonCreateParams params) throws ChargebeeException {
175235

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

251+
/** Async variant of create for addon with params. */
252+
public CompletableFuture<AddonCreateResponse> createAsync(AddonCreateParams params) {
253+
254+
return postAsync("/addons", params != null ? params.toFormData() : null)
255+
.thenApply(response -> AddonCreateResponse.fromJson(response.getBodyAsString(), response));
256+
}
257+
191258
/** delete a addon (executes immediately) - returns raw Response. */
192259
Response deleteRaw(String addonId) throws ChargebeeException {
193260
String path = buildPathWithParams("/addons/{addon-id}/delete", "addon-id", addonId);
@@ -199,4 +266,12 @@ public AddonDeleteResponse delete(String addonId) throws ChargebeeException {
199266
Response response = deleteRaw(addonId);
200267
return AddonDeleteResponse.fromJson(response.getBodyAsString(), response);
201268
}
269+
270+
/** Async variant of delete for addon without params. */
271+
public CompletableFuture<AddonDeleteResponse> deleteAsync(String addonId) {
272+
String path = buildPathWithParams("/addons/{addon-id}/delete", "addon-id", addonId);
273+
274+
return postAsync(path, null)
275+
.thenApply(response -> AddonDeleteResponse.fromJson(response.getBodyAsString(), response));
276+
}
202277
}

0 commit comments

Comments
 (0)