Description
After our discussion today I took a look at the different articles discussing Saga:
- Saga Pattern | Application transactions using Microservices – Part I
- Saga Pattern | How to implement business transactions using Microservices – Part II
- Managing data consistency in a microservice architecture using Sagas part 2 - coordinating sagas
Below I've taken some notes on how this would work for the payment
service (which should be extendable to the order
service`.
Saga notes
- Choreagraphy approach sends events directly to other services that are subscribed.
- Since each service is a set of replicas we need a place to store all the services that are subscribed to us.
- This could be cassandra or postgres or some other service (redis?)
- Upon an event (say creating the order) we emit this event (through http) to all that are subscribed given some details (like the transaction id)
- The choreagraphy approach (pushing messages to the services) is preferable as in this case we are sure only one replica answers to an event. K8s loadbalancer will make sure that this replica is healthy.
- These events trigger actions on the subscribed services.
- For example triggering a reservation
- They could trigger another event
- Upon success the next event will trigger the next step in the process
- Upon failure all services that performed some action can run the appropriate code to roll back the actions made.
- What happens when the next event doesn't arrive? In case a service actually failed.
- Make sure that sending an event waits for a 2xx code or reports failure on timeout.
- What happens when the next event doesn't arrive? In case a service actually failed.
- This means we should clearly document the events and when they happen in the process. As the amount of events can become quite large.
- The main benefit of this is that we don't need to implement all rollback logic on a single service and that we don't need to store any state in the services themselves.
- It also provides a clear distinction from the public api for actual behaviour that we want our clients to use while providing an internal api for communication between services to handle reservations or failures.
- Since each service is a set of replicas we need a place to store all the services that are subscribed to us.
Example
The payment service needs to reserve stock and credits before subtracting them and completing the order.
Payment
receives the payment requestPayment
creates a payment entry with statusINITIATED
and creates an eventPAYMENT_INITIATED
with the order id and a transaction id (uuid4?) (not sure if we need the transaction id)User
receives eventPAYMENT_INITIATED
and reserves credits for the transaction, emitsCREDITS_RESERVED
event for the same transaction id.Stock
receives eventCREDITS_RESERVED
and reserves the stock for the transaction, emits theSTOCK_RESERVED
event for the same transaction id.Payment
receives eventSTOCK_RESERVED
and changes the payment status toRESERVED
. EmitsPAYMENT_RESERVED
event.User
receivesPAYMENT_RESERVED
and applies the reservation for the transaction, emitsCREDITS_SUBTRACTED
for the transaction.Stock
receives eventCREDITS_SUBTRACTED
and applies the stock reservation. EmitsSTOCK_SUBTRACTED
for the transaction.- At this point the payment is complete.
Payment
receives the eventSTOCK_SUBTRACTED
and updates the status of the payment toPAID
.
At any point there are also failed responses. For example:
User
Failure
sends theINSUFFICIENT_CREDITS
which is received by the payment service and stops the transaction.Stock
Failure
sends theINSUFFICIENT_STOCK
which is received by the user service (who cancels their reservation) and payment service (who returns failure on the transaction).User
Failure
Not sure how this could happen, but in case both reservations should be removed using aFAILURE
event for the transaction.Stock
Failure
Not sure how this could happen, but in case the user service receives the event and credits the payment back using aFAILURE
event for the transaction.Payment
Failure
Again not sure how this could happen, but in case we return the stock and credit using aFAILURE
event for the transaction.
The only problem I see here is when an event is emitted but not responded to (2xx status code). The transaction will forever halt. This should be solvable using some sort of deliver-at-least once logic that waits for a 200 status code -> which could break if the node fails or is replaced -> This could be avoided by using a message broker that is highly available.
The only logic that is required on this message broker is to be highly available, send messages through their channels and wait for a response. If no response is given or an error we send a general FAILURE
event for that transaction which should roll back the actions on other systems. This should make it so that unless a machine actually shuts down unexpectedly the system should stay consistent.
The original request
An additional problem I see here is that because of this chain of messages we will need to keep the original request from the user to the payment service open until we either receive a failure or STOCK_SUBTRACTED
event.
In case of a failure of the payment service within this time we will not be able to let the user know the payment failed or succeeded.