Implement Saga Pattern
Setup local profile
To set up your local profile, please check the related chapter Saga configurations
Implement Saga Orchestrator
A Saga Orchestrator manages a chain of transactions modeled with the help of participants. For each domain service marked as Saga orchestrator, an implementation stub is generated under "src/main/java/yourProject/domain/yourDomain/service/YourOrchestrator.java"
afterSaga
will be triggered after all Participants of this service are executed
provides access to the input of the Orchestrator if it was modelled
defines the output of the Orchestrator if it was modelled
will not be triggered if one of the Participants failed with an Exception
onComplete
covers extra logic what to do on successful completion
only exists if On Complete Method was selected while modelling
will be called automatically by the lra-coordinator when the Saga completes successfully (asynchronously)
if this logic fails, the lra-coordinator will keep triggering it
onCompensate
covers extra compensation logic for the whole saga
only exists if On Compensate Method was selected while modelling
will be called automatically by the lra-coordinator when the Saga fails (asynchronously)
if this logic fails, the lra-coordinator will keep triggering it
Example:
// Example of orchestrator implementation
@Override
protected CreditCards afterSaga(CreditCard input) {
log.info("Orchestrator01.afterSaga()");
this.repo.getCc().getCard().insert(input);
return this.repo.getCc().getCard().findAll();
}
@Override
public void onComplete(SagaContext context) {
log.info("Orchestrator01.complete() for lra {}", context.getLraId());
}
@Override
public void onCompensate(SagaContext context) {
log.info("Orchestrator01.compensate() for lra {}", context.getLraId());
}
In order to execute the Orchestrator, you have to trigger the execute method, e.g. from an API operation and set the input if needed:
@Autowired
Orchestrator01 orchestrator;
@Override
public ResponseEntity<Void> executeSagaOrchestrator(String id) {
// create input for Saga
GetByIdInput input = new GetByIdInputBuilder() //
.setExternalID(id) //
.build();
// trigger Saga execution
orchestrator.execute(input);
return new ResponseEntity<Void>(HttpStatusCode.valueOf(202));
}
Implement Saga participant
For each Domain Service marked as Saga Participant, an implementation stub is generated under "src/main/java/yourProject/domain/yourDomain/service/YourParticipant.java" which comes with three methods:
execute
covers execution logic of the Participant
onComplete
covers extra logic what to do on successful completion of the Saga
only exists if On Complete Method was selected while modelling
will be called automatically by the lra-coordinator when the Saga completes successfully (asynchronously)
if this logic fails, the lra-coordinator will keep triggering it
onCompensate
covers compensation logic (e.g. undoing what happened in the execute) for the participant
only exists if On Compensate Method was selected while modelling
will be called automatically by the lra-coordinator when the Saga fails (asynchronously)
if this logic fails, the lra-coordinator will keep triggering it
Example:
@Service("sagadomainspace_Partcipant")
public class Partcipant extends PartcipantBase {
private static final Logger log = LoggerFactory.getLogger(Partcipant.class);
public Partcipant(DomainEntityBuilder entityBuilder, Repository repo) {
super(entityBuilder, repo);
}
// Example of participant implementation
@Override
public Balance execute(SagaContext context, CreditCard creditCard) throws CreditCardNotFoundError {
log.info("Participant.execute()");
// Use repository to get card instance
Optional<Card> cardRootEntity = this.repo.getCc().getCard().findById(creditCard.getId());
if(cardRootEntity.isPresent()) {
// Use Domain Entity Builder from base class to create an instance of Balance entity
Balance balance = this.entityBuilder.getCc().getBalance().build();
balance.setAvaliableBalance(cardRootEntity.getBalance());
balance.setHoldAmount(cardRootEntity.getHoldAmount());
return balance;
} else {
String errorMessage = String.format("Credit card with id %s not found", creditCard.getId());
throw new CreditCardNotFoundError(errorMessage);
}
}
The LRA id can be accessed from any Saga participant service using the provided SagaContext:
@Override
public void onComplete(SagaContext context) {
// how LRA can be accessed from context
log.info("Participant.onComplete() for lra {}", context.getLraId());
}
@Override
public void onCompensate(SagaContext context) {
log.info("Participant.onCompensate()");
}
Implement Saga across multiple services
To share a Saga context across multiple services, it is helpful to use the Saga API Header, which marks the API operation as a Saga participant. To do that, the Saga Pattern Participant flag needs to be set in the operation while modelling.
To establish a cross-service Saga use case, the API operation is meant to again trigger the execution of an Orchestrator, which will be automatically executed in the proper Saga context.
Marking the API operation as Saga Pattern Participant will have the following effects:
A Long-Running-Action (LRA) header will be added automatically to the operation headers
Any Orchestrator triggered from this API Operation will automatically apply the Saga context from the header
The LRA header value can be accessed from the NativeWebRequest in the API Operation
Example:
Orchestrator1 orchestrator;
@Override
public ResponseEntity<Void> apiOperation() {
// trigger Saga execution --> saga context is automatically applied
orchestrator.execute();
// optional: get saga header from request
Optional<NativeWebRequest> nativeWebRequest = getRequest();
nativeWebRequest.ifPresent((request) -> {
request.getHeader(SagaConstants.Saga_LONG_RUNNING_ACTION);
});
return ResponseEntity.ok(null);
}
When invoking a call against an API operation with a LRA header, the value will be automatically read from the current Saga context and used as header value
If needed, the header value can be overwritten with a custom LRA header value
Example:
@Override
public void execute(SagaContext context) {
log.info("Integration service.execute()");
HttpHeaders headerParams = new HttpHeaders();
// overwrite saga header with a custom header (usually not needed as it is automatically filled from the current saga context)
headerParams.add(SagaConstants.Saga_LONG_RUNNING_ACTION, "CustomLRAHeaderValue");
integrationService.integrationServiceOperation(headerParams);
}
Overwrite CamelConfiguration
In some use cases it might be necessary to apply a custom modification to your camel configuration (for example to use a different LRASagaService). To do so please create a new class with relevant name and add @Configuration annotation to the added class. Then, please define a CamelSagaService bean with your custom modifications. Please see the following example:
@Configuration
public class CustomCamelConfiguration {
@Bean
public CamelSagaService camelSagaService(
CamelContext cc,
LraServiceConfiguration configuration,
SagaContextHolder sagaContextHolder,
Environment env
) throws Exception {
LRASagaService service;
if (FeatureFlagUtils.isActiveFeature("feature.secure-narayana.enabled", env)) {
service = new AuthenticatingLRASagaService(sagaContextHolder);
} else {
service = new LRASagaService();
}
service.setCoordinatorUrl(configuration.getCoordinatorUrl());
service.setCoordinatorContextPath(configuration.getCoordinatorContextPath());
service.setLocalParticipantUrl(configuration.getLocalParticipantUrl());
service.setLocalParticipantContextPath(configuration.getLocalParticipantContextPath());
cc.addService(service);
return service;
}
}