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
CognigyTokenProviderrefresh logic. Ensure thetokenExpirytimestamp 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 verifyexpires_inparsing.
Error: HTTP 403 Forbidden
- Cause: The OAuth token lacks the
bot:executescope. - Fix: Regenerate the token request with
scope=bot:executein the form body. Verify your Cognigy.AI API key permissions in the project settings. - Code Fix: The
HttpRequestbody explicitly includes&scope=bot:execute. Do not remove this parameter.
Error: MongoDB TTL Index Not Evicting Documents
- Cause: The
expiresAtfield contains a non-date type or the index was created before documents were inserted. - Fix: Ensure
expiresAtis stored as anISODateorInstant. MongoDB TTL scans run every 60 seconds. - Code Fix: The
ensureTtlIndex()method creates the index onexpiresAtwithexpireAfter(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
executeWithRetrymethod implements automatic retry with doubling delay for 429 and 5xx responses. IncreasemaxRetriesor adjustdelayif your traffic pattern requires it.