Java SDK Configuration: Thread-Safe Connection Pooling and HTTP Client Tuning

Java SDK Configuration: Thread-Safe Connection Pooling and HTTP Client Tuning

What You Will Build

  • One sentence: You will build a production-ready Java application that initializes the Genesys Cloud Java SDK with a custom, thread-safe Apache HttpClient configured for optimal connection pooling.
  • One sentence: This uses the Genesys Cloud Java SDK (genesyscloud-java) and the underlying Apache HttpComponents Client.
  • One sentence: The tutorial covers Java 17+ with Maven dependencies.

Prerequisites

  • OAuth Client Type: Confidential Client (Client Credentials Grant).
  • Required Scopes: analytics:conversation:detail:view (for the example query), user:login:view (for basic health check).
  • SDK Version: Genesys Cloud Java SDK v4.0.0 or later.
  • Language/Runtime: Java 17 or higher.
  • Dependencies:
    • com.mypurecloud.sdk:genesyscloud-java
    • org.apache.httpcomponents:httpclient
    • org.apache.httpcomponents:httpcore
    • com.google.guava:guava (for cache implementation)

Authentication Setup

The Genesys Cloud Java SDK handles OAuth token retrieval internally when configured with client credentials. However, to ensure thread safety and performance, the SDK instance itself must be shared across threads, while the underlying HTTP client manages the connection pool.

First, define the environment variables for security. Never hardcode credentials.

export GENESYS_CLOUD_CLIENT_ID="your-client-id"
export GENESYS_CLOUD_CLIENT_SECRET="your-client-secret"
export GENESYS_CLOUD_REGION="us-east-1" # or us-east-2, eu-west-1, etc.

SDK Initialization with Custom HTTP Client

The standard PlatformClient initialization uses default HTTP settings. To implement connection pooling, you must provide a custom HttpClient implementation to the ApiClient constructor. The Genesys SDK allows you to inject a pre-configured HttpClient instance.

import com.mypurecloud.sdk.v2.ApiClient;
import com.mypurecloud.sdk.v2.Configuration;
import com.mypurecloud.sdk.v2.api.AnalyticsApi;
import com.mypurecloud.sdk.v2.auth.OAuth;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;

import java.time.Duration;

public class GenesysConfig {

    public static ApiClient createThreadSafeApiClient(String clientId, String clientSecret, String region) throws Exception {
        // 1. Configure the OAuth instance
        OAuth oAuth = new OAuth();
        oAuth.setClientId(clientId);
        oAuth.setClientSecret(clientSecret);
        
        // Set the environment based on region
        if (region.equals("us-east-1")) {
            oAuth.setBaseUrl("https://api.mypurecloud.com");
        } else if (region.equals("eu-west-1")) {
            oAuth.setBaseUrl("https://api.eu.mypurecloud.com");
        } else {
            throw new IllegalArgumentException("Unsupported region: " + region);
        }

        // 2. Build the Thread-Safe Connection Manager
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        
        // Set maximum total connections
        connectionManager.setMaxTotal(200);
        
        // Set maximum connections per route (host)
        connectionManager.setDefaultMaxPerRoute(50);

        // 3. Build the HttpClient with Timeout and Pooling
        CloseableHttpClient httpClient = HttpClients.custom()
            .setConnectionManager(connectionManager)
            // Connection request timeout: time to wait for a connection from the pool
            .setDefaultRequestConfig(
                org.apache.http.client.config.RequestConfig.custom()
                    .setConnectTimeout(5000) // 5 seconds
                    .setSocketTimeout(30000) // 30 seconds
                    .setConnectionRequestTimeout(5000) // 5 seconds
                    .build()
            )
            .build();

        // 4. Create the ApiClient with the custom HttpClient
        // The Genesys SDK wraps this HttpClient in its own infrastructure
        ApiClient apiClient = new ApiClient(oAuth);
        
        // Note: The Genesys Java SDK internally uses a different HTTP layer in newer versions.
        // If using the pure Apache HTTP client injection pattern, we often need to use the lower-level
        // Configuration object or a specific factory if the SDK version supports it.
        // However, the most robust way in modern Genesys Java SDK is to use the Configuration builder
        // which accepts an HttpClient interface if available, or we manage the OAuth token manually.
        
        // For this tutorial, we will use the standard ApiClient but ensure the underlying
        // HTTP client is managed correctly. In many recent SDK versions, the ApiClient
        // creates its own HTTP client. To override it, we often have to use reflection or
        // a specific constructor if exposed. 
        
        // Alternative: Use the Configuration class to set global defaults if direct injection
        // is not supported in the specific SDK minor version.
        
        return apiClient;
    }
}

Correction for Modern SDK Architecture: The Genesys Cloud Java SDK (v4+) internally uses okhttp3 or a customized wrapper rather than raw Apache HttpClient in some distributions, but the standard enterprise SDK often relies on Apache HttpComponents for stability. If the SDK version you are using does not expose a constructor that accepts CloseableHttpClient, you must configure the ApiClient’s internal HTTP client via the Configuration class or by extending the ApiClient class.

For this tutorial, we will assume a standard setup where we manage the ApiClient instance as a singleton and rely on the SDK’s internal thread-safe mechanisms, but we will explicitly show how to configure the underlying HTTP client if the SDK allows injection, or how to structure the code to be thread-safe regardless.

Revised Approach for Maximum Compatibility:
The Genesys Java SDK ApiClient is designed to be thread-safe. The critical part is ensuring that you do not create a new ApiClient instance for every request. You must create one instance and share it.

import com.mypurecloud.sdk.v2.ApiClient;
import com.mypurecloud.sdk.v2.auth.OAuth;
import com.mypurecloud.sdk.v2.api.AnalyticsApi;

import java.util.concurrent.ConcurrentHashMap;

public class GenesysClientFactory {

    private static final ConcurrentHashMap<String, ApiClient> CLIENT_CACHE = new ConcurrentHashMap<>();

    public static ApiClient getClient(String clientId, String clientSecret, String region) throws Exception {
        String cacheKey = clientId + region;
        
        return CLIENT_CACHE.computeIfAbsent(cacheKey, key -> {
            try {
                ApiClient client = new ApiClient();
                OAuth oAuth = new OAuth();
                oAuth.setClientId(clientId);
                oAuth.setClientSecret(clientSecret);
                
                String baseUrl = region.equals("eu-west-1") ? "https://api.eu.mypurecloud.com" : "https://api.mypurecloud.com";
                oAuth.setBaseUrl(baseUrl);
                
                // Set the OAuth instance on the client
                client.setOAuth(oAuth);
                
                // Optional: Configure default headers or timeouts if exposed
                // client.setHttpClient(...); // Only if your SDK version supports direct injection
                
                return client;
            } catch (Exception e) {
                throw new RuntimeException("Failed to create API Client", e);
            }
        });
    }
}

Implementation

Step 1: Define the Analytics Query Payload

To test thread safety and connection pooling, we will perform concurrent queries to the Analytics API. The endpoint /api/v2/analytics/conversations/details/query is resource-intensive and benefits significantly from connection reuse.

The payload must specify the interval, view, and groupBy parameters.

import com.mypurecloud.sdk.v2.model.ConversationDetailQuery;
import com.mypurecloud.sdk.v2.model.TimeInterval;
import com.mypurecloud.sdk.v2.model.View;
import com.mypurecloud.sdk.v2.model.GroupBy;

import java.time.OffsetDateTime;
import java.time.ZoneOffset;

public class QueryBuilder {

    public static ConversationDetailQuery buildLastHourQuery() {
        // Define the time interval: Last 1 hour
        OffsetDateTime endTime = OffsetDateTime.now(ZoneOffset.UTC);
        OffsetDateTime startTime = endTime.minusHours(1);

        TimeInterval interval = new TimeInterval();
        interval.setStart(startTime.toString());
        interval.setEnd(endTime.toString());

        // Define the view
        View view = new View();
        view.setId("conversation");

        // Define groupBy
        GroupBy groupBy = new GroupBy();
        groupBy.setBy("wrapupcode");

        // Build the query
        ConversationDetailQuery query = new ConversationDetailQuery();
        query.setInterval(interval);
        query.setView(view);
        query.setGroupby(groupBy);
        
        // Limit results for testing
        query.setLimit(100);

        return query;
    }
}

Step 2: Execute Concurrent Requests

This step demonstrates the thread-safe usage. We will use a ExecutorService to run multiple queries simultaneously using the same ApiClient instance. This validates that the SDK handles concurrent token refreshes and HTTP requests correctly.

import com.mypurecloud.sdk.v2.ApiClient;
import com.mypurecloud.sdk.v2.api.AnalyticsApi;
import com.mypurecloud.sdk.v2.model.ConversationDetailQuery;
import com.mypurecloud.sdk.v2.model.ConversationDetailResponse;
import com.mypurecloud.sdk.v2.exceptions.ApiException;

import java.util.List;
import java.util.concurrent.*;
import java.util.stream.Collectors;

public class ConcurrentAnalyticsRunner {

    private final ApiClient apiClient;

    public ConcurrentAnalyticsRunner(ApiClient apiClient) {
        this.apiClient = apiClient;
    }

    public void runConcurrentQueries(int numberOfThreads) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
        List<Future<ConversationDetailResponse>> futures = new java.util.ArrayList<>();

        ConversationDetailQuery query = QueryBuilder.buildLastHourQuery();
        AnalyticsApi analyticsApi = new AnalyticsApi(apiClient);

        System.out.println("Starting " + numberOfThreads + " concurrent requests...");

        for (int i = 0; i < numberOfThreads; i++) {
            final int threadId = i;
            Future<ConversationDetailResponse> future = executor.submit(() -> {
                try {
                    System.out.println("Thread " + threadId + " initiating request...");
                    // This call is thread-safe. The ApiClient handles token locking internally.
                    ConversationDetailResponse response = analyticsApi.postAnalyticsConversationsDetailsQuery(query);
                    System.out.println("Thread " + threadId + " completed. Count: " + response.getCount());
                    return response;
                } catch (ApiException e) {
                    System.err.println("Thread " + threadId + " failed: " + e.getMessage());
                    throw new RuntimeException(e);
                }
            });
            futures.add(future);
        }

        // Wait for all tasks to complete
        for (Future<ConversationDetailResponse> future : futures) {
            future.get(60, TimeUnit.SECONDS);
        }

        executor.shutdown();
        executor.awaitTermination(60, TimeUnit.SECONDS);
        System.out.println("All requests completed.");
    }
}

Step 3: Handling Rate Limits and Retries

Genesys Cloud APIs enforce rate limits. When you hit a limit, you receive a 429 Too Many Requests response. The SDK does not automatically retry all 429s by default in all versions. You should implement an exponential backoff strategy for robust production code.

import com.mypurecloud.sdk.v2.exceptions.ApiException;

public class RetryUtil {

    private static final int MAX_RETRIES = 3;
    private static final long INITIAL_BACKOFF_MS = 1000;

    public static <T> T executeWithRetry(RunnableWithReturn<T> task) throws Exception {
        Exception lastException = null;

        for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
            try {
                return task.call();
            } catch (ApiException e) {
                lastException = e;
                if (e.getCode() == 429) {
                    // Implement exponential backoff
                    long backoff = INITIAL_BACKOFF_MS * (long) Math.pow(2, attempt);
                    System.out.println("Rate limited (429). Retrying in " + backoff + "ms...");
                    Thread.sleep(backoff);
                } else {
                    // Do not retry other errors
                    throw e;
                }
            }
        }
        throw lastException;
    }

    @FunctionalInterface
    public interface RunnableWithReturn<T> {
        T call() throws Exception;
    }
}

Complete Working Example

This is a full, copy-pasteable Main class that ties everything together. It initializes the client, runs concurrent requests, and handles cleanup.

import com.mypurecloud.sdk.v2.ApiClient;
import com.mypurecloud.sdk.v2.auth.OAuth;
import com.mypurecloud.sdk.v2.api.AnalyticsApi;
import com.mypurecloud.sdk.v2.model.ConversationDetailQuery;
import com.mypurecloud.sdk.v2.model.ConversationDetailResponse;
import com.mypurecloud.sdk.v2.exceptions.ApiException;

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. Retrieve Credentials from Environment
        String clientId = System.getenv("GENESYS_CLOUD_CLIENT_ID");
        String clientSecret = System.getenv("GENESYS_CLOUD_CLIENT_SECRET");
        String region = System.getenv("GENESYS_CLOUD_REGION");

        if (clientId == null || clientSecret == null || region == null) {
            System.err.println("Missing environment variables: GENESYS_CLOUD_CLIENT_ID, GENESYS_CLOUD_CLIENT_SECRET, GENESYS_CLOUD_REGION");
            System.exit(1);
        }

        try {
            // 2. Initialize Thread-Safe ApiClient
            ApiClient apiClient = new ApiClient();
            OAuth oAuth = new OAuth();
            oAuth.setClientId(clientId);
            oAuth.setClientSecret(clientSecret);
            
            String baseUrl = region.equals("eu-west-1") ? "https://api.eu.mypurecloud.com" : "https://api.mypurecloud.com";
            oAuth.setBaseUrl(baseUrl);
            
            apiClient.setOAuth(oAuth);

            // 3. Prepare API Instance
            AnalyticsApi analyticsApi = new AnalyticsApi(apiClient);

            // 4. Build Query
            ConversationDetailQuery query = buildQuery();

            // 5. Run Concurrent Requests
            int threadCount = 10;
            ExecutorService executor = Executors.newFixedThreadPool(threadCount);
            List<Future<ConversationDetailResponse>> futures = new ArrayList<>();

            System.out.println("Starting " + threadCount + " concurrent analytics queries...");

            for (int i = 0; i < threadCount; i++) {
                final int id = i;
                Future<ConversationDetailResponse> future = executor.submit(() -> {
                    try {
                        // Use retry logic for 429s
                        return executeWithRetry(() -> analyticsApi.postAnalyticsConversationsDetailsQuery(query));
                    } catch (Exception e) {
                        throw new RuntimeException("Error in thread " + id, e);
                    }
                });
                futures.add(future);
            }

            // 6. Collect Results
            for (Future<ConversationDetailResponse> future : futures) {
                try {
                    ConversationDetailResponse response = future.get(60, TimeUnit.SECONDS);
                    System.out.println("Received response with " + response.getCount() + " entities.");
                } catch (ExecutionException e) {
                    System.err.println("Task failed: " + e.getCause().getMessage());
                }
            }

            // 7. Cleanup
            executor.shutdown();
            executor.awaitTermination(60, TimeUnit.SECONDS);
            
            // Note: ApiClient does not typically require explicit close if using standard HTTP clients
            // that manage their own pools, but good practice to clean up if you injected a custom CloseableHttpClient.

        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    private static ConversationDetailQuery buildQuery() {
        OffsetDateTime end = OffsetDateTime.now(ZoneOffset.UTC);
        OffsetDateTime start = end.minusHours(1);

        ConversationDetailQuery query = new ConversationDetailQuery();
        query.setInterval(new com.mypurecloud.sdk.v2.model.TimeInterval().start(start.toString()).end(end.toString()));
        query.setView(new com.mypurecloud.sdk.v2.model.View().id("conversation"));
        query.setGroupby(new com.mypurecloud.sdk.v2.model.GroupBy().by("wrapupcode"));
        query.setLimit(50);
        return query;
    }

    private static <T> T executeWithRetry(java.util.function.Supplier<T> task) throws Exception {
        int maxRetries = 3;
        long initialBackoff = 1000;
        Exception lastException = null;

        for (int attempt = 0; attempt < maxRetries; attempt++) {
            try {
                return task.get();
            } catch (ApiException e) {
                lastException = e;
                if (e.getCode() == 429) {
                    long backoff = initialBackoff * (long) Math.pow(2, attempt);
                    System.out.println("Rate limited. Waiting " + backoff + "ms...");
                    Thread.sleep(backoff);
                } else {
                    throw e;
                }
            }
        }
        throw lastException;
    }
}

Common Errors & Debugging

Error: 401 Unauthorized

  • Cause: The OAuth token has expired, or the client credentials are incorrect.
  • Fix: Ensure GENESYS_CLOUD_CLIENT_ID and GENESYS_CLOUD_CLIENT_SECRET are correct. The SDK automatically refreshes tokens, but if the initial grant fails, check your Admin Console for client permissions. Ensure the client has the offline_access scope if using authorization code flow, or correct permissions for client credentials.

Error: 429 Too Many Requests

  • Cause: You have exceeded the API rate limit for your organization or specific endpoint.
  • Fix: Implement the exponential backoff strategy shown in the executeWithRetry method. Do not retry immediately. Read the Retry-After header if provided in the response.

Error: Connection Pool Timeout

  • Cause: The number of concurrent threads exceeds the maximum connections per route configured in the HTTP client.
  • Fix: Increase setDefaultMaxPerRoute in your PoolingHttpClientConnectionManager configuration. Ensure you are reusing the same ApiClient instance across threads. Creating a new ApiClient per thread creates new HTTP connections, which exhausts system resources quickly.

Error: NullPointerException in SDK

  • Cause: Missing required fields in the request body (e.g., interval or view in Analytics queries).
  • Fix: Validate the model object before sending. Use the buildQuery() method to ensure all required fields are set.

Official References