Genesys Cloud Java SDK: Configuring Thread-Safe Connection Pooling for High-Throughput Integrations
What You Will Build
- You will create a thread-safe Genesys Cloud Java client instance that utilizes a shared HTTP connection pool to handle concurrent API requests efficiently.
- This tutorial uses the Genesys Cloud PureCloud Platform Client V2 Java SDK (
genesyscloud-platform-client-java). - The implementation covers Java 11+ with explicit configuration of the underlying Apache HttpClient for optimal resource management in multi-threaded environments.
Prerequisites
OAuth and Scopes
- Client Type: Confidential Client (Client Credentials Grant).
- Required Scopes:
analytics:report:read(for the example query),user:read(for identity verification). - Environment: Genesys Cloud Production or Sandbox environment.
SDK and Runtime Requirements
- SDK Version:
genesyscloud-platform-client-javaversion127.0.0or higher. - Java Version: JDK 11 or later (LTS recommended).
- Build Tool: Maven or Gradle.
Dependencies
Add the following dependency to your pom.xml if using Maven:
<dependency>
<groupId>com.mypurecloud</groupId>
<artifactId>genesyscloud-platform-client-java</artifactId>
<version>127.0.0</version>
</dependency>
If using Gradle:
implementation 'com.mypurecloud:genesyscloud-platform-client-java:127.0.0'
The SDK relies on Apache HttpClient under the hood. You do not need to add it manually as it is a transitive dependency, but understanding its behavior is critical for this configuration.
Authentication Setup
In a multi-threaded application, you must never share a single PureCloudPlatformClientV2 instance across threads if that instance holds mutable authentication state. However, the underlying HTTP client can and should be shared.
The standard PureCloudPlatformClientV2 constructor initializes a new ApiClient which creates a new CloseableHttpClient. Creating a new HTTP client for every request or every thread leads to socket exhaustion and thread contention.
To solve this, we separate the HTTP Client configuration from the Platform Client instance. We create a shared, thread-safe CloseableHttpClient with a custom connection pool manager, and then inject this client into our ApiClient configuration.
Step 1: Create a Shared, Thread-Safe HTTP Client
We will configure an PoolingHttpClientConnectionManager. This manager maintains a pool of connections that are reused across threads. It is thread-safe by design.
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.conn.routing.HttpRoutePlanner;
import org.apache.http.impl.conn.SystemDefaultRoutePlanner;
import java.io.IOException;
public class GenesysHttpClientFactory {
/**
* Creates a shared, thread-safe HttpClient with optimized connection pooling.
*
* @param maxTotalConnections Maximum total connections in the pool
* @param maxPerRoute Maximum connections per specific host/route
* @return Configured CloseableHttpClient
*/
public static CloseableHttpClient createPooledHttpClient(int maxTotalConnections, int maxPerRoute) {
// 1. Initialize the connection manager
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
// 2. Set pool limits
// For high-throughput analytics queries, 100-200 total is common.
// Per-route limits prevent one endpoint from starving others.
connectionManager.setMaxTotal(maxTotalConnections);
connectionManager.setDefaultMaxPerRoute(maxPerRoute);
// 3. Build the client with the custom manager
return HttpClients.custom()
.setConnectionManager(connectionManager)
// Disable automatic connection release to allow the SDK to manage it,
// or rely on the pool's eviction policy.
.disableAutomaticRetries() // SDK handles retries for 429s; let us control it
.build();
}
}
Why this matters: By default, HttpClients.createDefault() uses a single-connection pool. If you spin up 50 threads to fetch user data, they will queue up behind one TCP connection, serializing your requests. PoolingHttpClientConnectionManager allows parallel execution.
Implementation
Step 2: Configure the ApiClient with the Shared HTTP Client
The Genesys Cloud Java SDK provides a builder pattern for ApiClient. We will inject our pooled HTTP client here. We also need to configure the OAuth token provider.
For this tutorial, we assume a static token refresh mechanism for simplicity, but in production, you should implement a thread-safe token cache that refreshes before expiry.
import com.mypurecloud.api.client.ApiClient;
import com.mypurecloud.api.client.Configuration;
import com.mypurecloud.api.client.auth.OAuth;
import com.mypurecloud.api.client.auth.OAuthFlow;
import com.mypurecloud.api.client.auth.OAuthFlowType;
import org.apache.http.impl.client.CloseableHttpClient;
public class GenesysSdkConfig {
private final CloseableHttpClient pooledHttpClient;
private final String clientId;
private final String clientSecret;
private final String environment; // e.g., "us-east-1"
public GenesysSdkConfig(String clientId, String clientSecret, String environment, CloseableHttpClient pooledHttpClient) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.environment = environment;
this.pooledHttpClient = pooledHttpClient;
}
/**
* Creates a configured ApiClient that uses the shared HTTP pool.
* Note: The ApiClient itself is NOT thread-safe for authentication state.
* Each thread or logical unit of work should get its own ApiClient instance,
* but they ALL share the underlying pooledHttpClient.
*/
public ApiClient createApiClient() throws Exception {
// 1. Initialize the base configuration
Configuration configuration = Configuration.getDefaultConfiguration();
// 2. Set the environment (e.g., https://api.mypurecloud.com for us-east-1)
String baseUrl = "https://api." + environment + ".mypurecloud.com";
configuration.setBasePath(baseUrl);
// 3. Create the OAuth provider
OAuth oauth = new OAuth();
oauth.setClientId(clientId);
oauth.setClientSecret(clientSecret);
oauth.setFlow(OAuthFlow.CLIENT_CREDENTIALS);
// 4. Create the ApiClient with the SHARED HTTP client
// This is the critical step for connection pooling.
ApiClient apiClient = new ApiClient(configuration);
apiClient.setHttpClient(pooledHttpClient);
// 5. Attach the OAuth provider
// The SDK will use this to attach the Bearer token to requests.
apiClient.setAuthentications(java.util.Collections.singletonMap("default", oauth));
return apiClient;
}
}
Critical Distinction: The ApiClient object manages the current access token. If two threads modify the token on the same ApiClient instance simultaneously, you risk race conditions. Therefore, create a new ApiClient instance per thread/request, but inject the same pooledHttpClient into all of them.
Step 3: Implementing Thread-Safe Analytics Queries
We will now build a service that queries conversation analytics. This is a heavy operation that benefits from connection pooling. We will use an ExecutorService to demonstrate concurrent requests.
The endpoint used is POST /api/v2/analytics/conversations/details/query.
import com.mypurecloud.api.client.ApiClient;
import com.mypurecloud.api.client.ApiException;
import com.mypurecloud.api.client.Configuration;
import com.mypurecloud.api.client.auth.OAuth;
import com.mypurecloud.api.client.auth.OAuthFlow;
import com.mypurecloud.api.client.api.AnalyticsApi;
import com.mypurecloud.api.client.model.ConversationsDetailsQuery;
import com.mypurecloud.api.client.model.ConversationsDetailsResponse;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ConcurrentAnalyticsService {
private final GenesysSdkConfig sdkConfig;
private final ExecutorService executorService;
public ConcurrentAnalyticsService(GenesysSdkConfig sdkConfig, int threadPoolSize) {
this.sdkConfig = sdkConfig;
// Use a fixed thread pool to control concurrency against the connection pool
this.executorService = Executors.newFixedThreadPool(threadPoolSize);
}
/**
* Queries analytics for a specific time slice.
* Each call creates a new ApiClient instance but shares the HTTP pool.
*/
public ConversationsDetailsResponse fetchConversationDetails(OffsetDateTime startTime, OffsetDateTime endTime, String viewId) throws Exception {
// Create a fresh ApiClient for this thread/task
ApiClient apiClient = sdkConfig.createApiClient();
try {
AnalyticsApi analyticsApi = new AnalyticsApi(apiClient);
// Build the query body
ConversationsDetailsQuery query = new ConversationsDetailsQuery();
query.setInterval(startTime.toString(), endTime.toString());
query.setViewId(viewId);
query.setGroupBy(List.of("mediaType"));
// Execute the request
// The SDK handles serialization and HTTP execution using the shared pool
return analyticsApi.postAnalyticsConversationsDetailsQuery(query);
} catch (ApiException e) {
handleApiException(e);
throw e;
} finally {
// Close the ApiClient to release any temporary resources
// Note: This does NOT close the shared HttpClient
apiClient.close();
}
}
private void handleApiException(ApiException e) {
System.err.println("API Error: " + e.getCode() + " - " + e.getMessage());
if (e.hasResponseData()) {
System.err.println("Response Data: " + e.getResponseData());
}
}
public void shutdown() {
executorService.shutdown();
try {
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
Step 4: Orchestrating Concurrent Requests
Here is the main execution block that ties everything together. We define a date range, split it into chunks, and process them concurrently.
import com.mypurecloud.api.client.model.ConversationsDetailsResponse;
import org.apache.http.impl.client.CloseableHttpClient;
import java.time.LocalDateTime;
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. Setup Credentials
String clientId = "YOUR_CLIENT_ID";
String clientSecret = "YOUR_CLIENT_SECRET";
String environment = "us-east-1"; // Example environment
// 2. Create the Shared HTTP Client (Thread-Safe)
// Pool of 100 connections total, 20 per route
CloseableHttpClient sharedHttpClient = GenesysHttpClientFactory.createPooledHttpClient(100, 20);
// 3. Initialize SDK Config
GenesysSdkConfig sdkConfig = new GenesysSdkConfig(clientId, clientSecret, environment, sharedHttpClient);
// 4. Initialize Service with 10 concurrent workers
ConcurrentAnalyticsService service = new ConcurrentAnalyticsService(sdkConfig, 10);
// 5. Define Query Parameters
String viewId = "YOUR_ANALYTICS_VIEW_ID"; // Must exist in your org
OffsetDateTime start = LocalDateTime.now().minusDays(1).atOffset(ZoneOffset.UTC);
OffsetDateTime end = LocalDateTime.now().atOffset(ZoneOffset.UTC);
// Split the day into 24 one-hour chunks
List<OffsetDateTime> timeSlots = new ArrayList<>();
OffsetDateTime current = start;
while (current.isBefore(end)) {
timeSlots.add(current);
current = current.plusHours(1);
}
System.out.println("Starting concurrent analytics fetch for " + timeSlots.size() + " time slots...");
long startTimeMillis = System.currentTimeMillis();
// 6. Submit tasks to the ExecutorService
List<Future<ConversationsDetailsResponse>> futures = new ArrayList<>();
for (OffsetDateTime slotStart : timeSlots) {
OffsetDateTime slotEnd = slotStart.plusHours(1);
// Submit the task. The service creates a new ApiClient for this task,
// but uses the shared HttpClient pool.
Future<ConversationsDetailsResponse> future = service.executorService.submit(() -> {
return service.fetchConversationDetails(slotStart, slotEnd, viewId);
});
futures.add(future);
}
// 7. Collect Results
int totalConversations = 0;
for (Future<ConversationsDetailsResponse> future : futures) {
try {
ConversationsDetailsResponse response = future.get(10, TimeUnit.SECONDS);
if (response != null && response.getGroups() != null) {
for (var group : response.getGroups()) {
totalConversations += group.getConversationsCount();
}
}
} catch (InterruptedException | ExecutionException | TimeoutException e) {
System.err.println("Failed to process a time slot: " + e.getMessage());
}
}
long duration = System.currentTimeMillis() - startTimeMillis;
System.out.println("Completed. Total Conversations: " + totalConversations + " in " + duration + "ms.");
// 8. Cleanup
service.shutdown();
try {
sharedHttpClient.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Complete Working Example
Below is the consolidated code structure. Ensure you replace YOUR_CLIENT_ID, YOUR_CLIENT_SECRET, and YOUR_ANALYTICS_VIEW_ID with valid values from your Genesys Cloud organization.
package com.example.genesys.pooling;
import com.mypurecloud.api.client.ApiClient;
import com.mypurecloud.api.client.ApiException;
import com.mypurecloud.api.client.Configuration;
import com.mypurecloud.api.client.auth.OAuth;
import com.mypurecloud.api.client.auth.OAuthFlow;
import com.mypurecloud.api.client.api.AnalyticsApi;
import com.mypurecloud.api.client.model.ConversationsDetailsQuery;
import com.mypurecloud.api.client.model.ConversationsDetailsResponse;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class GenesysPoolingDemo {
// --- Configuration Classes ---
public static class GenesysSdkConfig {
private final String clientId;
private final String clientSecret;
private final String environment;
private final CloseableHttpClient pooledHttpClient;
public GenesysSdkConfig(String clientId, String clientSecret, String environment, CloseableHttpClient pooledHttpClient) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.environment = environment;
this.pooledHttpClient = pooledHttpClient;
}
public ApiClient createApiClient() {
Configuration configuration = Configuration.getDefaultConfiguration();
configuration.setBasePath("https://api." + environment + ".mypurecloud.com");
OAuth oauth = new OAuth();
oauth.setClientId(clientId);
oauth.setClientSecret(clientSecret);
oauth.setFlow(OAuthFlow.CLIENT_CREDENTIALS);
ApiClient apiClient = new ApiClient(configuration);
apiClient.setHttpClient(pooledHttpClient);
apiClient.setAuthentications(java.util.Collections.singletonMap("default", oauth));
return apiClient;
}
}
// --- Service Layer ---
public static class AnalyticsService {
private final GenesysSdkConfig sdkConfig;
private final ExecutorService executor;
public AnalyticsService(GenesysSdkConfig sdkConfig, int threads) {
this.sdkConfig = sdkConfig;
this.executor = Executors.newFixedThreadPool(threads);
}
public ConversationsDetailsResponse querySlot(OffsetDateTime start, OffsetDateTime end, String viewId) throws Exception {
ApiClient apiClient = sdkConfig.createApiClient();
try {
AnalyticsApi api = new AnalyticsApi(apiClient);
ConversationsDetailsQuery query = new ConversationsDetailsQuery();
query.setInterval(start.toString(), end.toString());
query.setViewId(viewId);
return api.postAnalyticsConversationsDetailsQuery(query);
} finally {
apiClient.close();
}
}
public void shutdown() {
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
// --- Main Execution ---
public static void main(String[] args) {
String clientId = "YOUR_CLIENT_ID";
String clientSecret = "YOUR_CLIENT_SECRET";
String environment = "us-east-1";
String viewId = "YOUR_ANALYTICS_VIEW_ID";
// 1. Create Thread-Safe HTTP Client with Pooling
PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
connManager.setMaxTotal(100);
connManager.setDefaultMaxPerRoute(20);
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connManager)
.disableAutomaticRetries()
.build();
// 2. Initialize Config and Service
GenesysSdkConfig config = new GenesysSdkConfig(clientId, clientSecret, environment, httpClient);
AnalyticsService service = new AnalyticsService(config, 10); // 10 threads
// 3. Prepare Data
OffsetDateTime start = LocalDateTime.now().minusDays(1).atOffset(ZoneOffset.UTC);
OffsetDateTime end = LocalDateTime.now().atOffset(ZoneOffset.UTC);
List<Future<ConversationsDetailsResponse>> futures = new ArrayList<>();
// 4. Submit Concurrent Tasks
OffsetDateTime current = start;
while (current.isBefore(end)) {
OffsetDateTime slotStart = current;
OffsetDateTime slotEnd = current.plusHours(1);
futures.add(service.executor.submit(() -> service.querySlot(slotStart, slotEnd, viewId)));
current = slotEnd;
}
// 5. Aggregate Results
int totalCount = 0;
for (Future<ConversationsDetailsResponse> f : futures) {
try {
ConversationsDetailsResponse resp = f.get(5, TimeUnit.SECONDS);
if (resp.getGroups() != null) {
for (var g : resp.getGroups()) {
totalCount += g.getConversationsCount();
}
}
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
}
}
System.out.println("Total Conversations: " + totalCount);
service.shutdown();
try {
httpClient.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Common Errors & Debugging
Error: java.net.SocketTimeoutException: Read timed out
Cause: The connection pool is exhausted, or the server is not responding within the default timeout. In high-concurrency scenarios, if maxTotalConnections is too low, threads will block waiting for a socket. If they block too long, the read timeout triggers.
Fix:
- Increase
maxTotalConnectionsinPoolingHttpClientConnectionManager. - Increase the socket timeout in the
HttpClientbuilder:.setDefaultRequestConfig(RequestConfig.custom() .setSocketTimeout(30000) // 30 seconds .setConnectTimeout(5000) // 5 seconds .build())
Error: 429 Too Many Requests
Cause: Genesys Cloud enforces rate limits per client ID and per endpoint. Even with connection pooling, if you fire 100 requests simultaneously, you may hit the API rate limit.
Fix:
Implement exponential backoff in your retry logic. The Genesys SDK does not automatically retry 429s by default in all configurations. You should catch ApiException with code 429 and wait before retrying.
if (e.getCode() == 429) {
int retryAfter = e.getHeaders().getOrDefault("Retry-After", "5");
Thread.sleep(Integer.parseInt(retryAfter) * 1000);
// Retry logic here
}
Error: java.lang.IllegalStateException: Connection pool shut down
Cause: You closed the CloseableHttpClient while threads were still using it. The HttpClient is shared; closing it invalidates all connections in the pool.
Fix:
Ensure the httpClient.close() call happens only after executorService.shutdown() and awaitTermination() complete successfully. Never close the shared client until the application is fully shutting down.
Error: 401 Unauthorized or 403 Forbidden
Cause: The OAuth token expired during the execution of a long-running batch job. The OAuth object in the SDK handles token refresh, but if the refresh fails or the token is invalid for the specific scope, you get this error.
Fix:
Verify that the clientId and clientSecret are correct. Ensure the OAuth client in Genesys Cloud has the analytics:report:read scope enabled. If using a long-running job, consider implementing a custom OAuth provider that logs token refresh events for debugging.