Skip to main content

Generating Unique Transaction IDs in Apex

When integrating Salesforce with external systems, you often need a unique identifier that travels with each transaction across system boundaries. Auto-number fields and simple random strings each have drawbacks. A more robust approach combines a timestamp with a cryptographic hash to produce an identifier that is unique, URL-safe, and useful for deduplication.

Why Simple Approaches Fall Short

Auto-number fields are sequential and predictable. They only exist after a record is inserted, so they cannot identify a transaction before persistence. They also reset if records are deleted and recreated, creating potential collisions across systems.

Random strings (e.g., Math.random() based) lack sufficient entropy in high-throughput scenarios. Two near-simultaneous transactions could generate the same value, and there is no inherent ordering or traceability.

GUIDs/UUIDs are an option, but Apex does not have a native UUID generator, and external system APIs may expect shorter, URL-safe identifiers.

The Timestamp + Crypto Pattern

The core idea is to concatenate a high-precision timestamp (providing ordering and rough uniqueness) with a cryptographically random segment (providing collision resistance). The result is Base64 URL-encoded for safe transmission over HTTP.

public class TransactionIdGenerator {

/**
* Generates a unique, URL-safe transaction identifier.
* Format: [timestamp]-[cryptographic hash segment]
* Example: 20260303143025887-a4Bf9xKmPqRsT2Wv
*/
public static String generate() {
// High-precision timestamp for ordering and rough uniqueness
String timestamp = Datetime.now().format('yyyyMMddHHmmssSSS');

// 128-bit AES key provides cryptographic randomness
Blob aesKey = Crypto.generateAesKey(128);

// Convert to URL-safe Base64, trimmed for readability
String randomSegment = EncodingUtil.base64Encode(aesKey)
.replaceAll('[^a-zA-Z0-9]', '')
.substring(0, 16);

return timestamp + '-' + randomSegment;
}

/**
* Generates a shorter variant for systems with length constraints.
* Uses only the cryptographic segment (no timestamp prefix).
*/
public static String generateShort() {
Blob aesKey = Crypto.generateAesKey(256);
return EncodingUtil.base64Encode(aesKey)
.replaceAll('[^a-zA-Z0-9]', '')
.substring(0, 24);
}
}

Why This Works

  • Datetime.now().format('yyyyMMddHHmmssSSS') captures the current time down to the millisecond. This ensures near-uniqueness by itself for typical transaction volumes and provides natural chronological sorting.
  • Crypto.generateAesKey(128) produces 128 bits of cryptographically secure random data via the platform's FIPS-compliant random number generator. This is far stronger than Math.random().
  • Base64 encoding with character stripping converts the binary key to a URL-safe alphanumeric string. Removing +, /, and = characters prevents encoding issues in URLs and JSON payloads.

Storing and Using Transaction IDs

Transaction IDs are most useful when stored in External ID fields on integration-related objects. This enables upsert-based deduplication:

public class IntegrationService {

public static void sendOrder(Order__c ord) {
String txnId = TransactionIdGenerator.generate();

// Store on the record for traceability
ord.Integration_Transaction_Id__c = txnId;
update ord;

// Include in the outbound payload
Map<String, Object> payload = new Map<String, Object>{
'transactionId' => txnId,
'orderId' => ord.Id,
'orderNumber' => ord.OrderNumber__c
};

// Send to external system...
}

public static void handleCallback(String txnId, String externalStatus) {
// Look up by transaction ID for idempotent processing
List<Order__c> orders = [
SELECT Id, Integration_Status__c
FROM Order__c
WHERE Integration_Transaction_Id__c = :txnId
LIMIT 1
];

if (!orders.isEmpty()) {
orders[0].Integration_Status__c = externalStatus;
update orders[0];
}
}
}

Best Practices

ConsiderationRecommendation
Field typeText(40) External ID, Unique
IndexAlways index transaction ID fields for query performance
IdempotencyUse the transaction ID to detect and skip duplicate inbound messages
LoggingInclude the transaction ID in all integration log records for end-to-end tracing
LengthKeep under 40 characters to fit standard Salesforce text fields

When to Use This Pattern

  • Outbound integrations where you need to correlate responses to requests
  • Inbound webhook processing where duplicate messages must be detected
  • Batch integration jobs where each batch run needs a unique identifier
  • Any scenario where two systems need to agree on a shared reference that did not originate from either system's primary key