Recommended practices for configuring, running, and maintaining the Java SDK.
Keep the SDK up to date
Always use a recent version of the SDK to ensure you have the latest features, performance improvements, and security fixes.
To stay informed about new releases:
- Subscribe to the GitHub releases feed
- Watch the GitHub repository
Store credentials securely
Never hardcode credentials such as
clientId and clientSecret in your source code. Always load them from environment variables or a secrets management service at runtime.String clientId = System.getenv("CTP_CLIENT_ID");
String clientSecret = System.getenv("CTP_CLIENT_SECRET");
String projectKey = System.getenv("CTP_PROJECT_KEY");
ProjectApiRoot apiRoot = ApiRootBuilder.of()
.defaultClient(
ClientCredentials.of()
.withClientId(clientId)
.withClientSecret(clientSecret)
.build(),
ServiceRegion.GCP_EUROPE_WEST1
)
.build(projectKey);
Reuse the API client
The
ProjectApiRoot (and equivalent root objects for other APIs) is thread-safe and designed to be created once and reused across your application. Creating a new client per request is wasteful. Each client holds its own thread pool and connection pool.Create a single instance and inject or share it through your application:
ProjectApiRoot apiRoot = ApiRootBuilder.of()
.defaultClient(
ClientCredentials.of()
.withClientId(System.getenv("CTP_CLIENT_ID"))
.withClientSecret(System.getenv("CTP_CLIENT_SECRET"))
.build(),
ServiceRegion.GCP_EUROPE_WEST1
)
.build(System.getenv("CTP_PROJECT_KEY"));
// Reuse apiRoot throughout the application lifetime
For Spring applications, declare the client as a
@Bean so the framework manages its lifecycle.Close the client
The Java SDK client holds resources including thread pools and IO connections. The
ApiRoot classes implement the Closeable interface and must be closed when your application shuts down.Explicit close
Call
close() explicitly when your application shuts down:// Close the client to release thread pools and IO connections
apiRoot.getApiHttpClient().close();
For Spring, use
@PreDestroy or register a shutdown hook:@PreDestroy
public void closeClient() {
apiRoot.getApiHttpClient().close();
}
try-with-resources
For short-lived operations (such as scripts or batch jobs), use a try-with-resources block to automatically close the client:
try (ProjectApiRoot apiRoot = ApiRootBuilder.of()
.defaultClient(
ClientCredentials.of()
.withClientId(System.getenv("CTP_CLIENT_ID"))
.withClientSecret(System.getenv("CTP_CLIENT_SECRET"))
.build(),
ServiceRegion.GCP_EUROPE_WEST1
)
.build(System.getenv("CTP_PROJECT_KEY"))) {
// Perform operations
Project project = apiRoot.get().executeBlocking().getBody();
System.out.println(project.getKey());
} // client is automatically closed here
Handle idle and stale connections
Long-running services that reuse pooled HTTP connections can see
Connection reset errors when a connection sits idle long enough for a load balancer, firewall, or NAT to silently close it before the client notices and tries to reuse it.The default client (
commercetools-http-client), commercetools-apachehttp-client, commercetools-okhttp-client5, commercetools-okhttp-client4, and commercetools-reactornetty-client all expose connection pool configuration to handle this. commercetools-javanet-client wraps Java's built-in java.net.http.HttpClient, which has no per-client option to configure idle connection eviction or validation (only JVM-wide system properties such as jdk.httpclient.keepalive.timeout), so avoid it for production services with intermittent traffic.VrapHttpClient httpClient = new CtOkHttp4Client(
builder -> builder.connectionPool(new okhttp3.ConnectionPool(5, 30, TimeUnit.SECONDS))
);
ProjectApiRoot apiRoot = ApiRootBuilder.of(httpClient)
.defaultClient(
ClientCredentials.of()
.withClientId(System.getenv("CTP_CLIENT_ID"))
.withClientSecret(System.getenv("CTP_CLIENT_SECRET"))
.build(),
ServiceRegion.GCP_EUROPE_WEST1
)
.build(System.getenv("CTP_PROJECT_KEY"));
For
commercetools-apachehttp-client, configure PoolingAsyncClientConnectionManagerBuilder with a ConnectionConfig that validates connections after a period of inactivity.For guidance on choosing among the other HTTP client modules, see Configure the underlying HTTP client.
Handle errors safely
The SDK throws exceptions that extend ApiHttpException for HTTP status codes of
400 or higher, including narrower subclasses such as BadRequestException, NotFoundException, and ConcurrentModificationException. Catch these in a global exception handler and map them to your own response. Avoid returning the caught exception object directly, since it also carries the original request and response and can expose internal details to API consumers.@RestControllerAdvice
public class ErrorHandler {
@ExceptionHandler(ApiHttpException.class)
public ResponseEntity<Map<String, String>> handleApiHttpException(ApiHttpException ex) {
return ResponseEntity
.status(ex.getStatusCode())
.body(Map.of("message", "The commercetools API request failed."));
}
}
Add narrower
@ExceptionHandler methods for specific subclasses, such as NotFoundException or ConcurrentModificationException, if you need different handling per error type.Troubleshooting
CompletionException wraps the original exception
The future returned by
execute() completes exceptionally when the API returns an error response. Handlers attached to that future, such as exceptionally(), and calls to join() receive the SDK exception wrapped in a CompletionException. This is standard CompletableFuture behavior and applies to every ExceptionMode. Call getCause() to reach the original exception, such as NotFoundException:apiRoot.get().execute().exceptionally(throwable -> {
Throwable cause = throwable instanceof CompletionException ? throwable.getCause() : throwable;
if (cause instanceof NotFoundException) {
// handle NotFoundException
}
return null;
});
ApiHttpResponse.getMessage() returns HTTP status text
ApiHttpResponse is an abstraction over the raw HTTP transfer. Its getMessage() method returns the HTTP status reason phrase (such as OK, Bad Request, or Not Found), not the response body.When a request fails, the ErrorMiddleware throws an exception whose message contains the HTTP body. The thrown exceptions extend ApiHttpException, which provides a
getBodyAs() helper to deserialize the error body:apiRoot.products().post(draft).execute()
.exceptionally(throwable -> {
Throwable cause = throwable instanceof CompletionException ? throwable.getCause() : throwable;
if (cause instanceof BadRequestException) {
// BadRequestException and ConcurrentModificationException automatically
// deserialize the error response
String message = ((BadRequestException) cause)
.getErrorResponse()
.getMessage();
}
if (cause instanceof ApiHttpException) {
// For all other API errors, use getBodyAs() to deserialize manually
String message = ((ApiHttpException) cause)
.getBodyAs(ErrorResponse.class)
.getMessage();
}
return null;
});
Further resources
- Middleware reference: configure error handling, retry, logging, and more
- Customization guide: configure proxies, timeouts, HTTP client, and authentication flows
- Integration tests: working examples for common SDK operations