Software project: Creating a real-life transactional microservices application on Kubernetes

Mika Rinne, Oracle EMEA
4 min readJan 18, 2024

--

Part 4: Writing the code to create the LRA

In Part 3 I did the LRA coodinator setup for the Gymapp Helidon microservices and discussed about the use cases. Now let’s cover some code to create a LRA in the Gymapp microservices.

For the use case of Gymuser attending a gym class I implemented the REST API to list the available classes by Gyminstructors using database schema that was described in Part 2.

In Helidon accessing database using JPA is easy. First I have created the Instructorclass.java with fields matching the DB table, then added the fields and setters and getters and finally added the NamedQueries to be used in the Gyminstructor microservice for the INSTRUCTORCLASS table:

@Entity(name = "Instructorclass")
@Table(name = "INSTRUCTORCLASS")
@Access(AccessType.PROPERTY)
@NamedQueries({
@NamedQuery(name = "getInstructorclasses",
query = "SELECT c FROM Instructorclass c order by c.id"),
@NamedQuery(name = "getInstructorclassesByInstructor",
query = "SELECT c FROM Instructorclass c WHERE c.instructorEmail = :instructor order by c.id")
})

The REST API (http GET) in InstructorclassResource.java using the NamedQuery getInstructorclasses to list the available classes by Gyminstructors is simply:

@GET
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
public List<Instructorclass> getInstructorclasses() {
return entityManager.createNamedQuery("getInstructorclasses", Instructorclass.class).getResultList();
}

Now using this REST API the Gymuser can list available classes and attend to one using it’s id. Here is where the LRA comes into play.

In case of the REST call failing between microservices the LRA coordinator orders the transaction participating microservices (via LRA compensate) to refund the money. Hence, the system works properly even in case of problems microservices being loosely coupled in this way.

Looking at the Gymapp database schema we can see that the primary key for both Gymuser and Gyminstructor is the email of the user. We will tie this to securing the Gymapp later on with JWT that Helidon has support for. For now let’s just assume the JWT data is available in the REST call as auth param.

Here is the Gymuser microservice attend REST API (http POST) in MyclassResource.java that is called when Gymuser does a successful payment to PayPal® with the payment JSON data to signup to a class:

@POST
@Path("/attend/{instructorclassid}")
@LRA(value = LRA.Type.REQUIRES_NEW, end = false, cancelOn = Response.Status.INTERNAL_SERVER_ERROR)
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Transactional(Transactional.TxType.REQUIRED)
public Response attendClass(@HeaderParam("Authorization") String auth,
@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@PathParam("instructorclassid") Long instructorClassId,
String paymentData) {
String userEmail = getEmail(auth);
Myclass myclass = new Myclass();
myclass.setUserEmail(userEmail);
myclass.setLra(lraId.toString());
myclass.setPayment(paymentData);
entityManager.persist(myclass);
try {
PayPalCaptureResponseData payPalCaptureResponseData = objectMapper.readValue(paymentData, PayPalCaptureResponseData.class);
myclass.setName(payPalCaptureResponseData.name);
myclass.setPrice(Double.valueOf(payPalCaptureResponseData.price));
// Let's do a signup to Gyminstructor with the class ID
Response signUpResponse = ClientBuilder.newClient()
.target(InstructorServiceURL)
.path("signups/" + instructorClassId)
.request()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + auth)
.post(null);
if(signUpResponse.getStatus() == Response.Status.OK.getStatusCode())
{
String json = signUpResponse.readEntity(String.class);
Instructorclass instructorclass = objectMapper.readValue(json, Instructorclass.class);
myclass.setInstructorEmail(instructorclass.getInstructorEmail());
myclass.setInfo(instructorclass.getInfo());
myclass.setStatus(instructorclass.getStatus());
entityManager.persist(myclass);
return Response.ok(myclass).build();
} else {
myclass.setInfo("Sign Up error, cancelled");
myclass.setStatus(11);
entityManager.persist(myclass);
return Response.serverError().build();
}
} catch (Exception e)
{
e.printStackTrace();
myclass.setInfo("Sign Up error, cancelled");
myclass.setStatus(11);
entityManager.persist(myclass);
return Response.serverError().build();
}
}

What happens here is that when entering the API the LRA is created (value = LRA.Type.REQUIRES_NEW) and it is kept open (end = false) after the API call ends with the following LRA annotation of the method above:

@LRA(value = LRA.Type.REQUIRES_NEW, end = false, cancelOn = Response.Status.INTERNAL_SERVER_ERROR)

It is also important to notice the annotation’s last part cancelOn = Response.Status.INTERNAL_SERVER_ERROR that marks the LRA cancelled whenever there is a failure in the REST API call. This makes the LRA coordinator to do a compensation for the LRA with the parties involved, the Gymuser and the Gyminstructor.

Such may happen if Gymuser cannot reach Gyminstructor microservice signup method or the call fails for some other reason, e.g. microservice can be down due to an error or the sign up for the same class has already been done. In this case the LRA coordinator asks Gymuser (via LRA compensate) to refund the money to PayPal® and Gymuser’s money is not lost. Hence, the system works properly microservices being loosely coupled in this way.

If all goes well the Gymuser will save a new Myclass with Instructorclass details into it’s database and the called Gyminstructor signup REST API will create a matching ClassAttendance in ClassAttendanceResource.java to it’s database both having the same LRA being participants of the same transaction:

@POST
@Path("/{instructorclassid}")
@LRA(value = LRA.Type.MANDATORY, end = false)
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Transactional(Transactional.TxType.REQUIRED)
public Response signUpByInstructorclassid(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) URI lraId,
@HeaderParam("Authorization") String auth,
@PathParam("instructorclassid") Long instructorclassid) {
try {
String userEmail = getEmail(auth);
if(userEmail == null)
{
return Response.serverError().build();
}
if(isSignedUpByInstructorclassid(auth, instructorclassid)) {
System.out.println(userEmail + " is already signed up with id " + instructorclassid + ", user :" + userEmail);
return Response.serverError().build();
}
ClassAttendance classAttendance = new ClassAttendance();
classAttendance.setUserEmail(userEmail);
classAttendance.setInstructorclassid(instructorclassid);
classAttendance.setStatus(2);
classAttendance.setLra(lraId.toString());
entityManager.persist(classAttendance);
return Response.ok(instructorclass).build();
} catch (Exception e)
{
e.printStackTrace();
return Response.serverError().build();
}
}
Gymapp microservices loose coupling using LRA coordinator

As you can see in the code snippets above it is not necessary pass the LRA between microservices the REST API calls. The Helidon LRA propagation will take care of this automatically.

Using the LRA annotation in the Gyminstructor signup REST API the ClassAttendance participates in the LRA which is kept open and the call fails if the LRA is not in place:

@LRA(value = LRA.Type.MANDATORY, end = false)

In Part 5 let’s see what happens when either of the LRA participants, Gymuser or Gyminstructor, decide to cancel the class or Gyminstructor closes the class as held.

--

--