Java SDK: Configuring Connection Pooling and Thread-Safe HTTP Clients

Java SDK: Configuring Connection Pooling and Thread-Safe HTTP Clients

What You Will Build

  • You will build a production-ready Java utility class that initializes the Genesys Cloud ApiClient with explicit connection pooling, timeout configurations, and thread-safe settings.
  • This tutorial uses the Genesys Cloud Java SDK (com.mypurecloud.api) to demonstrate proper HTTP client configuration.
  • The code is written in Java 11+ using the Apache HttpClient library, which is the default underlying HTTP engine for the Genesys Cloud Java SDK.

Prerequisites

  • OAuth Client Type: Service Account (Client Credentials Grant).
  • Required Scopes: analytics:query (for the example API call).
  • SDK Version: Genesys Cloud Java SDK v2 (latest stable release).
  • Runtime: Java Development Kit (JDK) 11 or higher.
  • Dependencies:
    • com.mypurecloud.api:java-purecloud-api-client
    • org.apache.httpcomponents:httpclient (transitive dependency, but explicit management is recommended for pooling).
    • com.google.guava:guava (for thread-safe caching utilities).

Authentication Setup

The Genesys Cloud Java SDK handles OAuth token acquisition via the AuthServiceClient. However, the security of your application depends on how you manage the ApiClient instance that holds the HTTP connection pool. If you create a new ApiClient for every request, you exhaust system file descriptors and suffer from TCP handshake latency.

The correct pattern is to create one shared, thread-safe ApiClient instance per OAuth client ID, initialized once at application startup.

Step 1: Initialize the ApiClient with Pooling

The ApiClient class in the Genesys Cloud Java SDK wraps an Apache CloseableHttpClient. By default, the SDK uses reasonable defaults, but production environments require explicit control over connection pool sizes, timeouts, and eviction policies to prevent thread starvation under high load.

You must configure the PoolingHttpClientConnectionManager. This manager allows multiple threads to share connections. Without this, the underlying HTTP client may only support one concurrent connection per route (host:port), causing severe bottlenecks.

package com.example.genesys.config;

import com.mypurecloud.api.client.ApiClient;
import com.mypurecloud.api.client.auth.AuthException;
import com.mypurecloud.api.client.auth.OAuthClient;
import com.mypurecloud.api.client.auth.OAuthClientCredentials;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
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.ssl.SSLContextBuilder;

import javax.net.ssl.SSLContext;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;

public class GenesysClientFactory {

    private static final String REGION = "my.genesis.com"; // e.g., usw2.platform.mygen.com
    private static final String CLIENT_ID = "your-client-id";
    private static final String CLIENT_SECRET = "your-client-secret";

    // Thread-safe singleton holder
    private static volatile ApiClient sharedApiClient;

    private GenesysClientFactory() {
        // Private constructor to prevent instantiation
    }

    /**
     * Returns a singleton, thread-safe ApiClient instance.
     * Uses double-checked locking for thread safety during initialization.
     */
    public static ApiClient getSharedApiClient() {
        if (sharedApiClient == null) {
            synchronized (GenesysClientFactory.class) {
                if (sharedApiClient == null) {
                    sharedApiClient = createPooledApiClient();
                }
            }
        }
        return sharedApiClient;
    }

    /**
     * Creates an ApiClient with explicit connection pooling and timeout settings.
     */
    private static ApiClient createPooledApiClient() {
        try {
            // 1. Configure SSL Context (Standard TrustStore)
            SSLContext sslContext = SSLContextBuilder.create()
                .loadTrustMaterial(null, new TrustStrategy() {
                    @Override
                    public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                        return true; // Trust all certificates in the default trust store
                    }
                })
                .build();

            SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext);

            // 2. Configure Connection Pooling
            PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(sslSocketFactory);
            
            // Set maximum total connections in the pool
            connectionManager.setMaxTotal(200);
            
            // Set maximum connections per route (host:port)
            // This is critical for Genesys APIs which often use a single base URL
            connectionManager.setDefaultMaxPerRoute(50);

            // 3. Build the HTTP Client with timeouts
            CloseableHttpClient httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .setDefaultRequestConfig(org.apache.http.client.config.RequestConfig.custom()
                    .setConnectTimeout(5000) // Connection timeout: 5 seconds
                    .setSocketTimeout(30000) // Read timeout: 30 seconds
                    .setConnectionRequestTimeout(5000) // Wait for connection from pool: 5 seconds
                    .build())
                .evictIdleConnections(30, TimeUnit.SECONDS) // Clean up idle connections
                .evictExpiredConnections()
                .build();

            // 4. Initialize the Genesys ApiClient with the custom HTTP client
            ApiClient apiClient = new ApiClient(REGION);
            apiClient.setHttpClient(httpClient);

            // 5. Configure OAuth Credentials
            OAuthClientCredentials credentials = new OAuthClientCredentials();
            credentials.setClientId(CLIENT_ID);
            credentials.setClientSecret(CLIENT_SECRET);

            // Attach the OAuth client to the ApiClient
            apiClient.setOAuthClient(new OAuthClient(credentials, apiClient));

            return apiClient;

        } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
            throw new RuntimeException("Failed to initialize Genesys ApiClient with SSL context", e);
        }
    }
}

Why this configuration matters:

  • setMaxTotal(200): Prevents the application from opening unlimited TCP connections. If you have 1000 threads hitting the API, they will queue up waiting for one of the 200 available connections rather than crashing the OS with Too many open files.
  • setDefaultMaxPerRoute(50): Genesys Cloud APIs typically resolve to a single IP range. Without this, the pool might only allow 2-4 concurrent connections to that specific host, creating a bottleneck.
  • evictIdleConnections: TCP connections have lifetimes enforced by firewalls and load balancers (often 60-120 seconds). If your app holds a connection idle for 5 minutes, the remote server may drop it. When your app tries to reuse it, it fails. Evicting idle connections ensures you always get a fresh, valid connection from the pool.

Implementation

Step 2: Execute a Thread-Safe API Call

Now that you have a thread-safe ApiClient, you can use it across multiple threads. The Genesys Cloud Java SDK uses Retrofit under the hood. Retrofit is thread-safe as long as the OkHttpClient (or in this case, the custom HttpClient injected) is properly configured for concurrency.

The ApiClient instance is immutable after creation. You can pass it to multiple ApiService instances safely.

Below is an example of fetching analytics data. This is a heavy operation that benefits significantly from connection reuse.

package com.example.genesys.service;

import com.mypurecloud.api.client.ApiClient;
import com.mypurecloud.api.client.ApiException;
import com.mypurecloud.api.client.auth.AuthException;
import com.mypurecloud.api.analytics.AnalyticsApi;
import com.mypurecloud.api.analytics.model.ConversationDetailsQuery;
import com.mypurecloud.api.analytics.model.ConversationDetailResponse;
import com.example.genesys.config.GenesysClientFactory;

import java.util.List;

public class AnalyticsService {

    private final AnalyticsApi analyticsApi;

    public AnalyticsService() {
        // Get the shared, pooled ApiClient
        ApiClient apiClient = GenesysClientFactory.getSharedApiClient();
        
        // Instantiate the specific API service
        // Note: AnalyticsApi uses the ApiClient for HTTP calls
        this.analyticsApi = new AnalyticsApi(apiClient);
    }

    /**
     * Fetches conversation details for the last hour.
     * This method is thread-safe and can be called from multiple threads concurrently.
     */
    public ConversationDetailResponse fetchRecentConversations() throws ApiException, AuthException, InterruptedException {
        
        // Define the query body
        ConversationDetailsQuery query = new ConversationDetailsQuery();
        query.setInterval("now-1h/now"); // Last hour
        query.setPageSize(25);
        
        // Add required entity type
        query.setEntityTypes(List.of("conversation"));

        try {
            // Execute the call
            // The SDK handles OAuth token refresh automatically if the token expires
            ConversationDetailResponse response = analyticsApi.postAnalyticsConversationsDetailsQuery(query);
            
            if (response == null) {
                throw new RuntimeException("API returned null response");
            }

            System.out.println("Fetched " + response.getTotalCount() + " conversations.");
            return response;

        } catch (ApiException e) {
            // Handle specific HTTP errors
            if (e.getCode() == 429) {
                System.err.println("Rate limited. Implement exponential backoff here.");
                Thread.sleep(1000); // Simple delay for demonstration
            } else if (e.getCode() == 401 || e.getCode() == 403) {
                System.err.println("Authentication/Authorization failed: " + e.getMessage());
            } else {
                System.err.println("API Error: " + e.getMessage());
            }
            throw e;
        }
    }
}

Step 3: Handling Concurrent Requests

To demonstrate that the connection pool is working, you can run multiple requests in parallel. Without pooling, this would fail or be extremely slow. With the configuration from Step 1, these requests share the pool efficiently.

package com.example.genesys.runner;

import com.example.genesys.service.AnalyticsService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ConcurrencyRunner {

    public static void main(String[] args) throws InterruptedException {
        int threadCount = 20; // Simulate 20 concurrent users/threads
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        AtomicInteger successCount = new AtomicInteger(0);
        AtomicInteger errorCount = new AtomicInteger(0);

        // Submit 20 tasks concurrently
        for (int i = 0; i < threadCount; i++) {
            final int taskId = i;
            executor.submit(() -> {
                try {
                    AnalyticsService service = new AnalyticsService();
                    service.fetchRecentConversations();
                    successCount.incrementAndGet();
                    System.out.println("Task " + taskId + " completed successfully.");
                } catch (Exception e) {
                    errorCount.incrementAndGet();
                    System.err.println("Task " + taskId + " failed: " + e.getMessage());
                }
            });
        }

        // Wait for all tasks to complete
        executor.shutdown();
        executor.awaitTermination(2, TimeUnit.MINUTES);

        System.out.println("Completed. Successes: " + successCount.get() + ", Errors: " + errorCount.get());
    }
}

Complete Working Example

Below is the complete, consolidated code structure. In a real project, you would split these into separate packages and files. For this tutorial, they are combined to show the dependencies clearly.

build.gradle (Maven users should adapt the dependencies accordingly):

plugins {
    id 'java'
}

group = 'com.example'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    // Genesys Cloud Java SDK
    implementation 'com.mypurecloud.api:java-purecloud-api-client:2.0.0' // Check for latest version
    
    // Apache HttpClient (usually transitive, but explicit versioning is safer)
    implementation 'org.apache.httpcomponents:httpclient:4.5.13'
    
    // SLF4J for logging (required by SDK)
    implementation 'org.slf4j:slf4j-simple:1.7.36'
}

sourceCompatibility = '11'

GenesysClientFactory.java:

package com.example.genesys.config;

import com.mypurecloud.api.client.ApiClient;
import com.mypurecloud.api.client.auth.OAuthClient;
import com.mypurecloud.api.client.auth.OAuthClientCredentials;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
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.ssl.SSLContextBuilder;

import javax.net.ssl.SSLContext;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;

public class GenesysClientFactory {

    private static final String REGION = "usw2.platform.mygen.com"; // Replace with your region
    private static final String CLIENT_ID = "YOUR_CLIENT_ID";
    private static final String CLIENT_SECRET = "YOUR_CLIENT_SECRET";

    private static volatile ApiClient sharedApiClient;

    private GenesysClientFactory() {}

    public static ApiClient getSharedApiClient() {
        if (sharedApiClient == null) {
            synchronized (GenesysClientFactory.class) {
                if (sharedApiClient == null) {
                    sharedApiClient = createPooledApiClient();
                }
            }
        }
        return sharedApiClient;
    }

    private static ApiClient createPooledApiClient() {
        try {
            SSLContext sslContext = SSLContextBuilder.create()
                .loadTrustMaterial(null, new TrustStrategy() {
                    @Override
                    public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                        return true;
                    }
                })
                .build();

            SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext);

            PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(sslSocketFactory);
            connectionManager.setMaxTotal(200);
            connectionManager.setDefaultMaxPerRoute(50);

            CloseableHttpClient httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .setDefaultRequestConfig(org.apache.http.client.config.RequestConfig.custom()
                    .setConnectTimeout(5000)
                    .setSocketTimeout(30000)
                    .setConnectionRequestTimeout(5000)
                    .build())
                .evictIdleConnections(30, TimeUnit.SECONDS)
                .evictExpiredConnections()
                .build();

            ApiClient apiClient = new ApiClient(REGION);
            apiClient.setHttpClient(httpClient);

            OAuthClientCredentials credentials = new OAuthClientCredentials();
            credentials.setClientId(CLIENT_ID);
            credentials.setClientSecret(CLIENT_SECRET);

            apiClient.setOAuthClient(new OAuthClient(credentials, apiClient));

            return apiClient;

        } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
            throw new RuntimeException("Failed to initialize Genesys ApiClient", e);
        }
    }
}

AnalyticsService.java:

package com.example.genesys.service;

import com.mypurecloud.api.client.ApiClient;
import com.mypurecloud.api.client.ApiException;
import com.mypurecloud.api.client.auth.AuthException;
import com.mypurecloud.api.analytics.AnalyticsApi;
import com.mypurecloud.api.analytics.model.ConversationDetailsQuery;
import com.mypurecloud.api.analytics.model.ConversationDetailResponse;
import com.example.genesys.config.GenesysClientFactory;

import java.util.List;

public class AnalyticsService {

    private final AnalyticsApi analyticsApi;

    public AnalyticsService() {
        ApiClient apiClient = GenesysClientFactory.getSharedApiClient();
        this.analyticsApi = new AnalyticsApi(apiClient);
    }

    public ConversationDetailResponse fetchRecentConversations() throws ApiException, AuthException {
        ConversationDetailsQuery query = new ConversationDetailsQuery();
        query.setInterval("now-1h/now");
        query.setPageSize(25);
        query.setEntityTypes(List.of("conversation"));

        try {
            return analyticsApi.postAnalyticsConversationsDetailsQuery(query);
        } catch (ApiException e) {
            if (e.getCode() == 429) {
                System.err.println("Rate Limited (429). Backoff required.");
            } else if (e.getCode() == 401) {
                System.err.println("Auth Error (401). Check credentials.");
            }
            throw e;
        }
    }
}

Common Errors & Debugging

Error: java.net.SocketTimeoutException: Read timed out

What causes it:
The server did not send data within the SocketTimeout period. This often happens with large analytics queries or during Genesys Cloud maintenance windows.

How to fix it:
Increase the setSocketTimeout value in the RequestConfig. For large data exports, 30 seconds may be too short. Try 60000 (1 minute) or higher.

.setSocketTimeout(60000) // Increased to 1 minute

Error: org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool

What causes it:
All connections in the pool are in use, and no new connections can be created because setMaxTotal or setDefaultMaxPerRoute limits have been reached. This is a sign of thread starvation.

How to fix it:

  1. Increase setMaxTotal and setDefaultMaxPerRoute if your server has the resources.
  2. Implement a queue or rate limiter in your application to prevent submitting more concurrent requests than the pool can handle.
  3. Ensure you are not leaking connections. Do not hold HttpClient instances open longer than necessary, though the pool manager handles this if configured correctly.

Error: 401 Unauthorized after initial success

What causes it:
The OAuth access token has expired (tokens typically last 1 hour). The SDK attempts to refresh the token automatically using the client credentials grant.

How to fix it:
Ensure your OAuthClientCredentials are correct. The SDK handles refresh automatically. If you see this error, it usually means the CLIENT_ID or CLIENT_SECRET is invalid, or the service account has been disabled in Genesys Cloud. Check the Genesys Cloud Admin console for the OAuth Client status.

Error: javax.net.ssl.SSLHandshakeException

What causes it:
The Java runtime does not trust the Genesys Cloud certificate. This can happen if your corporate proxy intercepts SSL traffic with an internal CA certificate that is not in the Java TrustStore.

How to fix it:
Import your corporate CA certificate into the Java TrustStore (cacerts) or configure the SSLContextBuilder to load the specific trust store file.

keytool -import -alias corporateCA -file corporate-ca.crt -keystore $JAVA_HOME/jre/lib/security/cacerts

Official References