Genesys Cloud Java SDK: Configuring Thread-Safe Connection Pooling for High-Throughput Integrations

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-java version 127.0.0 or 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:

  1. Increase maxTotalConnections in PoolingHttpClientConnectionManager.
  2. Increase the socket timeout in the HttpClient builder:
    .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.

Official References