Implement Saga Pattern (Java)
The Saga pattern is only available for Domain Services (Java) based on Java Spring Boot Stack 2.0
Setup local profile
If you want to use the Saga pattern in your project for implementing transactional use cases, the application-local.yaml
needs to have additional configurations. The Saga extension is implemented by Apache Camel which requires the below defined settings:
camel:
lra:
coordinator-url: <camel-server-url>
local-participant-url: ${k5.sdk.springboot.server.baseurl}/camel
local-participant-context-path: ""
Please consider, that the LRA coordinator might not be able to trigger the callbacks if your service is running locally
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 void 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()); } 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; } }