Aggregator KeyGen Custom Implementation
Overview
AggregatorKeyGenerator is an interface that allows you to define a custom strategy for generating unique identifiers for saga transactions. the interface provides two methods for generating keys:
-
generateTransactionKey
- generates a unique identifier for a new saga transaction. -
generateIdempotencyKey
- generates an idempotency key for each span of the transaction.
Generating Unique Identifiers for Saga Transactions
Every saga transaction requires a globally unique identifier To supply a custom key generator for an Aggregator.
implement the AggregatorKeyGenerator
interface.
Its generateKey
method provides rich context you can leverage to construct a deterministic or randomized identifier: serviceName
, serviceVersion
, instanceId
, region
, zone
,executionMode
, and the sagaAggregator
metadata.
Providing a custom KeyGen is optional.
StackSaga ships with a production-ready default (DefaultKeyGen.class
).
A custom generator can still be valuable to:
-
align identifiers with your sharding/partitioning strategy,
-
improve index locality or read/write patterns,
-
embed minimal routing or observability hints,
-
satisfy organization-specific compliance or traceability rules.
The default generator produces time-ordered, high-entropy identifiers using this shape:
-
<serviceInitials>-<epochMillis>-<nanoId>
For example, with a service named order-service
, default IDs might look like:
-
OS-1713809175237-021575259417101
-
OS-1713809468378-117401549843120
-
OS-1713809493499-012220401009440
jnanoid is used to generate the random NanoID segment, providing excellent entropy and collision resistance. |
Generating Idempotency Keys for Saga Spans
Each span of a saga transaction, (i.e., each executor invocation) requires an idempotency key to ensure safe retries.
The generateIdempotencyKey
method provides context including serviceName
, serviceVersion
, instanceId
, region
, zone
, currentExecutor
, transactionId
, executionMode
, and a hashGenerator
utility.
it is highly recommended to use the provided hashGenerator to produce a fixed-length hash of a composite string that includes the transactionId , currentExecutor , and executionMode .
This approach ensures idempotency keys are compact, consistent in length, and collision-resistant.
|
Custom implementation of AggregatorKeyGenerator
The implementation is as follows.
Due to the fact that the AggregatorKeyGenerator’s all methods are `default methods, no need to implement all methods. you can implement only the required methods as needed.
|
@Component
public class PlaceOrderAggregatorKeyGenerator implements AggregatorKeyGenerator {
@Override
public String generateKey(String serviceName, String serviceVersion, String instanceId, String region, String zone, SagaAggregator sagaAggregator) {
StringBuilder regionKey = new StringBuilder();
for (char c : region.toCharArray()) {
regionKey.append((int) c);
}
return String.format("%s-%s-%s", serviceName , regionKey , UUID.randomUUID());
}
@Override
public String generateIdempotencyKey(String serviceName,
String serviceVersion,
String instanceId,
String region,
String zone,
String currentExecutor,
SagaAggregator sagaAggregator,
String transactionId,
ExecutionMode executionMode,
HashGenerator hashGenerator)
{
final String string = new StringJoiner(":")
.add(transactionId)
.add(currentExecutor)
.add(executionMode.name().toLowerCase())
.toString();
return hashGenerator.generateHash(string, HashGenerator.ALGType.MD5);
}
}
Register the implementation as a Spring bean (e.g., @Component ) and ensure it is stateless and thread-safe.
The framework may invoke it concurrently.
|
After implementing, wire it into the Aggregator via the keyGen
attribute on @SagaAggregator
:
@SagaAggregator(
version = @SagaAggregatorVersion(major = 1, minor = 0, patch = 1),
name = "PlaceOrderAggregator",
sagaSerializable = PlaceOrderAggregatorSample.class,
keyGen = PlaceOrderAggregatorKeyGenerator.class
)
public class PlaceOrderAggregator extends Aggregator {
// ...existing code...
}
After configuring the custom keygen to the aggregator that will be invoked by framework when start the transaction.
and then the generated ID will be stored to the Aggregator#aggregatorTransactionId
. and then it can be accessed by calling the get method from your aggregator class due to that your aggregator class has been extended by the Aggregator
.
Accessing the generated identifier:
@RequestMapping("/order-place")
@RestController
public class PlaceOrderController {
private final SagaTemplate<PlaceOrderAggregator> placeOrderAggregatorSagaTemplate;
public PlaceOrderController(SagaTemplate<PlaceOrderAggregator> placeOrderAggregatorSagaTemplate) {
this.placeOrderAggregatorSagaTemplate = placeOrderAggregatorSagaTemplate;
}
@PostMapping
public Object placeOrder() {
PlaceOrderAggregator placeOrderAggregator = new PlaceOrderAggregator();
// initialize aggregator payload
placeOrderAggregator.setUserName("mafei");
// ...existing code...
// Before process(): the ID is not yet assigned.
String aggregatorTransactionIdBefore = placeOrderAggregator.getAggregatorTransactionId();
System.out.println("aggregatorTransactionIdBefore = " + aggregatorTransactionIdBefore);
// process(): framework assigns aggregatorTransactionId and persists it in the Aggregator state.
placeOrderAggregatorSagaTemplate.process(
placeOrderAggregator,
InitializeOrderExecutor.class
);
// After process(): the ID is available.
String aggregatorTransactionIdAfter = placeOrderAggregator.getAggregatorTransactionId();
System.out.println("aggregatorTransactionIdAfter = " + aggregatorTransactionIdAfter);
return Collections.singletonMap("aggregator_id", aggregatorTransactionIdAfter);
}
}
Sample output:
aggregatorTransactionIdBefore = null
aggregatorTransactionIdAfter = ORDER-SERVICE-117115-71230b5c-79b5-418b-990b-058fba747869