Implement Saga Pattern (Java)

Note:

The Saga pattern is only available for Domain Services (Java) based on Java Spring Boot Stack 2.0

Setup local profile

Note:

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: ""
Note:

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;
  }
}