Domain Entity and Event Sourcing
The Domain-Entity is the first building block of a saga on the orchestrator side — the typed object that carries a transaction’s state from step to step and identifies which saga domain it belongs to. Every other orchestrator component is generically bound to this class (StackSagaKafkaTemplate<OrderDomainEntity, ?>, AbstractEventManager<OrderDomainEntity, ?>), so a new orchestrator implementation begins here, then moves on to defining its Topics and EventManager, triggering sagas with the StackSagaKafkaTemplate, and finally tuning Configuration.
What is Domain Entity Event Sourcing?
The Domain-Entity object serves as the central data container throughout the entire lifecycle of a distributed transaction. It functions as a shared, strongly typed data bucket, enabling each executor (span) in the saga workflow to consume existing transactional data and contribute newly produced state as the business process moves forward.
Rather than mutating a single record in place without history, StackSaga employs Domain-Entity Event Sourcing. Every significant atomic transition generates a new, immutable snapshot of the Domain-Entity state, which is immediately committed to the Event Store:
-
Baseline State: Upon transaction initiation, the initial state of the Domain-Entity is persisted as the baseline version with status
STARTED. -
Step Snapshots: As each atomic executor completes its operation, a point-in-time snapshot of the updated Domain-Entity is persisted, stamped with the unique span identifier and execution metadata.
Domain-Entity event sourcing provides two core architectural capabilities:
-
Transaction Re-Invoke & Automatic Recovery (Retrying and Restoring)
-
If an atomic execution fails due to a transient issue (such as temporary network latency or service unavailability), the saga engine can safely retry the operation. If a transaction halts due to a system crash or node restart, it can be seamlessly restored and re-invoked from the exact point of interruption. Because historical state is preserved at every milestone, the engine restores the precise Domain-Entity snapshot that existed when the failure occurred, eliminating data corruption and partial-state inconsistencies.
-
-
Full Transaction Observability & Auditability via Dashboard
-
Storing every discrete state transition provides complete chronological traceability. System administrators and developers can inspect the Domain-Entity state before and after each atomic execution runs, making it straightforward to audit the evolution of a transaction, diagnose edge-case anomalies, and inspect payload mutations through the Trace-Window dashboard.
-
Domain Entity as the Saga Domain Identifier
In addition to serving as the transactional data carrier, the DomainEntity class fulfills a fundamental architectural role in the StackSaga framework: it serves as the Saga Domain Identifier.
The framework uses the DomainEntity class type to distinguish business domains and bind together all executors responsible for handling that specific workflow. In other words, the Domain-Entity class acts as the strongly typed anchor for the entire saga:
-
For example, in an e-commerce platform, an order fulfillment process uses
OrderDomainEntityas its domain identifier, while a customer subscription process usesSubscriptionDomainEntity. -
Each business domain defines its own dedicated
DomainEntitysubclass containing fields relevant to that business context.
|
Each distinct Long-Running Transaction (LRT) requires its own dedicated Beyond transaction identification, the
This design guarantees compile-time type safety, structured routing, and clean domain isolation across the entire application. |
Domain Entity Lifecycle & State Progression in Saga Execution
Throughout a saga transaction, the DomainEntity traverses three distinct lifecycle phases:
-
Phase 1: State Creation & Initialization (
STARTED) -
Phase 2: Progressive Evolution & Forward Mutation (
IN_PROGRESS) -
Phase 3: Lifecycle Termination (
COMPLETEDorFAILED)
Understanding how data is populated, consumed, mutated, and finalized across these phases is essential for designing reliable, idempotent saga workflows.
Phase 1: State Creation & Initialization
The lifecycle begins with the client application before the transaction is handed to the saga orchestration engine:
-
Custom Instance Creation: The developer instantiates the custom
DomainEntitysubclass (for example,OrderDomainEntity) and populates it with the initial request payload received from the customer or upstream service (e.g.,user_id,amount,items). -
Initial Field State: Only the initial request attributes are populated at this point. All downstream attributes (such as
delivery_details,order_id, andpayment_id) remainnull. -
Saga Handoff: The entity is passed to the orchestrator via the template initiation method:
sagaTemplate.init(orderDomainEntity) .startWith(UserDetailExecutor.class) ... .execute(); -
Baseline Event Persistence: Before invoking the first executor, the orchestration engine serializes the entity and commits the initial snapshot (Version 0) to the Event Store, tagging the transaction with status
STARTED.
Phase 2: Progressive Evolution & Forward Mutation
Once the saga engine assumes execution control, it coordinates the sequential execution of atomic spans (QueryExecutor and CommandExecutor):
-
Selective State Consumption (Read Flow):
Each executor does not need the entire dataset; it queries only the specific properties it requires from the current Domain-Entity snapshot. For instance:
-
UserDetailExecutorreadsuser_idto query customer shipping details. -
OrderInitializeExecutorreadsuser_idandamountto register the order. -
MakePaymentExecutorreadsorder_idandamountto process the payment.
-
-
Forward State Mutation (
doProcess()):When an executor completes its external invocation, it enriches the Domain-Entity using setter methods (e.g.,
domainEntity.setDeliveryDetails(…),domainEntity.setOrderId(…)). Upon return fromdoProcess(), the framework automatically serializes the mutated Domain-Entity and writes an immutable snapshot to the Event Store, stamped with the unique span identifier and marked with statusIN_PROGRESS. -
State Preservation in Non-Mutating Spans:
Not every executor needs to add new properties to the Domain-Entity. For example, ReserveItemsExecutorreadsitemsandorder_idto reserve stock in an external warehouse service. Even though it performs a critical transactional command, it does not add new fields to the domain entity. In this step, existing fields remain preserved, and the engine persists a milestone snapshot confirming successful execution of that atomic span. -
Compensation Immutability Rule (
doRevert()):During compensating executions (
doRevert()), the Domain-Entity is strictly read-only. Compensations must never alter the Domain-Entity state, ensuring that the historical audit record of what actually occurred remains immutable. Any metadata required for compensation (such as authorization tokens or transient cancellation IDs) is managed independently through the Revert-Hint-Store.
Phase 3: Lifecycle Termination
The saga lifecycle terminates in one of two deterministic states:
-
Successful Completion (
COMPLETED):-
When all executors in the workflow successfully return
stepManager.next(…)without encountering a pivot exception (unrecoverable failure), the saga workflow reaches its terminal node. -
The engine transitions the transaction to status
COMPLETED, and the final, fully enriched Domain-Entity snapshot is sealed in the Event Store as the permanent record of the completed business transaction.
-
-
Pivot Failure & Backward Compensation (
FAILED):-
If an atomic execution encounters a non-retryable error (a pivot execution failure), forward progression immediately halts.
-
The saga engine initiates backward rollback, executing
doRevert()on all previously completed command executors in reverse order. -
Once all compensations are finalized, the transaction terminates with status
FAILED. The Domain-Entity retains its last valid forward state prior to the failure, providing developers and operators with exact forensic data in the Trace-Window dashboard.
-
Visualizing State Progression: The Place-Order Architecture
The following architectural diagram illustrates the complete step-by-step lifecycle and state progression of OrderDomainEntity during a place-order transaction:
Understanding the Diagram Layout
The diagram is organized into three architectural columns across the execution timeline:
-
Left Column (Saga Executors): Displays the atomic execution units (
QueryExecutorandCommandExecutor), detailing the specific method invocations (doProcess()vsdoRevert()) and external service interactions. -
Center Spine (Execution Timeline & Data Flow): Illustrates the chronological flow from initiation (
INIT) through steps01to04down to terminal completion (END).-
Blue Dashed Connectors (
Reads from State): Depict an executor reading specific input properties from the previous snapshot. -
Green Solid Connectors (
Updates State): Depict state updates flowing fromdoProcess()to produce a new persisted milestone in the Event Store.
-
-
Right Column (OrderDomainEntity Snapshots): Displays the point-in-time state of the Domain-Entity committed to the Event Store after each step:
-
Mint Green Rows (
✓ initial/✓ updated): Highlight newly initialized or mutated properties. -
Gray Rows (
preserved): Highlight previously captured properties that remain intact and accessible. -
White Rows (
null): Indicate uninitialized properties awaiting downstream execution.
-
Step-by-Step State Evolution Matrix
| Step | Executor & Type | State Read (Input) | State Mutation & Lifecycle Event |
|---|---|---|---|
INIT |
Client Controller |
Order request payload from client. |
Creates |
01 |
UserDetailExecutor |
Reads |
Queries external |
02 |
OrderInitializeExecutor |
Reads |
Calls |
03 |
ReserveItemsExecutor |
Reads |
Calls |
04 |
MakePaymentExecutor |
Reads |
Calls |
END |
Saga Engine |
Final transaction validation. |
All executors completed successfully without pivot exceptions. Transaction reaches |
Key Architectural Takeaways
-
Decoupled Microservice Architecture: Microservices never communicate directly with each other to pass context. The
DomainEntityacts as the single source of truth and shared data carrier across the entire distributed workflow. -
Monotonic State Accumulation: The Domain-Entity accumulates state progressively—each executor enriches the shared bucket with the output of its execution so subsequent executors can consume it.
-
State Preservation vs. Modification: An executor is not required to mutate the Domain-Entity. It can execute business logic and succeed while keeping existing state preserved.
-
Event Sourcing & Re-Invoke Guarantees: Because each step’s snapshot is committed to the Event Store, the engine can restore the exact historical state to retry a failed operation or compensate previous steps without data corruption.
-
Strict Mutation Boundary: State modifications are strictly confined to
doProcess(). Compensating executions (doRevert()) remain read-only, with rollback metadata isolated in the Revert-Hint-Store.
Creating a Custom Domain-Entity
To define a custom Domain-Entity, create a class that extends the DomainEntity base class provided by the framework and annotate it with @SagaDomainEntity.
You can declare any fields required to maintain state across the saga workflow (such as username, totalAmount, productItems, etc.). When starting a transaction, pass an initialized instance of this custom class to StackSagaKafkaTemplate.init(…).startWith(…).execute().
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.stacksaga.DomainEntity;
import org.stacksaga.MissingPropertyCollector;
import org.stacksaga.annotation.SagaDomainEntity;
import org.stacksaga.annotation.SagaDomainEntityVersion;
@Getter
@Setter
(1)
@SagaDomainEntity(
version = @SagaDomainEntityVersion(major = 1, minor = 0, patch = 0),
name = "OrderDomainEntity"
)
public class OrderDomainEntity extends DomainEntity { (2)
(4)
@JsonProperty("username")
private String username;
@JsonProperty("order_id")
private String orderId;
@JsonProperty("total_amount")
private double totalAmount;
@JsonProperty("payment_reference_id")
private String paymentReferenceId;
@JsonProperty("user_validation_data")
private UserValidationData userValidationData;
@JsonProperty("product_items")
private List<ProductItem> productItems;
@JsonProperty("metadata")
private Map<String, String> metadata;
protected OrderDomainEntity() {
(3)
super(OrderDomainEntity.class);
}
(5)
@Getter
@Setter
@NoArgsConstructor
public static class ProductItem extends MissingPropertyCollector {
@JsonProperty("product_id")
private String productId;
@JsonProperty("quantity")
private int quantity;
@JsonProperty("price")
private double price;
}
@Getter
@Setter
@NoArgsConstructor
public static class UserValidationData extends MissingPropertyCollector {
@JsonProperty("is_user_validated")
private boolean userValidated;
@JsonProperty("validation_note")
private String validationNote;
}
}
| 1 | @SagaDomainEntity: Configures the domain entity metadata:
|
| 2 | Inheritance: The custom class must extend DomainEntity to inherit core saga identification and lifecycle capabilities. |
| 3 | Constructor: A protected no-argument constructor that calls super(OrderDomainEntity.class). Because the framework instantiates domain entities dynamically during deserialization and state restoration, parameterized constructors should not be declared. |
| 4 | Field Mapping: Declare attributes required to carry transaction state. Using @JsonProperty annotations is strongly recommended to guarantee deterministic JSON serialization and avoid naming discrepancies during schema evolution. |
| 5 | Nested Types & Missing Property Collection: For complex nested objects, declare static inner classes (or separate classes) extending org.stacksaga.MissingPropertyCollector. The MissingPropertyCollector base class automatically captures unrecognized properties during deserialization, ensuring seamless backward and forward compatibility during schema upgrades. |
|
Refactoring Safety: The framework identifies the domain entity using the logical |
|
Spring Bean Notice: In StackSaga, custom domain entities are lightweight state carriers—they are not Spring beans.
They do not need to be within the application’s component scan package. Instead, register the package containing your domain entities using the |
Further Configurations
Furthermore, you can provide some additional configurations for the custom domain-entity by using the attributes of the @SagaDomainEntity annotation.
Custom Mapper Provider for Domain-Entity
By default, stacksaga uses the default ObjectMapper that spring boot provides via DefaultDomainEntityMapperProvider.
in case if you want to customize the ObjectMapper for your target domain-entity, you can create and provide a custom objectMapper object for the target Domain-Entity as a custom implementation of AbstractDomainEntityMapperProvider.
It can be created any number of custom mapper providers for different domain-entities as needed as below.
@Component (1)
public class OrderDomainEntityMapperProvider extends AbstractDomainEntityMapperProvider { (2)
@Override (3)
protected ObjectMapper provide() {
return new ObjectMapper(); (4)
}
}
//-------------------------------------------------------------------------------
@Getter
@Setter
@SagaDomainEntity(
version = @SagaDomainEntityVersion(major = 1, minor = 0, patch = 0),
name = "OrderDomainEntity",
mapper = OrderDomainEntityMapperProvider.class (5)
)
public class OrderDomainEntity extends DomainEntity {
//...
}
| 1 | @Component: Mark your custom object mapper implementation as a Spring bean. | ||
| 2 | Extend class by AbstractDomainEntityMapperProvider abstract class.
|
||
| 3 | Override the method for providing the custom ObjectMapper object. |
||
| 4 | return the customized ObjectMapper object. |
||
| 5 | mapper: provide your custom domain entity mapper provider class in the DomainEntity class. |
Custom Key Generator Provider for Domain-Entity
The key generator is responsible for generating the transaction key prefix and the idempotency keys for each span.
By default, StackSaga uses DefaultDomainEntityKeyGenerator as the key generator for all domain-entities.
If you want to customize key generation for a specific domain-entity, you can create a custom implementation by extending AbstractDomainEntityKeyGenerator.
You can create separate custom key generator providers for different domain-entities as needed.
AbstractDomainEntityKeyGenerator provides two methods with default implementations that you can override:
-
generateTransactionKey- generates the transaction key prefix used bySagaUUID. -
generateIdempotencyKey- generates an idempotency key for each span of the transaction.
Transaction Key Generation
Every saga transaction requires a globally unique identifier represented by SagaUUID.
A SagaUUID consists of two parts separated by a hyphen:
-
<prefix>-<UUIDv7>-
Prefix: Generated by
generateTransactionKey(…)inAbstractDomainEntityKeyGenerator. -
Suffix: A time-based unique UUID generated automatically by the framework using the java-uuid-generator library.
-
Because the framework automatically appends a high-entropy, time-based UUID suffix, uniqueness across transactions and distributed nodes is guaranteed. The prefix serves as a concise, human-readable qualifier for observability, tracing, and domain segmentation.
|
Why UUIDv7 for Transaction Identifiers? UUIDv7 embeds a high-precision epoch timestamp in its leading 48 bits, producing chronologically monotonic identifiers. This design provides critical architectural advantages for event sourcing and transactional storage:
|
Default Behavior (first4)
Providing a custom key generator is optional.
If you do not provide a custom key generator, StackSaga uses DefaultDomainEntityKeyGenerator.
By default, it extracts the first 4 characters (first4) of the domain-entity name defined in the @SagaDomainEntity(name = "…") annotation and converts them to lowercase.
For example, with @SagaDomainEntity(name = "OrderDomainEntity"), the first 4 characters ("Orde") are converted to lowercase ("orde"), resulting in transaction IDs like:
-
orde-0195655a-350f-786d-96eb-63c1dfc6e9ba -
orde-0195655a-4e20-7a1b-80c2-1249fae53412
Prefix Constraints & Lowercase Formatting
When customizing the prefix via generateTransactionKey(…):
-
Lowercase Requirement: Developers should provide the prefix string in lowercase. If not provided in lowercase, the framework automatically converts it to lowercase internally.
-
Allowed Characters: Alphanumeric characters and hyphens only (
[a-zA-Z0-9-]). -
Disallowed: Spaces, whitespace, and special characters (such as
_,:,#,@,.,/,%) are strictly prohibited and will cause the framework to reject the transaction ID.
A custom prefix can be used to:
-
incorporate service name or domain namespace,
-
embed region or cluster identifiers for multi-region routing and log filtering,
-
satisfy organizational compliance or observability conventions.
| To see how to supply a custom prefix in code, see the Custom Key Generator Example below. |
Generating Idempotency Keys for Saga Spans
| If you are new to the concept of idempotency, refer to Idempotency & Atomic Transactions For LRT first. |
Each span in a saga transaction (i.e., each atomic execution attempt) requires an idempotency key to ensure safe, duplicate-proof retries.
The generateIdempotencyKey method receives runtime execution context divided into two input objects: SafeIdempotentInput and UnSafeIdempotentInput.
|
Do not use properties from Always derive idempotency keys exclusively from |
The framework’s default implementation produces a fixed-length MD5 hash by concatenating the transactionId, currentExecutor, and executionMode from SafeIdempotentInput and hashing the composite string via the provided HashGenerator. This ensures compact, collision-resistant idempotency keys.
Custom Key Generator Example
Here is an example of how to override generateTransactionKey and generateIdempotencyKey to provide custom logic and configure it in your custom DomainEntity class.
Because AbstractDomainEntityKeyGenerator provides default implementations for all methods, you only need to override the specific methods you want to customize.
|
@Component (1)
public class OrderDomainEntityKeyGenerator extends AbstractDomainEntityKeyGenerator { (2)
@Override (3)
// This method is called when each transaction is initialized to supply the prefix
public String generateTransactionKey(String serviceName, String applicationVersion, String instanceId, String region, String zone, SagaDomainEntity sagaDomainEntity) {
// Custom prefix should be lowercase (alphanumeric and hyphens only)
return String.format("%s-%s", serviceName.toLowerCase(), region.toLowerCase());
}
@Override (4)
public String generateIdempotencyKey(SafeIdempotentInput safeIdempotentInput, UnSafeIdempotentInput unSafeIdempotentInput) {
final String rowKey = new StringJoiner(":")
.add(safeIdempotentInput.transactionId())
.add(safeIdempotentInput.currentExecutionName())
.add(safeIdempotentInput.executionMode().name().toLowerCase())
.toString();
return this.hashGenerator.generateHash(rowKey, HashGenerator.ALGType.MD5);
}
}
//: Configure The custom KeyGen With Custom DomainEntity
@Getter
@Setter
@SagaDomainEntity(
version = @SagaDomainEntityVersion(major = 1, minor = 0, patch = 0),
name = "OrderDomainEntity",
mapper = OrderDomainEntityMapperProvider.class,
keyGen = OrderDomainEntityKeyGenerator.class (5)
)
public class OrderDomainEntity extends DomainEntity {
//...
}
| 1 | @Component: Mark your custom key generator implementation as a Spring bean. |
| 2 | Extend the custom class by AbstractDomainEntityKeyGenerator. |
| 3 | Override the generateTransactionKey method to create your custom prefix for the transaction ID (SagaUUID). Developers should provide the prefix in lowercase; if uppercase characters are provided, the framework converts them to lowercase internally.The method is called when each transaction is initialized. |
| 4 | Override the generateIdempotencyKey method and create your custom idempotency key for each span.The method is called before each span execution is invoked. |
| 5 | Provide your custom class as keyGen of @SagaDomainEntity in your DomainEntity class. |
it is highly recommended to use the provided hashGenerator to produce a fixed-length hash of a composite string for generating the idempotency key, rather than returning a raw concatenation of input values. this approach ensures that the idempotency key is compact, consistent in length, and has a low risk of collisions, even when the input values are long or contain variable content.
|
Register the implementation as a Spring bean (e.g., @Component) and ensure it is stateless and thread-safe.
The framework may invoke it concurrently.
|
Domain-Entity Versioning
Domain-Entity versioning is the most important thing in StackSaga.
All the applications are being updated with new features time to time.
Any kind of changes that you make regarding the entire transaction, it caused for a version update of the particular Domain-Entity.
It can be adding new fields, removing fields, changing the data type of the existing fields, etc.
all those changes are considered as the version update of the Domain-Entity.
and it is important to update the version of the Domain-Entity by using the @SagaDomainEntityVersion annotation whenever you make any kind of changes in the Domain-Entity.
and also it is important to maintain the backward compatibility of the Domain-Entity when you make any kind of changes in it. because there might be some transactions that are still running with the old version of the Domain-Entity, and if you make any breaking changes in the Domain-Entity without maintaining the backward compatibility, it can cause disruption in those transactions.
Creating a Custom Domain-Entity
To define a custom Domain-Entity, create a class that extends the DomainEntity base class provided by the framework and annotate it with @SagaDomainEntity.
You can declare any fields required to maintain state across the saga workflow (such as username, totalAmount, productItems, etc.). When starting a transaction, pass an initialized instance of this custom class to StackSagaKafkaTemplate.init(…).startWith(…).execute().
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.stacksaga.DomainEntity;
import org.stacksaga.MissingPropertyCollector;
import org.stacksaga.annotation.SagaDomainEntity;
import org.stacksaga.annotation.SagaDomainEntityVersion;
@Getter
@Setter
(1)
@SagaDomainEntity(
version = @SagaDomainEntityVersion(major = 1, minor = 0, patch = 0),
name = "OrderDomainEntity"
)
public class OrderDomainEntity extends DomainEntity { (2)
(4)
@JsonProperty("username")
private String username;
@JsonProperty("order_id")
private String orderId;
@JsonProperty("total_amount")
private double totalAmount;
@JsonProperty("payment_reference_id")
private String paymentReferenceId;
@JsonProperty("user_validation_data")
private UserValidationData userValidationData;
@JsonProperty("product_items")
private List<ProductItem> productItems;
@JsonProperty("metadata")
private Map<String, String> metadata;
protected OrderDomainEntity() {
(3)
super(OrderDomainEntity.class);
}
(5)
@Getter
@Setter
@NoArgsConstructor
public static class ProductItem extends MissingPropertyCollector {
@JsonProperty("product_id")
private String productId;
@JsonProperty("quantity")
private int quantity;
@JsonProperty("price")
private double price;
}
@Getter
@Setter
@NoArgsConstructor
public static class UserValidationData extends MissingPropertyCollector {
@JsonProperty("is_user_validated")
private boolean userValidated;
@JsonProperty("validation_note")
private String validationNote;
}
}
| 1 | @SagaDomainEntity: Configures the domain entity metadata:
|
| 2 | Inheritance: The custom class must extend DomainEntity to inherit core saga identification and lifecycle capabilities. |
| 3 | Constructor: A protected no-argument constructor that calls super(OrderDomainEntity.class). Because the framework instantiates domain entities dynamically during deserialization and state restoration, parameterized constructors should not be declared. |
| 4 | Field Mapping: Declare attributes required to carry transaction state. Using @JsonProperty annotations is strongly recommended to guarantee deterministic JSON serialization and avoid naming discrepancies during schema evolution. |
| 5 | Nested Types & Missing Property Collection: For complex nested objects, declare static inner classes (or separate classes) extending org.stacksaga.MissingPropertyCollector. The MissingPropertyCollector base class automatically captures unrecognized properties during deserialization, ensuring seamless backward and forward compatibility during schema upgrades. |
|
Refactoring Safety: The framework identifies the domain entity using the logical |
|
Spring Bean Notice: In StackSaga, custom domain entities are lightweight state carriers—they are not Spring beans.
They do not need to be within the application’s component scan package. Instead, register the package containing your domain entities using the |
Further Configurations
Furthermore, you can provide some additional configurations for the custom domain-entity by using the attributes of the @SagaDomainEntity annotation.
Custom Mapper Provider for Domain-Entity
By default, stacksaga uses the default ObjectMapper that spring boot provides via DefaultDomainEntityMapperProvider.
in case if you want to customize the ObjectMapper for your target domain-entity, you can create and provide a custom objectMapper object for the target Domain-Entity as a custom implementation of AbstractDomainEntityMapperProvider.
It can be created any number of custom mapper providers for different domain-entities as needed as below.
@Component (1)
public class OrderDomainEntityMapperProvider extends AbstractDomainEntityMapperProvider { (2)
@Override (3)
protected ObjectMapper provide() {
return new ObjectMapper(); (4)
}
}
//-------------------------------------------------------------------------------
@Getter
@Setter
@SagaDomainEntity(
version = @SagaDomainEntityVersion(major = 1, minor = 0, patch = 0),
name = "OrderDomainEntity",
mapper = OrderDomainEntityMapperProvider.class (5)
)
public class OrderDomainEntity extends DomainEntity {
//...
}
| 1 | @Component: Mark your custom object mapper implementation as a Spring bean. | ||
| 2 | Extend class by AbstractDomainEntityMapperProvider abstract class.
|
||
| 3 | Override the method for providing the custom ObjectMapper object. |
||
| 4 | return the customized ObjectMapper object. |
||
| 5 | mapper: provide your custom domain entity mapper provider class in the DomainEntity class. |
Custom Key Generator Provider for Domain-Entity
The key generator is responsible for generating the transaction key prefix and the idempotency keys for each span.
By default, StackSaga uses DefaultDomainEntityKeyGenerator as the key generator for all domain-entities.
If you want to customize key generation for a specific domain-entity, you can create a custom implementation by extending AbstractDomainEntityKeyGenerator.
You can create separate custom key generator providers for different domain-entities as needed.
AbstractDomainEntityKeyGenerator provides two methods with default implementations that you can override:
-
generateTransactionKey- generates the transaction key prefix used bySagaUUID. -
generateIdempotencyKey- generates an idempotency key for each span of the transaction.
Transaction Key Generation
Every saga transaction requires a globally unique identifier represented by SagaUUID.
A SagaUUID consists of two parts separated by a hyphen:
-
<prefix>-<UUIDv7>-
Prefix: Generated by
generateTransactionKey(…)inAbstractDomainEntityKeyGenerator. -
Suffix: A time-based unique UUID generated automatically by the framework using the java-uuid-generator library.
-
Because the framework automatically appends a high-entropy, time-based UUID suffix, uniqueness across transactions and distributed nodes is guaranteed. The prefix serves as a concise, human-readable qualifier for observability, tracing, and domain segmentation.
|
Why UUIDv7 for Transaction Identifiers? UUIDv7 embeds a high-precision epoch timestamp in its leading 48 bits, producing chronologically monotonic identifiers. This design provides critical architectural advantages for event sourcing and transactional storage:
|
Default Behavior (first4)
Providing a custom key generator is optional.
If you do not provide a custom key generator, StackSaga uses DefaultDomainEntityKeyGenerator.
By default, it extracts the first 4 characters (first4) of the domain-entity name defined in the @SagaDomainEntity(name = "…") annotation and converts them to lowercase.
For example, with @SagaDomainEntity(name = "OrderDomainEntity"), the first 4 characters ("Orde") are converted to lowercase ("orde"), resulting in transaction IDs like:
-
orde-0195655a-350f-786d-96eb-63c1dfc6e9ba -
orde-0195655a-4e20-7a1b-80c2-1249fae53412
Prefix Constraints & Lowercase Formatting
When customizing the prefix via generateTransactionKey(…):
-
Lowercase Requirement: Developers should provide the prefix string in lowercase. If not provided in lowercase, the framework automatically converts it to lowercase internally.
-
Allowed Characters: Alphanumeric characters and hyphens only (
[a-zA-Z0-9-]). -
Disallowed: Spaces, whitespace, and special characters (such as
_,:,#,@,.,/,%) are strictly prohibited and will cause the framework to reject the transaction ID.
A custom prefix can be used to:
-
incorporate service name or domain namespace,
-
embed region or cluster identifiers for multi-region routing and log filtering,
-
satisfy organizational compliance or observability conventions.
| To see how to supply a custom prefix in code, see the Custom Key Generator Example below. |
Generating Idempotency Keys for Saga Spans
| If you are new to the concept of idempotency, refer to Idempotency & Atomic Transactions For LRT first. |
Each span in a saga transaction (i.e., each atomic execution attempt) requires an idempotency key to ensure safe, duplicate-proof retries.
The generateIdempotencyKey method receives runtime execution context divided into two input objects: SafeIdempotentInput and UnSafeIdempotentInput.
|
Do not use properties from Always derive idempotency keys exclusively from |
The framework’s default implementation produces a fixed-length MD5 hash by concatenating the transactionId, currentExecutor, and executionMode from SafeIdempotentInput and hashing the composite string via the provided HashGenerator. This ensures compact, collision-resistant idempotency keys.
Custom Key Generator Example
Here is an example of how to override generateTransactionKey and generateIdempotencyKey to provide custom logic and configure it in your custom DomainEntity class.
Because AbstractDomainEntityKeyGenerator provides default implementations for all methods, you only need to override the specific methods you want to customize.
|
@Component (1)
public class OrderDomainEntityKeyGenerator extends AbstractDomainEntityKeyGenerator { (2)
@Override (3)
// This method is called when each transaction is initialized to supply the prefix
public String generateTransactionKey(String serviceName, String applicationVersion, String instanceId, String region, String zone, SagaDomainEntity sagaDomainEntity) {
// Custom prefix should be lowercase (alphanumeric and hyphens only)
return String.format("%s-%s", serviceName.toLowerCase(), region.toLowerCase());
}
@Override (4)
public String generateIdempotencyKey(SafeIdempotentInput safeIdempotentInput, UnSafeIdempotentInput unSafeIdempotentInput) {
final String rowKey = new StringJoiner(":")
.add(safeIdempotentInput.transactionId())
.add(safeIdempotentInput.currentExecutionName())
.add(safeIdempotentInput.executionMode().name().toLowerCase())
.toString();
return this.hashGenerator.generateHash(rowKey, HashGenerator.ALGType.MD5);
}
}
//: Configure The custom KeyGen With Custom DomainEntity
@Getter
@Setter
@SagaDomainEntity(
version = @SagaDomainEntityVersion(major = 1, minor = 0, patch = 0),
name = "OrderDomainEntity",
mapper = OrderDomainEntityMapperProvider.class,
keyGen = OrderDomainEntityKeyGenerator.class (5)
)
public class OrderDomainEntity extends DomainEntity {
//...
}
| 1 | @Component: Mark your custom key generator implementation as a Spring bean. |
| 2 | Extend the custom class by AbstractDomainEntityKeyGenerator. |
| 3 | Override the generateTransactionKey method to create your custom prefix for the transaction ID (SagaUUID). Developers should provide the prefix in lowercase; if uppercase characters are provided, the framework converts them to lowercase internally.The method is called when each transaction is initialized. |
| 4 | Override the generateIdempotencyKey method and create your custom idempotency key for each span.The method is called before each span execution is invoked. |
| 5 | Provide your custom class as keyGen of @SagaDomainEntity in your DomainEntity class. |
it is highly recommended to use the provided hashGenerator to produce a fixed-length hash of a composite string for generating the idempotency key, rather than returning a raw concatenation of input values. this approach ensures that the idempotency key is compact, consistent in length, and has a low risk of collisions, even when the input values are long or contain variable content.
|
Register the implementation as a Spring bean (e.g., @Component) and ensure it is stateless and thread-safe.
The framework may invoke it concurrently.
|
Domain-Entity Versioning
Domain-Entity versioning is the most important thing in StackSaga.
All the applications are being updated with new features time to time.
Any kind of changes that you make regarding the entire transaction, it caused for a version update of the particular Domain-Entity.
It can be adding new fields, removing fields, changing the data type of the existing fields, etc.
all those changes are considered as the version update of the Domain-Entity.
and it is important to update the version of the Domain-Entity by using the @SagaDomainEntityVersion annotation whenever you make any kind of changes in the Domain-Entity.
and also it is important to maintain the backward compatibility of the Domain-Entity when you make any kind of changes in it. because there might be some transactions that are still running with the old version of the Domain-Entity, and if you make any breaking changes in the Domain-Entity without maintaining the backward compatibility, it can cause disruption in those transactions.