Configure Thread-Safe Java SDK Clients with Connection Pooling

Configure Thread-Safe Java SDK Clients with Connection Pooling

What You Will Build

  • You will build a Java application that initializes a single, thread-safe PureCloudPlatformClientV2 instance configured with an optimized Apache HTTP Client connection pool.
  • This configuration prevents port exhaustion and socket leaks in high-concurrency environments by reusing underlying TCP connections.
  • The tutorial covers Java 17+ using the official Genesys Cloud CX Java SDK (com.mypurecloud.api:platform-client-v2).

Prerequisites

  • Java Version: Java 17 or later (LTS).
  • SDK Version: platform-client-v2 version 167.0.0 or later.
  • Build Tool: Maven or Gradle.
  • Dependencies:
    • com.mypurecloud.api:platform-client-v2:167.0.0
    • org.apache.httpcomponents:httpclient:4.5.14 (Transitive dependency, but good to pin for stability).
  • Genesys Cloud Environment:
    • An OAuth Client ID and Client Secret.
    • A valid scope for the API calls used in this tutorial (e.g., user:read for testing connectivity, analytics:query for data retrieval).

Authentication Setup

The Genesys Cloud Java SDK handles the OAuth2 Client Credentials flow internally. However, to ensure thread safety and performance, you must configure the underlying HTTP client before initializing the PureCloudPlatformClientV2. The SDK uses Apache HttpClient 4.x under the hood. By default, it uses a basic connection manager that is not thread-safe for concurrent requests. You must inject a PoolingHttpClientConnectionManager.

Step 1: Define the Connection Pool Configuration

Create a utility class to manage the HTTP client configuration. This class will define the socket timeouts, connection timeouts, and the pool size.

package com.example.genesys.config;

import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.client.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;
import javax.net.ssl.SSLContext;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;

/**
 * Configures the underlying Apache HTTP Client connection manager for the Genesys SDK.
 * This ensures thread-safe connection pooling.
 */
public class HttpConfig {

    private static final int MAX_TOTAL_CONNECTIONS = 100;
    private static final int MAX_CONNECTIONS_PER_ROUTE = 20;
    private static final int CONNECT_TIMEOUT_MS = 5000;
    private static final int SOCKET_TIMEOUT_MS = 10000;
    private static final int TIME_TO_LIVE_MS = 300000; // 5 minutes

    /**
     * Creates a thread-safe PoolingHttpClientConnectionManager.
     * 
     * @return The configured connection manager.
     */
    public static PoolingHttpClientConnectionManager createConnectionManager() {
        try {
            // Trust all certificates for development ease. 
            // In production, use a proper TrustManager or the default system trust store.
            SSLContext sslContext = SSLContextBuilder.create()
                    .loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true)
                    .build();

            SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext);

            PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(sslSocketFactory);
            
            // Set global max connections
            connManager.setMaxTotal(MAX_TOTAL_CONNECTIONS);
            
            // Set max connections per host (route). Genesys API endpoints are distributed across subdomains.
            connManager.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE);

            return connManager;
        } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
            throw new RuntimeException("Failed to initialize SSL Context for Genesys HTTP Client", e);
        }
    }

    public static int getConnectTimeoutMs() {
        return CONNECT_TIMEOUT_MS;
    }

    public static int getSocketTimeoutMs() {
        return SOCKET_TIMEOUT_MS;
    }
}

Step 2: Initialize the Thread-Safe SDK Client

The PureCloudPlatformClientV2 constructor allows you to pass a custom ApiClient or configure the underlying HttpClients builder. In recent SDK versions, the recommended approach is to configure the ApiClient directly or use the PlatformClientFactory with a custom builder if available. However, the most robust method for explicit control is to configure the ApiClient instance before passing it to the platform client, or to use the static initialization methods with custom properties.

For this tutorial, we will use the ApiClient configuration approach which is explicit and version-stable.

package com.example.genesys.config;

import com.mypurecloud.api.v2.ApiClient;
import com.mypurecloud.api.v2.PureCloudPlatformClientV2;
import com.mypurecloud.api.v2.auth.OAuthClientCredentials;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.PoolingHttpClientConnectionManager;

/**
 * Factory for creating a thread-safe Genesys Cloud Client.
 */
public class GenesysClientFactory {

    private static final String CLIENT_ID = System.getenv("GENESYS_CLIENT_ID");
    private static final String CLIENT_SECRET = System.getenv("GENESYS_CLIENT_SECRET");
    private static final String ENVIRONMENT = "mypurecloud.com"; // Use "mypurecloud.ie" for EU

    private static PureCloudPlatformClientV2 platformClient;

    /**
     * Initializes the platform client with a shared, thread-safe connection pool.
     * This method is idempotent and thread-safe.
     *
     * @return The configured PureCloudPlatformClientV2 instance.
     */
    public static synchronized PureCloudPlatformClientV2 getClient() {
        if (platformClient == null) {
            try {
                // 1. Create the connection manager
                PoolingHttpClientConnectionManager connManager = HttpConfig.createConnectionManager();

                // 2. Build the Apache HttpClient
                CloseableHttpClient httpClient = HttpClientBuilder.create()
                        .setConnectionManager(connManager)
                        .setConnectTimeout(HttpConfig.getConnectTimeoutMs())
                        .setSocketTimeout(HttpConfig.getSocketTimeoutMs())
                        .build();

                // 3. Create the ApiClient and inject the custom HttpClient
                // Note: The constructor signature may vary slightly by SDK version.
                // In v167+, you typically configure the ApiClient via setters or a builder.
                // If the direct constructor is not available, use the default ApiClient and replace the httpClient.
                
                ApiClient apiClient = new ApiClient();
                apiClient.setHttpClient(httpClient); // Inject the thread-safe client
                apiClient.setEnvironment(ENVIRONMENT);

                // 4. Configure OAuth
                OAuthClientCredentials oAuth = new OAuthClientCredentials();
                oAuth.setClientId(CLIENT_ID);
                oAuth.setClientSecret(CLIENT_SECRET);
                oAuth.setGrantType("client_credentials");
                
                // Define scopes. Add scopes as needed for your use case.
                oAuth.setScopes("user:read", "analytics:query");

                apiClient.setAuthenticator(oAuth);

                // 5. Initialize the Platform Client
                platformClient = new PureCloudPlatformClientV2(apiClient);

            } catch (Exception e) {
                throw new RuntimeException("Failed to initialize Genesys Platform Client", e);
            }
        }
        return platformClient;
    }

    /**
     * Shuts down the HTTP client and releases resources.
     * Call this during application shutdown.
     */
    public static synchronized void shutdown() {
        if (platformClient != null) {
            platformClient.close();
            platformClient = null;
        }
    }
}

Implementation

Step 3: Verify Connectivity with a Simple API Call

Before implementing complex logic, verify that the thread-safe client can authenticate and retrieve data. We will use the UsersApi to list users. This is a lightweight operation that tests authentication and basic HTTP connectivity.

Required Scope: user:read

package com.example.genesys.demo;

import com.mypurecloud.api.v2.PureCloudPlatformClientV2;
import com.mypurecloud.api.v2.api.UsersApi;
import com.mypurecloud.api.v2.model.PagedEntityPresenceList;
import com.example.genesys.config.GenesysClientFactory;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ConnectivityTest {

    public static void main(String[] args) {
        PureCloudPlatformClientV2 client = GenesysClientFactory.getClient();
        
        // Create an API client instance for Users
        UsersApi usersApi = client.getUsersApi();

        try {
            System.out.println("Testing connectivity...");
            
            // Fetch the first page of users
            // Page size 5, page number 1
            PagedEntityPresenceList users = usersApi.getUsers(5, 1, null, null, null, null, null, null);
            
            System.out.println("Successfully fetched " + users.getEntities().size() + " users.");
            users.getEntities().forEach(user -> {
                System.out.println("User: " + user.getName() + " (" + user.getId() + ")");
            });
            
        } catch (Exception e) {
            System.err.println("Error during connectivity test:");
            e.printStackTrace();
        } finally {
            GenesysClientFactory.shutdown();
        }
    }
}

Expected Response:
If successful, the console will print the names and IDs of up to 5 users. If authentication fails, you will receive a 401 Unauthorized exception. If the connection pool is misconfigured, you may encounter SocketTimeoutException or ConnectTimeoutException.

Step 4: Implement Concurrent Data Retrieval

The primary benefit of connection pooling is handling concurrent requests. In this step, we will create a multi-threaded scenario that retrieves analytics data for multiple queues simultaneously. This demonstrates that the single PureCloudPlatformClientV2 instance can serve multiple threads without creating a new TCP connection for each request.

Required Scope: analytics:query

We will use the AnalyticsApi to query conversation details. This is a heavier operation that benefits from connection reuse.

package com.example.genesys.demo;

import com.mypurecloud.api.v2.PureCloudPlatformClientV2;
import com.mypurecloud.api.v2.api.AnalyticsApi;
import com.mypurecloud.api.v2.api.AnalyticsConversationsApi;
import com.mypurecloud.api.v2.model.ConversationDetailsQuery;
import com.mypurecloud.api.v2.model.ConversationDetailsResponse;
import com.mypurecloud.api.v2.model.Sort;
import com.example.genesys.config.GenesysClientFactory;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class ConcurrentAnalyticsDemo {

    private static final int THREAD_POOL_SIZE = 10;
    private static final int NUM_QUEUES = 5;
    private static final String QUEUE_ID_1 = "YOUR_QUEUE_ID_1"; // Replace with actual Queue UUID
    private static final String QUEUE_ID_2 = "YOUR_QUEUE_ID_2"; // Replace with actual Queue UUID

    public static void main(String[] args) {
        PureCloudPlatformClientV2 client = GenesysClientFactory.getClient();
        AnalyticsConversationsApi analyticsApi = client.getAnalyticsConversationsApi();

        ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
        List<Future<Long>> futures = new java.util.ArrayList<>();

        // Simulate multiple concurrent queries
        for (int i = 0; i < NUM_QUEUES; i++) {
            // Alternate between two queue IDs for demonstration
            String queueId = (i % 2 == 0) ? QUEUE_ID_1 : QUEUE_ID_2;
            
            futures.add(executor.submit(() -> {
                return fetchConversationCount(analyticsApi, queueId);
            }));
        }

        // Collect results
        for (Future<Long> future : futures) {
            try {
                long count = future.get(30, TimeUnit.SECONDS);
                System.out.println("Queue processed. Conversations found: " + count);
            } catch (InterruptedException | ExecutionException | TimeoutException e) {
                System.err.println("Error processing queue: " + e.getMessage());
            }
        }

        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }

        GenesysClientFactory.shutdown();
    }

    /**
     * Queries the Analytics API for conversation details.
     * 
     * @param analyticsApi The configured Analytics API client.
     * @param queueId The UUID of the queue to query.
     * @return The total number of conversations matching the query.
     */
    private static long fetchConversationCount(AnalyticsConversationsApi analyticsApi, String queueId) {
        try {
            // Construct the query body
            ConversationDetailsQuery query = new ConversationDetailsQuery();
            
            // Set date range: Last 24 hours
            java.time.ZonedDateTime now = java.time.ZonedDateTime.now();
            java.time.ZonedDateTime start = now.minusHours(24);
            
            query.setStartTime(start.toString());
            query.setEndTime(now.toString());
            
            // Filter by queue ID
            query.setFilter(
                new com.mypurecloud.api.v2.model.Filter()
                    .field("queueId")
                    .op("eq")
                    .value(queueId)
            );

            // Set sort order
            query.setSort(
                new Sort()
                    .field("startTime")
                    .order("desc")
            );

            // Execute the query
            // Note: The SDK method signature may vary. 
            // In v167+, it is typically: postAnalyticsConversationsDetailsQuery(ConversationDetailsQuery body)
            ConversationDetailsResponse response = analyticsApi.postAnalyticsConversationsDetailsQuery(query);

            if (response != null && response.getEntities() != null) {
                return response.getEntities().size();
            }
            return 0;

        } catch (Exception e) {
            System.err.println("API Error for Queue " + queueId + ": " + e.getMessage());
            // Log the full stack trace for debugging 4xx/5xx errors
            e.printStackTrace();
            return -1;
        }
    }
}

Key Implementation Details:

  1. Shared Client: The analyticsApi instance is derived from the same platformClient instance used in Step 3. This ensures all threads share the same connection pool.
  2. Thread Safety: The PoolingHttpClientConnectionManager is thread-safe. Multiple threads calling fetchConversationCount will borrow connections from the pool, use them, and return them.
  3. Pagination: The postAnalyticsConversationsDetailsQuery endpoint returns paginated results. This example only fetches the first page. For production, you must implement pagination logic using the nextPage URI provided in the response headers or body.

Step 5: Handling Pagination and Retry Logic

Genesys Cloud APIs use standard HTTP pagination. When processing large datasets, you must handle pagination to avoid missing data. Additionally, you should implement retry logic for transient errors (429 Too Many Requests, 5xx Server Errors).

The Java SDK does not automatically retry 429 errors. You must implement this in your application code.

package com.example.genesys.demo;

import com.mypurecloud.api.v2.PureCloudPlatformClientV2;
import com.mypurecloud.api.v2.api.AnalyticsConversationsApi;
import com.mypurecloud.api.v2.model.ConversationDetailsQuery;
import com.mypurecloud.api.v2.model.ConversationDetailsResponse;
import com.mypurecloud.api.v2.model.Sort;
import com.example.genesys.config.GenesysClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class PaginatedAnalyticsFetcher {

    private static final Logger logger = LoggerFactory.getLogger(PaginatedAnalyticsFetcher.class);
    private static final int MAX_RETRIES = 3;
    private static final long RETRY_DELAY_MS = 1000;

    public static void main(String[] args) {
        PureCloudPlatformClientV2 client = GenesysClientFactory.getClient();
        AnalyticsConversationsApi analyticsApi = client.getAnalyticsConversationsApi();

        String queueId = "YOUR_QUEUE_ID_1"; // Replace with actual Queue UUID
        
        try {
            List<String> allConversationIds = fetchAllConversations(analyticsApi, queueId);
            System.out.println("Total conversations fetched: " + allConversationIds.size());
        } catch (Exception e) {
            logger.error("Failed to fetch conversations", e);
        } finally {
            GenesysClientFactory.shutdown();
        }
    }

    /**
     * Fetches all conversations for a queue, handling pagination and retries.
     */
    public static List<String> fetchAllConversations(AnalyticsConversationsApi analyticsApi, String queueId) {
        List<String> conversationIds = new ArrayList<>();
        String nextPageUri = null;
        int retryCount = 0;

        // Initial query
        ConversationDetailsQuery query = buildQuery(queueId);

        do {
            boolean success = false;
            ConversationDetailsResponse response = null;

            // Retry loop for transient errors
            while (retryCount < MAX_RETRIES) {
                try {
                    if (nextPageUri == null) {
                        // First page
                        response = analyticsApi.postAnalyticsConversationsDetailsQuery(query);
                    } else {
                        // Subsequent pages
                        // Note: The SDK may not have a direct method for nextPageUri.
                        // You may need to use the generic ApiClient call or construct the URL manually.
                        // For simplicity, we assume the SDK handles pagination via a specific method if available.
                        // If not, you must use the ApiClient's generic execute method.
                        // Here we assume a hypothetical method for clarity. In reality, check SDK docs for pagination helpers.
                        throw new UnsupportedOperationException("Pagination via nextPageUri requires custom HTTP call in this SDK version.");
                    }
                    success = true;
                    break; // Exit retry loop on success

                } catch (Exception e) {
                    int statusCode = extractStatusCode(e);
                    if (statusCode == 429 || (statusCode >= 500 && statusCode < 600)) {
                        retryCount++;
                        logger.warn("Transient error (Status: {}). Retrying in {} ms...", statusCode, RETRY_DELAY_MS);
                        try {
                            Thread.sleep(RETRY_DELAY_MS * retryCount); // Exponential backoff
                        } catch (InterruptedException ie) {
                            Thread.currentThread().interrupt();
                            throw new RuntimeException("Interrupted during retry", ie);
                        }
                    } else {
                        throw e; // Non-retryable error
                    }
                }
            }

            if (!success) {
                throw new RuntimeException("Max retries exceeded for conversation query.");
            }

            // Process results
            if (response != null && response.getEntities() != null) {
                response.getEntities().forEach(entity -> {
                    conversationIds.add(entity.getId());
                });
                // Check for next page
                // The SDK response object may contain a 'nextPage' field or header.
                // Check the specific SDK model for ConversationDetailsResponse.
                if (response.getNextPage() != null) {
                    nextPageUri = response.getNextPage().toString();
                } else {
                    nextPageUri = null;
                }
            }

        } while (nextPageUri != null);

        return conversationIds;
    }

    private static ConversationDetailsQuery buildQuery(String queueId) {
        ConversationDetailsQuery query = new ConversationDetailsQuery();
        java.time.ZonedDateTime now = java.time.ZonedDateTime.now();
        query.setStartTime(now.minusHours(24).toString());
        query.setEndTime(now.toString());
        query.setFilter(
            new com.mypurecloud.api.v2.model.Filter()
                .field("queueId")
                .op("eq")
                .value(queueId)
        );
        query.setPageSize(100); // Max page size
        return query;
    }

    private static int extractStatusCode(Exception e) {
        // Helper to extract status code from SDK exceptions
        // Implementation depends on specific SDK exception classes
        if (e instanceof com.mypurecloud.api.v2.ApiException) {
            return ((com.mypurecloud.api.v2.ApiException) e).getCode();
        }
        return -1;
    }
}

Note on Pagination: The Genesys Cloud Java SDK’s handling of pagination can be verbose. Always check the ApiResponse or the specific model class for nextPage or links fields. If the SDK does not provide a helper method, you must use the ApiClient’s generic execute method with the nextPage URI.

Complete Working Example

Below is the complete pom.xml and the main class structure for a standalone application.

Maven 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-sdk-pooling</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <genesys.sdk.version>167.0.0</genesys.sdk.version>
    </properties>

    <dependencies>
        <!-- Genesys Cloud Java SDK -->
        <dependency>
            <groupId>com.mypurecloud.api</groupId>
            <artifactId>platform-client-v2</artifactId>
            <version>${genesys.sdk.version}</version>
        </dependency>

        <!-- SLF4J for Logging -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>2.0.9</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
            </plugin>
        </plugins>
    </build>
</project>

Main Application Class

package com.example.genesys;

import com.mypurecloud.api.v2.PureCloudPlatformClientV2;
import com.mypurecloud.api.v2.api.UsersApi;
import com.mypurecloud.api.v2.model.PagedEntityPresenceList;
import com.example.genesys.config.GenesysClientFactory;

public class Application {

    public static void main(String[] args) {
        System.out.println("Starting Genesys Cloud SDK Demo...");

        try {
            // 1. Get the thread-safe client
            PureCloudPlatformClientV2 client = GenesysClientFactory.getClient();
            UsersApi usersApi = client.getUsersApi();

            // 2. Execute a simple API call
            System.out.println("Fetching users...");
            PagedEntityPresenceList users = usersApi.getUsers(5, 1, null, null, null, null, null, null);
            
            System.out.println("Fetched " + users.getEntities().size() + " users.");
            users.getEntities().forEach(user -> 
                System.out.println(" - " + user.getName() + " (" + user.getId() + ")")
            );

        } catch (Exception e) {
            System.err.println("Application error:");
            e.printStackTrace();
        } finally {
            // 3. Shutdown to release resources
            GenesysClientFactory.shutdown();
            System.out.println("Application shutdown complete.");
        }
    }
}

Common Errors & Debugging

Error: javax.net.ssl.SSLHandshakeException

  • Cause: The SSL context is not configured correctly, or the trust store does not trust the Genesys Cloud certificate.
  • Fix: Ensure the SSLContextBuilder in HttpConfig is correctly initialized. In production, remove the TrustStrategy that accepts all certificates and use the default system trust store.

Error: java.util.concurrent.TimeoutException

  • Cause: The request took longer than the configured timeout.
  • Fix: Increase SOCKET_TIMEOUT_MS and CONNECT_TIMEOUT_MS in HttpConfig. Analytics queries can take several seconds.

Error: 429 Too Many Requests

  • Cause: You have exceeded the API rate limit.
  • Fix: Implement retry logic with exponential backoff as shown in Step 5. Monitor the Retry-After header in the response.

Error: ConnectionPoolTimeoutException

  • Cause: The connection pool is exhausted. All connections are in use, and no new connections can be created because MAX_TOTAL_CONNECTIONS has been reached.
  • Fix: Increase MAX_TOTAL_CONNECTIONS and MAX_CONNECTIONS_PER_ROUTE in HttpConfig. Ensure that connections are being returned to the pool (i.e., you are not holding onto HTTP responses indefinitely).

Error: 401 Unauthorized

  • Cause: Invalid Client ID/Secret or missing scopes.
  • Fix: Verify the environment variables GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET. Ensure the OAuth client in the Genesys Admin console has the correct scopes assigned.

Official References