Genesys Cloud Java SDK: Configuring Thread-Safe Connection Pooling
What You Will Build
- You will build a production-ready Java application that initializes the Genesys Cloud PureCloud Platform Client with explicit HTTP connection pooling and thread safety.
- You will use the official
com.mypurecloud.api:platform-java-sdklibrary to interact with the Genesys Cloud REST API. - You will configure the underlying Apache HttpClient to manage concurrent requests efficiently, preventing connection exhaustion in multi-threaded environments.
Prerequisites
- OAuth Client Type: Service Account (Client Credentials Grant).
- Required Scopes:
analytics:query:read(for the example query) oruser:read(for user listing). - SDK Version:
platform-java-sdkv130.0.0 or later. - Language/Runtime: Java 11 or later.
- Build Tool: Maven or Gradle.
- External Dependencies:
com.mypurecloud.api:platform-java-sdkorg.apache.httpcomponents:httpclient(transitive dependency, but explicit control is recommended)com.google.code.gson:gson(for JSON parsing if needed outside SDK models)
Authentication Setup
The Genesys Cloud Java SDK handles OAuth2 token acquisition automatically when configured with a Service Account. However, the default ApiClient instance is not thread-safe by default if shared across threads without proper configuration. To ensure thread safety and efficient connection reuse, you must configure the underlying HttpClient before making API calls.
Step 1: Define the API Client Configuration
The core of the thread-safe configuration lies in the ApiClient class. By default, the SDK creates a new HttpClient instance for each ApiClient if not explicitly provided. To enable connection pooling, you must create a shared CloseableHttpClient with a PoolingHttpClientConnectionManager and pass it to the ApiClient.
import com.mypurecloud.api.client.ApiClient;
import com.mypurecloud.api.client.Configuration;
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.HttpRoute;
import java.time.Duration;
public class GenesysConfig {
private static final String ENVIRONMENT = "mypurecloud.com"; // Use "us.genesys.cloud" for US region
public static ApiClient createThreadSafeApiClient() {
// 1. Configure Connection Pooling
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
// Set maximum total connections
connectionManager.setMaxTotal(100);
// Set maximum connections per route (host)
connectionManager.setDefaultMaxPerRoute(20);
// Optional: Set specific limits for the API host
connectionManager.setMaxPerRoute(new HttpRoute("api." + ENVIRONMENT), 50);
// 2. Configure the HttpClient with the Pool
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.setConnectionTimeToLive(Duration.ofMinutes(5)) // Keep connections alive for 5 minutes
.setKeepAliveStrategy((response, context) -> Duration.ofMinutes(5).toMillis())
.build();
// 3. Initialize the ApiClient
ApiClient apiClient = new ApiClient();
// 4. Inject the Custom HttpClient
// The SDK allows setting the HTTP client directly. This ensures all API calls share the pool.
apiClient.setHttpClient(httpClient);
// 5. Set the Base Path
apiClient.setBasePath("https://api." + ENVIRONMENT);
return apiClient;
}
}
Why this matters:
Without this configuration, each thread or request might open a new TCP connection to api.mypurecloud.com. Genesys Cloud imposes rate limits and connection limits. A sudden burst of concurrent requests can exhaust available file descriptors or hit the server’s connection limit, resulting in 429 Too Many Requests or connection timeouts. The PoolingHttpClientConnectionManager reuses idle connections, reducing latency and server load.
Step 2: Configure OAuth Service Account
You must configure the ApiClient with your Service Account credentials. The SDK uses the Configuration class to store these globally or per-client.
import com.mypurecloud.api.client.Configuration;
import com.mypurecloud.api.client.AuthService;
public class GenesysAuth {
private static final String CLIENT_ID = "YOUR_CLIENT_ID";
private static final String CLIENT_SECRET = "YOUR_CLIENT_SECRET";
public static void configureOAuth(ApiClient apiClient) {
// Use the built-in OAuth service for Client Credentials Grant
AuthService authService = new AuthService(apiClient);
try {
// This call fetches the token and caches it internally
// The SDK handles token refresh automatically when it expires
authService.authenticate(CLIENT_ID, CLIENT_SECRET);
} catch (Exception e) {
throw new RuntimeException("Failed to authenticate with Genesys Cloud", e);
}
}
}
Critical Note on Thread Safety:
The AuthService and ApiClient instances configured above are thread-safe for reading the token and making HTTP requests. Do not modify the ApiClient configuration (e.g., changing basePath or httpClient) after it has been used in a thread. Create the ApiClient once, configure it, and share it across threads.
Implementation
Step 1: Create a Thread-Safe API Service
We will create a service class that uses the configured ApiClient to make API calls. This service will be instantiated once and used by multiple threads.
import com.mypurecloud.api.client.ApiClient;
import com.mypurecloud.api.v2.analytics.AnalyticsApi;
import com.mypurecloud.api.v2.analytics.model.ConversationDetailsQuery;
import com.mypurecloud.api.v2.analytics.model.ConversationDetailsResponse;
import com.mypurecloud.api.client.PureCloudException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AnalyticsService {
private final AnalyticsApi analyticsApi;
private final ExecutorService executor;
public AnalyticsService(ApiClient apiClient) {
this.analyticsApi = new AnalyticsApi(apiClient);
// Create a fixed thread pool to simulate concurrent requests
this.executor = Executors.newFixedThreadPool(10);
}
/**
* Query conversation details asynchronously.
* This method is thread-safe because AnalyticsApi uses the shared, pooled ApiClient.
*/
public CompletableFuture<ConversationDetailsResponse> queryConversationsAsync(ConversationDetailsQuery query) {
return CompletableFuture.supplyAsync(() -> {
try {
// This call uses the shared connection pool
return analyticsApi.postAnalyticsConversationsDetailsQuery(query);
} catch (PureCloudException e) {
// Handle API errors (4xx, 5xx)
System.err.println("API Error: " + e.getCode() + " - " + e.getMessage());
throw new RuntimeException(e);
} catch (Exception e) {
// Handle network or serialization errors
System.err.println("Unexpected Error: " + e.getMessage());
throw new RuntimeException(e);
}
}, executor);
}
public void shutdown() {
executor.shutdown();
}
}
Step 2: Construct the Query Payload
The postAnalyticsConversationsDetailsQuery endpoint requires a JSON body. We will construct a ConversationDetailsQuery object.
import com.mypurecloud.api.v2.analytics.model.ConversationDetailsQuery;
import com.mypurecloud.api.v2.analytics.model.Interval;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.List;
public class QueryBuilder {
public static ConversationDetailsQuery buildQuery() {
// Define the time interval (last 24 hours)
OffsetDateTime endTime = OffsetDateTime.now();
OffsetDateTime startTime = endTime.minusHours(24);
Interval interval = new Interval();
interval.setFrom(startTime);
interval.setTo(endTime);
// Define the view (standard "Conversations" view)
ConversationDetailsQuery query = new ConversationDetailsQuery();
query.setInterval(interval);
query.setView("Conversations");
// Select specific columns to reduce payload size
List<String> columns = Arrays.asList("id", "type", "startTime", "endTime", "duration");
query.setColumns(columns);
// Optional: Filter by specific queues or users
// query.setQueues(Arrays.asList("queue-id-1", "queue-id-2"));
return query;
}
}
Step 3: Execute Concurrent Requests
We will now run multiple concurrent queries to demonstrate the thread safety and connection pooling.
import com.mypurecloud.api.v2.analytics.model.ConversationDetailsResponse;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
// 1. Initialize Thread-Safe Client
ApiClient apiClient = GenesysConfig.createThreadSafeApiClient();
GenesysAuth.configureOAuth(apiClient);
// 2. Initialize Service
AnalyticsService service = new AnalyticsService(apiClient);
// 3. Build Query
var query = QueryBuilder.buildQuery();
// 4. Launch 5 Concurrent Requests
List<CompletableFuture<ConversationDetailsResponse>> futures = java.util.stream.IntStream.range(0, 5)
.mapToObj(i -> service.queryConversationsAsync(query))
.collect(Collectors.toList());
// 5. Wait for All Results
try {
List<ConversationDetailsResponse> results = futures.stream()
.map(CompletableFuture::join) // Blocks until complete
.collect(Collectors.toList());
// 6. Process Results
for (int i = 0; i < results.size(); i++) {
ConversationDetailsResponse response = results.get(i);
System.out.println("Request " + (i + 1) + " returned " + response.getTotalCount() + " conversations.");
}
} catch (Exception e) {
System.err.println("Failed to execute concurrent requests: " + e.getMessage());
e.printStackTrace();
} finally {
service.shutdown();
// Close the HTTP client to release resources
try {
apiClient.getHttpClient().close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
Complete Working Example
Below is the full, copy-pasteable Maven project structure.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>genesys-thread-safe-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!-- Genesys Cloud Java SDK -->
<dependency>
<groupId>com.mypurecloud.api</groupId>
<artifactId>platform-java-sdk</artifactId>
<version>130.0.0</version> <!-- Check for latest version -->
</dependency>
<!-- Logging (Optional but recommended) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.36</version>
</dependency>
</dependencies>
</project>
GenesysConfig.java
package com.example.genesys;
import com.mypurecloud.api.client.ApiClient;
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.HttpRoute;
import java.time.Duration;
public class GenesysConfig {
private static final String ENVIRONMENT = "mypurecloud.com";
public static ApiClient createThreadSafeApiClient() {
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(100);
connectionManager.setDefaultMaxPerRoute(20);
connectionManager.setMaxPerRoute(new HttpRoute("api." + ENVIRONMENT), 50);
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.setConnectionTimeToLive(Duration.ofMinutes(5))
.setKeepAliveStrategy((response, context) -> Duration.ofMinutes(5).toMillis())
.build();
ApiClient apiClient = new ApiClient();
apiClient.setHttpClient(httpClient);
apiClient.setBasePath("https://api." + ENVIRONMENT);
return apiClient;
}
}
GenesysAuth.java
package com.example.genesys;
import com.mypurecloud.api.client.ApiClient;
import com.mypurecloud.api.client.AuthService;
public class GenesysAuth {
private static final String CLIENT_ID = "YOUR_CLIENT_ID";
private static final String CLIENT_SECRET = "YOUR_CLIENT_SECRET";
public static void configureOAuth(ApiClient apiClient) {
AuthService authService = new AuthService(apiClient);
try {
authService.authenticate(CLIENT_ID, CLIENT_SECRET);
} catch (Exception e) {
throw new RuntimeException("Failed to authenticate with Genesys Cloud", e);
}
}
}
AnalyticsService.java
package com.example.genesys;
import com.mypurecloud.api.client.ApiClient;
import com.mypurecloud.api.v2.analytics.AnalyticsApi;
import com.mypurecloud.api.v2.analytics.model.ConversationDetailsQuery;
import com.mypurecloud.api.v2.analytics.model.ConversationDetailsResponse;
import com.mypurecloud.api.client.PureCloudException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AnalyticsService {
private final AnalyticsApi analyticsApi;
private final ExecutorService executor;
public AnalyticsService(ApiClient apiClient) {
this.analyticsApi = new AnalyticsApi(apiClient);
this.executor = Executors.newFixedThreadPool(10);
}
public CompletableFuture<ConversationDetailsResponse> queryConversationsAsync(ConversationDetailsQuery query) {
return CompletableFuture.supplyAsync(() -> {
try {
return analyticsApi.postAnalyticsConversationsDetailsQuery(query);
} catch (PureCloudException e) {
System.err.println("API Error: " + e.getCode() + " - " + e.getMessage());
throw new RuntimeException(e);
} catch (Exception e) {
System.err.println("Unexpected Error: " + e.getMessage());
throw new RuntimeException(e);
}
}, executor);
}
public void shutdown() {
executor.shutdown();
}
}
QueryBuilder.java
package com.example.genesys;
import com.mypurecloud.api.v2.analytics.model.ConversationDetailsQuery;
import com.mypurecloud.api.v2.analytics.model.Interval;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.List;
public class QueryBuilder {
public static ConversationDetailsQuery buildQuery() {
OffsetDateTime endTime = OffsetDateTime.now();
OffsetDateTime startTime = endTime.minusHours(24);
Interval interval = new Interval();
interval.setFrom(startTime);
interval.setTo(endTime);
ConversationDetailsQuery query = new ConversationDetailsQuery();
query.setInterval(interval);
query.setView("Conversations");
List<String> columns = Arrays.asList("id", "type", "startTime", "endTime", "duration");
query.setColumns(columns);
return query;
}
}
Main.java
package com.example.genesys;
import com.mypurecloud.api.v2.analytics.model.ConversationDetailsResponse;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
ApiClient apiClient = GenesysConfig.createThreadSafeApiClient();
GenesysAuth.configureOAuth(apiClient);
AnalyticsService service = new AnalyticsService(apiClient);
var query = QueryBuilder.buildQuery();
List<CompletableFuture<ConversationDetailsResponse>> futures = java.util.stream.IntStream.range(0, 5)
.mapToObj(i -> service.queryConversationsAsync(query))
.collect(Collectors.toList());
try {
List<ConversationDetailsResponse> results = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
for (int i = 0; i < results.size(); i++) {
ConversationDetailsResponse response = results.get(i);
System.out.println("Request " + (i + 1) + " returned " + response.getTotalCount() + " conversations.");
}
} catch (Exception e) {
System.err.println("Failed to execute concurrent requests: " + e.getMessage());
e.printStackTrace();
} finally {
service.shutdown();
try {
apiClient.getHttpClient().close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
Common Errors & Debugging
Error: java.lang.IllegalStateException: Connection pool shut down
Cause: You are attempting to make an API call after the HttpClient has been closed. This often happens if you close the client prematurely in a web application lifecycle or if you share the ApiClient across multiple servlet contexts without proper lifecycle management.
Fix: Ensure the HttpClient is only closed when the application is shutting down. In a Spring Boot application, use @PreDestroy to close the client.
@PreDestroy
public void cleanup() {
try {
if (apiClient != null && apiClient.getHttpClient() != null) {
apiClient.getHttpClient().close();
}
} catch (Exception e) {
log.error("Error closing HTTP client", e);
}
}
Error: 429 Too Many Requests
Cause: Even with connection pooling, you may exceed Genesys Cloud’s API rate limits. Connection pooling reuses TCP connections but does not throttle request frequency.
Fix: Implement exponential backoff in your retry logic. The Java SDK does not automatically retry 429s. You must handle this in your service layer.
public ConversationDetailsResponse queryWithRetry(ConversationDetailsQuery query, int maxRetries) {
int retries = 0;
while (retries < maxRetries) {
try {
return analyticsApi.postAnalyticsConversationsDetailsQuery(query);
} catch (PureCloudException e) {
if (e.getCode() == 429) {
retries++;
long delay = (long) Math.pow(2, retries) * 1000; // Exponential backoff
System.out.println("Rate limited. Waiting " + delay + "ms before retry " + retries);
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted during retry", ie);
}
} else {
throw e; // Re-throw non-rate-limit errors
}
}
}
throw new RuntimeException("Max retries exceeded");
}
Error: java.net.SocketTimeoutException: Read timed out
Cause: The default socket timeout in the SDK may be too short for large analytics queries.
Fix: Configure the RequestConfig in the HttpClient builder.
RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(60000) // 60 seconds
.setConnectTimeout(10000) // 10 seconds
.setConnectionRequestTimeout(5000) // 5 seconds
.build();
CloseableHttpClient httpClient = HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(connectionManager)
.build();