Say I receive two concurrent requests with the same payload. But I must (1) perform a single payment transaction (using a third-party API) and (2) somehow return the same response to both requests. It's this second requirement that complicates things. Otherwise, I could have just returned an error response to the duplicate request.
I have two entities: Session and Payment (related via @OneToOne relation). Session has two fields to keep track of the overall state: PaymentStatus (NONE, OK, ERROR), SessionStatus (CHECKED_IN, CHECKED_OUT). The initial condition is NONE and CHECKED_IN.
The request payload does contain a unique session number, which I use to get the relevant session. For now, assume that the payment service is sort of "idempotent" for a unique order id: it performs only one transaction for a given order id. The order id also comes in the request payload (the same value for the twin requests).
The flow I have in mind is along these lines:
- Get the session
- If
session.getPaymentStatus() == OK, find the payment and return success response. - Perform the payment
- Save the payment to DB.
Sessionhas a field with unique constraint generated from the request payload. So if one of the threads tries to insert a duplicate, aDataIntegrityViolationExceptionwill be thrown. I catch it, find the already inserted payment, and return a response based on it. - If no exception is thrown in 4, return the appropriate response.
In this flow, there seems to be at least one scenario where I might have to return error responses to both requests despite the fact that the payment transaction was successfully completed! For instance, say an error occurs for the "first" request, payment is not done, and an error response is returned. But for the "second" request, which happens to take a bit longer to process, payment is done, but upon insertion to DB, the already inserted payment record is discovered, and an error response is formed on the basis of it.
I'd like to avoid all these race condition-like situations. And I've a feeling that I'm missing something very obvious here. In essence, the problem is to somehow make one request to wait for another to complete. Is there a way that I can utilize DB transactions and locks to handle this smoothly?
Above I assumed that the payment service is idempotent for a given order id. What if it wasn't and I had to absolutely avoid sending duplicate requests to it?
Here's the relevant part of the service method:
Session session = sessionRepo.findById(sessionId)
.orElseThrow(SessionNotFoundException::new);
Payment payment = paymentManager.pay(session, req.getReference(), req.getAmount());
Payment saved;
try {
saved = paymentRepo.save(payment);
} catch (DataIntegrityViolationException ex) {
saved = paymentRepo.findByOrderId(req.getReference())
.orElseThrow(PaymentNotFoundException::new);
}
PaymentStatus status = saved.getSession().getPaymentStatus();
PaymentStage stage = saved.getSession().getPaymentStage();
if (stage == COMPLETION && status == OK)
return CheckOutResponse.success(req.getTerminalId(), req.getReference(),
req.getPlateNumber(), saved.getAmount(), saved.getRrn());
return CheckOutResponse.error(req.getTerminalId(), req.getReference(),
"Unable to complete transaction.");