Persisting Multi-Turn Conversation State in NICE Cognigy.AI with Java and MongoDB

Persisting Multi-Turn Conversation State in NICE Cognigy.AI with Java and MongoDB

What You Will Build

  • A Java Spring Boot service that captures dialog execution snapshots when Cognigy.AI reaches checkpoint nodes.
  • Serialization of nested conversation variables to MongoDB with automatic TTL-based eviction for stale sessions.
  • Context restoration via webhook injection when users resume interactions across different channels.
  • Implementation covers Java 17, Spring Boot 3, MongoDB Java Driver 4, and Cognigy.AI REST API.

Prerequisites

  • Cognigy.AI project with OAuth 2.0 Client Credentials configured
  • Required OAuth scope: bot:execute
  • MongoDB 5.0+ instance
  • Java 17 runtime
  • Spring Boot 3.2+, com.fasterxml.jackson.core, MongoDB Java Driver 4.9+
  • Maven or Gradle build tool

Authentication Setup

Cognigy.AI uses OAuth 2.0 Client Credentials for programmatic access. You must exchange your client ID and secret for a bearer token before calling the execution API. The token expires after 3600 seconds and requires caching with automatic refresh.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class CognigyTokenProvider {
    private final String region;
    private final String clientId;
    private final String clientSecret;
    private String cachedToken;
    private long tokenExpiry;
    private final HttpClient httpClient;
    private final ObjectMapper mapper;

    public CognigyTokenProvider(String region, String clientId, String clientSecret) {
        this.region = region;
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.httpClient = HttpClient.newBuilder()
            .connectTimeout(java.time.Duration.ofSeconds(10))
            .build();
        this.mapper = new ObjectMapper();
        this.tokenExpiry = 0;
    }

    public String getAccessToken() throws Exception {
        if (System.currentTimeMillis() < tokenExpiry - 30000) {
            return cachedToken;
        }
        String credentials = Base64.getEncoder().encodeToString(
            (clientId + ":" + clientSecret).getBytes()
        );
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://" + region + ".cognigy.ai/api/oauth/token"))
            .header("Authorization", "Basic " + credentials)
            .header("Content-Type", "application/x-www-form-urlencoded")
            .POST(HttpRequest.BodyPublishers.ofString(
                "grant_type=client_credentials&scope=bot:execute"
            ))
            .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() != 200) {
            throw new RuntimeException("Token acquisition failed with status " + response.statusCode());
        }

        JsonNode json = mapper.readTree(response.body());
        cachedToken = json.get("access_token").asText();
        tokenExpiry = System.currentTimeMillis() + (json.get("expires_in").asLong() * 1000);
        return cachedToken;
    }
}

OAuth Scope Requirement: bot:execute is mandatory for the /api/v1/bot/{botId}/execute endpoint. Missing this scope returns HTTP 403.

Implementation

Step 1: Configure MongoDB with TTL Index and Data Model

MongoDB handles session eviction natively through TTL indexes. You must create a collection with an index on the expiresAt field. The index automatically deletes documents after the specified timestamp passes.

import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.IndexOptions;
import org.bson.Document;
import java.time.Instant;
import java.util.concurrent.TimeUnit;

public class SessionStorage {
    private final MongoCollection<Document> collection;
    private static final int DEFAULT_TTL_HOURS = 24;

    public SessionStorage(MongoDatabase database) {
        this.collection = database.getCollection("conversation_states");
        ensureTtlIndex();
    }

    private void ensureTtlIndex() {
        String indexName = "expiresAt_1";
        if (!collection.getIndexModelNames().contains(indexName)) {
            collection.createIndex(
                new Document("expiresAt", 1),
                new IndexOptions().name(indexName).expireAfter(0, TimeUnit.SECONDS)
            );
        }
    }

    public void saveSession(String sessionId, String channel, Document variables, Instant expiresAt) {
        Document state = new Document()
            .append("sessionId", sessionId)
            .append("channel", channel)
            .append("variables", variables)
            .append("expiresAt", expiresAt)
            .append("updatedAt", Instant.now());

        collection.replaceOne(
            new Document("sessionId", sessionId),
            state,
            com.mongodb.client.model.UpdateOptions().upsert(true)
        );
    }

    public Document restoreSession(String sessionId) {
        return collection.find(new Document("sessionId", sessionId)).first();
    }
}

Step 2: Capture Dialog Snapshots at Checkpoint Nodes

Cognigy.AI returns the full dialog state in the execution response. When the response indicates a checkpoint, your service extracts the variables object and persists it. The service exposes a /capture endpoint that your channel adapter or orchestrator calls after receiving the Cognigy response.

HTTP Request to /capture

POST /capture
Content-Type: application/json

{
  "sessionId": "sess_8f7a9c2b",
  "channel": "webchat",
  "status": "checkpoint",
  "variables": {
    "user": { "name": "Alex", "tier": "premium" },
    "cart": { "items": [{"sku": "A100", "qty": 2}], "total": 49.98 },
    "context": { "lastNode": "collect_shipping", "attempts": 1 }
  }
}

Java Controller Implementation

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.bson.Document;
import java.time.Instant;

@RestController
@RequestMapping("/api/v1/state")
public class StateController {
    private final SessionStorage sessionStorage;
    private final ObjectMapper mapper;

    public StateController(SessionStorage sessionStorage) {
        this.sessionStorage = sessionStorage;
        this.mapper = new ObjectMapper();
    }

    @PostMapping("/capture")
    public ResponseEntity<?> captureCheckpoint(@RequestBody ObjectNode cognigyResponse) {
        try {
            String status = cognigyResponse.path("status").asText();
            if (!"checkpoint".equals(status) && !cognigyResponse.path("checkpoint").isBoolean(true)) {
                return ResponseEntity.accepted().body("Not a checkpoint. Skipping persistence.");
            }

            String sessionId = cognigyResponse.get("sessionId").asText();
            String channel = cognigyResponse.get("channel").asText();
            Document variables = Document.parse(
                cognigyResponse.get("variables").toString()
            );

            Instant expiresAt = Instant.now().plusSeconds(86400);
            sessionStorage.saveSession(sessionId, channel, variables, expiresAt);

            return ResponseEntity.ok(new Document("status", "persisted", "sessionId", sessionId));
        } catch (Exception e) {
            return ResponseEntity.status(500).body("Persistence failed: " + e.getMessage());
        }
    }
}

Step 3: Restore Context via Webhook Injection

When a user resumes a session through a different channel, your orchestration layer calls the /restore endpoint. The service returns the serialized variables. You inject these variables into the variables field of the Cognigy execute request. Cognigy merges incoming variables with internal state, allowing seamless continuation.

HTTP Request to /restore

GET /api/v1/state/restore?sessionId=sess_8f7a9c2b

Java Controller Implementation

    @GetMapping("/restore")
    public ResponseEntity<?> restoreContext(@RequestParam String sessionId) {
        Document savedState = sessionStorage.restoreSession(sessionId);
        if (savedState == null) {
            return ResponseEntity.noContent().build();
        }
        return ResponseEntity.ok(savedState.get("variables"));
    }

Cognigy Execute API Injection Pattern

POST https://{region}.cognigy.ai/api/v1/bot/{botId}/execute
Authorization: Bearer {access_token}
Content-Type: application/json

{
  "sessionId": "sess_8f7a9c2b",
  "channel": "sms",
  "input": "I need to continue my order",
  "variables": {
    "user": { "name": "Alex", "tier": "premium" },
    "cart": { "items": [{"sku": "A100", "qty": 2}], "total": 49.98 },
    "context": { "lastNode": "collect_shipping", "attempts": 1 }
  }
}

OAuth Scope Requirement: bot:execute is required for state injection during execution.

Step 4: Execution Client with Retry Logic and Error Handling

Production integrations must handle rate limits and transient failures. The following client wraps the Cognigy execute call with exponential backoff for HTTP 429 and 5xx responses.

import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import com.fasterxml.jackson.databind.ObjectMapper;

public class CognigyExecutor {
    private final String region;
    private final String botId;
    private final CognigyTokenProvider tokenProvider;
    private final HttpClient httpClient;
    private final ObjectMapper mapper;

    public CognigyExecutor(String region, String botId, CognigyTokenProvider tokenProvider) {
        this.region = region;
        this.botId = botId;
        this.tokenProvider = tokenProvider;
        this.httpClient = HttpClient.newBuilder()
            .connectTimeout(java.time.Duration.ofSeconds(10))
            .build();
        this.mapper = new ObjectMapper();
    }

    public String executeWithRetry(String sessionId, String channel, String input, String variablesJson) throws Exception {
        String token = tokenProvider.getAccessToken();
        String payload = mapper.createObjectNode()
            .put("sessionId", sessionId)
            .put("channel", channel)
            .put("input", input)
            .set("variables", mapper.readTree(variablesJson))
            .toString();

        int maxRetries = 3;
        long delay = 1000;

        for (int attempt = 1; attempt <= maxRetries; attempt++) {
            HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://" + region + ".cognigy.ai/api/v1/bot/" + botId + "/execute"))
                .header("Authorization", "Bearer " + token)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(payload))
                .build();

            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            int status = response.statusCode();

            if (status == 200) {
                return response.body();
            } else if ((status == 429 || status >= 500) && attempt < maxRetries) {
                Thread.sleep(delay);
                delay *= 2;
            } else {
                throw new RuntimeException("Cognigy execution failed with status " + status + ": " + response.body());
            }
        }
        throw new RuntimeException("Max retries exceeded");
    }
}

Complete Working Example

The following Maven configuration and Spring Boot application structure provides a runnable foundation. Replace placeholder values in application.properties with your credentials.

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.3</version>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>cognigy-state-service</artifactId>
    <version>1.0.0</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongodb-driver-sync</artifactId>
            <version>4.9.1</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
    </dependencies>
</project>

application.properties

cognigy.region=eu1
cognigy.client-id=your_client_id
cognigy.client-secret=your_client_secret
cognigy.bot-id=your_bot_id
mongodb.uri=mongodb://localhost:27017
mongodb.database=cognigy_sessions

CognigyStateApplication.java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;

@SpringBootApplication
@ConfigurationProperties(prefix = "cognigy")
public class CognigyStateApplication {
    private String region;
    private String clientId;
    private String clientSecret;
    private String botId;
    private String mongodbUri;
    private String mongodbDatabase;

    public static void main(String[] args) {
        SpringApplication.run(CognigyStateApplication.class, args);
    }

    @Bean
    public MongoDatabase mongoDatabase() {
        return MongoClients.create(mongodbUri).getDatabase(mongodbDatabase);
    }

    @Bean
    public SessionStorage sessionStorage(MongoDatabase database) {
        return new SessionStorage(database);
    }

    @Bean
    public CognigyTokenProvider tokenProvider() {
        return new CognigyTokenProvider(region, clientId, clientSecret);
    }

    @Bean
    public CognigyExecutor cognigyExecutor() {
        return new CognigyExecutor(region, botId, tokenProvider());
    }

    // Getters required for @ConfigurationProperties
    public void setRegion(String region) { this.region = region; }
    public void setClientId(String clientId) { this.clientId = clientId; }
    public void setClientSecret(String clientSecret) { this.clientSecret = clientSecret; }
    public void setBotId(String botId) { this.botId = botId; }
    public void setMongodbUri(String mongodbUri) { this.mongodbUri = mongodbUri; }
    public void setMongodbDatabase(String mongodbDatabase) { this.mongodbDatabase = mongodbDatabase; }
}

Deploy this service, expose the /capture and /restore endpoints behind your internal network or API gateway, and wire your channel adapters to call them during session transitions.

Common Errors & Debugging

Error: HTTP 401 Unauthorized

  • Cause: The OAuth token expired or was never cached correctly.
  • Fix: Verify the CognigyTokenProvider refresh logic. Ensure the tokenExpiry timestamp accounts for clock skew. Add a 30-second safety buffer before expiry.
  • Code Fix: The provided getAccessToken() method already implements a 30-second buffer. If issues persist, log the raw token response to verify expires_in parsing.

Error: HTTP 403 Forbidden

  • Cause: The OAuth token lacks the bot:execute scope.
  • Fix: Regenerate the token request with scope=bot:execute in the form body. Verify your Cognigy.AI API key permissions in the project settings.
  • Code Fix: The HttpRequest body explicitly includes &scope=bot:execute. Do not remove this parameter.

Error: MongoDB TTL Index Not Evicting Documents

  • Cause: The expiresAt field contains a non-date type or the index was created before documents were inserted.
  • Fix: Ensure expiresAt is stored as an ISODate or Instant. MongoDB TTL scans run every 60 seconds.
  • Code Fix: The ensureTtlIndex() method creates the index on expiresAt with expireAfter(0, TimeUnit.SECONDS). Verify the collection name matches exactly.

Error: HTTP 429 Too Many Requests

  • Cause: Cognigy.AI rate limits execution calls per bot or per tenant.
  • Fix: Implement exponential backoff. Throttle channel adapters during peak traffic.
  • Code Fix: The executeWithRetry method implements automatic retry with doubling delay for 429 and 5xx responses. Increase maxRetries or adjust delay if your traffic pattern requires it.

Official References