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 thanMath.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
| Consideration | Recommendation |
|---|---|
| Field type | Text(40) External ID, Unique |
| Index | Always index transaction ID fields for query performance |
| Idempotency | Use the transaction ID to detect and skip duplicate inbound messages |
| Logging | Include the transaction ID in all integration log records for end-to-end tracing |
| Length | Keep 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