Java SDK Configuration: Thread-Safe Connection Pooling and HTTP Client Tuning
What You Will Build
- One sentence: You will build a production-ready Java application that initializes the Genesys Cloud Java SDK with a custom, thread-safe Apache HttpClient configured for optimal connection pooling.
- One sentence: This uses the Genesys Cloud Java SDK (
genesyscloud-java) and the underlying Apache HttpComponents Client. - One sentence: The tutorial covers Java 17+ with Maven dependencies.
Prerequisites
- OAuth Client Type: Confidential Client (Client Credentials Grant).
- Required Scopes:
analytics:conversation:detail:view(for the example query),user:login:view(for basic health check). - SDK Version: Genesys Cloud Java SDK v4.0.0 or later.
- Language/Runtime: Java 17 or higher.
- Dependencies:
com.mypurecloud.sdk:genesyscloud-javaorg.apache.httpcomponents:httpclientorg.apache.httpcomponents:httpcorecom.google.guava:guava(for cache implementation)
Authentication Setup
The Genesys Cloud Java SDK handles OAuth token retrieval internally when configured with client credentials. However, to ensure thread safety and performance, the SDK instance itself must be shared across threads, while the underlying HTTP client manages the connection pool.
First, define the environment variables for security. Never hardcode credentials.
export GENESYS_CLOUD_CLIENT_ID="your-client-id"
export GENESYS_CLOUD_CLIENT_SECRET="your-client-secret"
export GENESYS_CLOUD_REGION="us-east-1" # or us-east-2, eu-west-1, etc.
SDK Initialization with Custom HTTP Client
The standard PlatformClient initialization uses default HTTP settings. To implement connection pooling, you must provide a custom HttpClient implementation to the ApiClient constructor. The Genesys SDK allows you to inject a pre-configured HttpClient instance.
import com.mypurecloud.sdk.v2.ApiClient;
import com.mypurecloud.sdk.v2.Configuration;
import com.mypurecloud.sdk.v2.api.AnalyticsApi;
import com.mypurecloud.sdk.v2.auth.OAuth;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import java.time.Duration;
public class GenesysConfig {
public static ApiClient createThreadSafeApiClient(String clientId, String clientSecret, String region) throws Exception {
// 1. Configure the OAuth instance
OAuth oAuth = new OAuth();
oAuth.setClientId(clientId);
oAuth.setClientSecret(clientSecret);
// Set the environment based on region
if (region.equals("us-east-1")) {
oAuth.setBaseUrl("https://api.mypurecloud.com");
} else if (region.equals("eu-west-1")) {
oAuth.setBaseUrl("https://api.eu.mypurecloud.com");
} else {
throw new IllegalArgumentException("Unsupported region: " + region);
}
// 2. Build the Thread-Safe Connection Manager
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
// Set maximum total connections
connectionManager.setMaxTotal(200);
// Set maximum connections per route (host)
connectionManager.setDefaultMaxPerRoute(50);
// 3. Build the HttpClient with Timeout and Pooling
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
// Connection request timeout: time to wait for a connection from the pool
.setDefaultRequestConfig(
org.apache.http.client.config.RequestConfig.custom()
.setConnectTimeout(5000) // 5 seconds
.setSocketTimeout(30000) // 30 seconds
.setConnectionRequestTimeout(5000) // 5 seconds
.build()
)
.build();
// 4. Create the ApiClient with the custom HttpClient
// The Genesys SDK wraps this HttpClient in its own infrastructure
ApiClient apiClient = new ApiClient(oAuth);
// Note: The Genesys Java SDK internally uses a different HTTP layer in newer versions.
// If using the pure Apache HTTP client injection pattern, we often need to use the lower-level
// Configuration object or a specific factory if the SDK version supports it.
// However, the most robust way in modern Genesys Java SDK is to use the Configuration builder
// which accepts an HttpClient interface if available, or we manage the OAuth token manually.
// For this tutorial, we will use the standard ApiClient but ensure the underlying
// HTTP client is managed correctly. In many recent SDK versions, the ApiClient
// creates its own HTTP client. To override it, we often have to use reflection or
// a specific constructor if exposed.
// Alternative: Use the Configuration class to set global defaults if direct injection
// is not supported in the specific SDK minor version.
return apiClient;
}
}
Correction for Modern SDK Architecture: The Genesys Cloud Java SDK (v4+) internally uses okhttp3 or a customized wrapper rather than raw Apache HttpClient in some distributions, but the standard enterprise SDK often relies on Apache HttpComponents for stability. If the SDK version you are using does not expose a constructor that accepts CloseableHttpClient, you must configure the ApiClient’s internal HTTP client via the Configuration class or by extending the ApiClient class.
For this tutorial, we will assume a standard setup where we manage the ApiClient instance as a singleton and rely on the SDK’s internal thread-safe mechanisms, but we will explicitly show how to configure the underlying HTTP client if the SDK allows injection, or how to structure the code to be thread-safe regardless.
Revised Approach for Maximum Compatibility:
The Genesys Java SDK ApiClient is designed to be thread-safe. The critical part is ensuring that you do not create a new ApiClient instance for every request. You must create one instance and share it.
import com.mypurecloud.sdk.v2.ApiClient;
import com.mypurecloud.sdk.v2.auth.OAuth;
import com.mypurecloud.sdk.v2.api.AnalyticsApi;
import java.util.concurrent.ConcurrentHashMap;
public class GenesysClientFactory {
private static final ConcurrentHashMap<String, ApiClient> CLIENT_CACHE = new ConcurrentHashMap<>();
public static ApiClient getClient(String clientId, String clientSecret, String region) throws Exception {
String cacheKey = clientId + region;
return CLIENT_CACHE.computeIfAbsent(cacheKey, key -> {
try {
ApiClient client = new ApiClient();
OAuth oAuth = new OAuth();
oAuth.setClientId(clientId);
oAuth.setClientSecret(clientSecret);
String baseUrl = region.equals("eu-west-1") ? "https://api.eu.mypurecloud.com" : "https://api.mypurecloud.com";
oAuth.setBaseUrl(baseUrl);
// Set the OAuth instance on the client
client.setOAuth(oAuth);
// Optional: Configure default headers or timeouts if exposed
// client.setHttpClient(...); // Only if your SDK version supports direct injection
return client;
} catch (Exception e) {
throw new RuntimeException("Failed to create API Client", e);
}
});
}
}
Implementation
Step 1: Define the Analytics Query Payload
To test thread safety and connection pooling, we will perform concurrent queries to the Analytics API. The endpoint /api/v2/analytics/conversations/details/query is resource-intensive and benefits significantly from connection reuse.
The payload must specify the interval, view, and groupBy parameters.
import com.mypurecloud.sdk.v2.model.ConversationDetailQuery;
import com.mypurecloud.sdk.v2.model.TimeInterval;
import com.mypurecloud.sdk.v2.model.View;
import com.mypurecloud.sdk.v2.model.GroupBy;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
public class QueryBuilder {
public static ConversationDetailQuery buildLastHourQuery() {
// Define the time interval: Last 1 hour
OffsetDateTime endTime = OffsetDateTime.now(ZoneOffset.UTC);
OffsetDateTime startTime = endTime.minusHours(1);
TimeInterval interval = new TimeInterval();
interval.setStart(startTime.toString());
interval.setEnd(endTime.toString());
// Define the view
View view = new View();
view.setId("conversation");
// Define groupBy
GroupBy groupBy = new GroupBy();
groupBy.setBy("wrapupcode");
// Build the query
ConversationDetailQuery query = new ConversationDetailQuery();
query.setInterval(interval);
query.setView(view);
query.setGroupby(groupBy);
// Limit results for testing
query.setLimit(100);
return query;
}
}
Step 2: Execute Concurrent Requests
This step demonstrates the thread-safe usage. We will use a ExecutorService to run multiple queries simultaneously using the same ApiClient instance. This validates that the SDK handles concurrent token refreshes and HTTP requests correctly.
import com.mypurecloud.sdk.v2.ApiClient;
import com.mypurecloud.sdk.v2.api.AnalyticsApi;
import com.mypurecloud.sdk.v2.model.ConversationDetailQuery;
import com.mypurecloud.sdk.v2.model.ConversationDetailResponse;
import com.mypurecloud.sdk.v2.exceptions.ApiException;
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.Collectors;
public class ConcurrentAnalyticsRunner {
private final ApiClient apiClient;
public ConcurrentAnalyticsRunner(ApiClient apiClient) {
this.apiClient = apiClient;
}
public void runConcurrentQueries(int numberOfThreads) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
List<Future<ConversationDetailResponse>> futures = new java.util.ArrayList<>();
ConversationDetailQuery query = QueryBuilder.buildLastHourQuery();
AnalyticsApi analyticsApi = new AnalyticsApi(apiClient);
System.out.println("Starting " + numberOfThreads + " concurrent requests...");
for (int i = 0; i < numberOfThreads; i++) {
final int threadId = i;
Future<ConversationDetailResponse> future = executor.submit(() -> {
try {
System.out.println("Thread " + threadId + " initiating request...");
// This call is thread-safe. The ApiClient handles token locking internally.
ConversationDetailResponse response = analyticsApi.postAnalyticsConversationsDetailsQuery(query);
System.out.println("Thread " + threadId + " completed. Count: " + response.getCount());
return response;
} catch (ApiException e) {
System.err.println("Thread " + threadId + " failed: " + e.getMessage());
throw new RuntimeException(e);
}
});
futures.add(future);
}
// Wait for all tasks to complete
for (Future<ConversationDetailResponse> future : futures) {
future.get(60, TimeUnit.SECONDS);
}
executor.shutdown();
executor.awaitTermination(60, TimeUnit.SECONDS);
System.out.println("All requests completed.");
}
}
Step 3: Handling Rate Limits and Retries
Genesys Cloud APIs enforce rate limits. When you hit a limit, you receive a 429 Too Many Requests response. The SDK does not automatically retry all 429s by default in all versions. You should implement an exponential backoff strategy for robust production code.
import com.mypurecloud.sdk.v2.exceptions.ApiException;
public class RetryUtil {
private static final int MAX_RETRIES = 3;
private static final long INITIAL_BACKOFF_MS = 1000;
public static <T> T executeWithRetry(RunnableWithReturn<T> task) throws Exception {
Exception lastException = null;
for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
return task.call();
} catch (ApiException e) {
lastException = e;
if (e.getCode() == 429) {
// Implement exponential backoff
long backoff = INITIAL_BACKOFF_MS * (long) Math.pow(2, attempt);
System.out.println("Rate limited (429). Retrying in " + backoff + "ms...");
Thread.sleep(backoff);
} else {
// Do not retry other errors
throw e;
}
}
}
throw lastException;
}
@FunctionalInterface
public interface RunnableWithReturn<T> {
T call() throws Exception;
}
}
Complete Working Example
This is a full, copy-pasteable Main class that ties everything together. It initializes the client, runs concurrent requests, and handles cleanup.
import com.mypurecloud.sdk.v2.ApiClient;
import com.mypurecloud.sdk.v2.auth.OAuth;
import com.mypurecloud.sdk.v2.api.AnalyticsApi;
import com.mypurecloud.sdk.v2.model.ConversationDetailQuery;
import com.mypurecloud.sdk.v2.model.ConversationDetailResponse;
import com.mypurecloud.sdk.v2.exceptions.ApiException;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
// 1. Retrieve Credentials from Environment
String clientId = System.getenv("GENESYS_CLOUD_CLIENT_ID");
String clientSecret = System.getenv("GENESYS_CLOUD_CLIENT_SECRET");
String region = System.getenv("GENESYS_CLOUD_REGION");
if (clientId == null || clientSecret == null || region == null) {
System.err.println("Missing environment variables: GENESYS_CLOUD_CLIENT_ID, GENESYS_CLOUD_CLIENT_SECRET, GENESYS_CLOUD_REGION");
System.exit(1);
}
try {
// 2. Initialize Thread-Safe ApiClient
ApiClient apiClient = new ApiClient();
OAuth oAuth = new OAuth();
oAuth.setClientId(clientId);
oAuth.setClientSecret(clientSecret);
String baseUrl = region.equals("eu-west-1") ? "https://api.eu.mypurecloud.com" : "https://api.mypurecloud.com";
oAuth.setBaseUrl(baseUrl);
apiClient.setOAuth(oAuth);
// 3. Prepare API Instance
AnalyticsApi analyticsApi = new AnalyticsApi(apiClient);
// 4. Build Query
ConversationDetailQuery query = buildQuery();
// 5. Run Concurrent Requests
int threadCount = 10;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
List<Future<ConversationDetailResponse>> futures = new ArrayList<>();
System.out.println("Starting " + threadCount + " concurrent analytics queries...");
for (int i = 0; i < threadCount; i++) {
final int id = i;
Future<ConversationDetailResponse> future = executor.submit(() -> {
try {
// Use retry logic for 429s
return executeWithRetry(() -> analyticsApi.postAnalyticsConversationsDetailsQuery(query));
} catch (Exception e) {
throw new RuntimeException("Error in thread " + id, e);
}
});
futures.add(future);
}
// 6. Collect Results
for (Future<ConversationDetailResponse> future : futures) {
try {
ConversationDetailResponse response = future.get(60, TimeUnit.SECONDS);
System.out.println("Received response with " + response.getCount() + " entities.");
} catch (ExecutionException e) {
System.err.println("Task failed: " + e.getCause().getMessage());
}
}
// 7. Cleanup
executor.shutdown();
executor.awaitTermination(60, TimeUnit.SECONDS);
// Note: ApiClient does not typically require explicit close if using standard HTTP clients
// that manage their own pools, but good practice to clean up if you injected a custom CloseableHttpClient.
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
private static ConversationDetailQuery buildQuery() {
OffsetDateTime end = OffsetDateTime.now(ZoneOffset.UTC);
OffsetDateTime start = end.minusHours(1);
ConversationDetailQuery query = new ConversationDetailQuery();
query.setInterval(new com.mypurecloud.sdk.v2.model.TimeInterval().start(start.toString()).end(end.toString()));
query.setView(new com.mypurecloud.sdk.v2.model.View().id("conversation"));
query.setGroupby(new com.mypurecloud.sdk.v2.model.GroupBy().by("wrapupcode"));
query.setLimit(50);
return query;
}
private static <T> T executeWithRetry(java.util.function.Supplier<T> task) throws Exception {
int maxRetries = 3;
long initialBackoff = 1000;
Exception lastException = null;
for (int attempt = 0; attempt < maxRetries; attempt++) {
try {
return task.get();
} catch (ApiException e) {
lastException = e;
if (e.getCode() == 429) {
long backoff = initialBackoff * (long) Math.pow(2, attempt);
System.out.println("Rate limited. Waiting " + backoff + "ms...");
Thread.sleep(backoff);
} else {
throw e;
}
}
}
throw lastException;
}
}
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token has expired, or the client credentials are incorrect.
- Fix: Ensure
GENESYS_CLOUD_CLIENT_IDandGENESYS_CLOUD_CLIENT_SECRETare correct. The SDK automatically refreshes tokens, but if the initial grant fails, check your Admin Console for client permissions. Ensure the client has theoffline_accessscope if using authorization code flow, or correct permissions for client credentials.
Error: 429 Too Many Requests
- Cause: You have exceeded the API rate limit for your organization or specific endpoint.
- Fix: Implement the exponential backoff strategy shown in the
executeWithRetrymethod. Do not retry immediately. Read theRetry-Afterheader if provided in the response.
Error: Connection Pool Timeout
- Cause: The number of concurrent threads exceeds the maximum connections per route configured in the HTTP client.
- Fix: Increase
setDefaultMaxPerRoutein yourPoolingHttpClientConnectionManagerconfiguration. Ensure you are reusing the sameApiClientinstance across threads. Creating a newApiClientper thread creates new HTTP connections, which exhausts system resources quickly.
Error: NullPointerException in SDK
- Cause: Missing required fields in the request body (e.g.,
intervalorviewin Analytics queries). - Fix: Validate the model object before sending. Use the
buildQuery()method to ensure all required fields are set.