Java SDK best practices

Ask about this Page
Copy for AI
View as Markdown

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:

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.
Load credentials from environment variablesjava
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:

Create the API client once at application startupjava
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 API clientjava
// Close the client to release thread pools and IO connections
apiRoot.getApiHttpClient().close();
For Spring, use @PreDestroy or register a shutdown hook:
Close the API client with @PreDestroyjava
@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:

Close the API client with try-with-resourcesjava
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.
Configure connection pooling with OkHttpjava
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.
Map SDK exceptions to a safe responsejava
@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:
Unwrap CompletionException to reach the SDK exceptionjava
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:
Handle API errorsjava
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