Transactional Outbox Pattern
While developing microservices, sometimes in monolithic apps as well, there is a need to save data to the database and call another service or messaging platform with some more information. What if these two operations need to be atomic? Meaning if the first call to the database is successful, the second call must be successful as well — at least eventually.
Sometimes, people write code like below to send a message to the second service:
dbClient.save(data);
var retries = 5;
while(retries !== 0) {
try{
//call another service/messagr broker
serviceClient.send(data);
break;
}catch(err){
tries -= 1;
}
}
The issue with the above approach is that your service might die after saving to the database or the external service call may not succeed even after all retries are exhausted.
One way to solve this problem, another is event sourcing, is by using a transactional outbox pattern. It is a pretty simple and useful pattern as seen below:
As seen from the diagram an extra table needs to be created for saving outgoing messages. Now in one transaction service can write data to the main table and outgoing data/message to the outbox table. As the transaction is local it is guaranteed to be atomic(provided you use relevant db transaction while saving). If NoSQL database is used as the main database then an outgoing message can be added to the record itself as generally they support document or row-level atomicity.
Once the messages/events are saved to the outbox table then the relay process can send that data to an external service or broker using normal polling or transaction log tailing(if supported by db; for example, AWS DynamoDB streams, Postgres WAL etc.). One important thing to note, however, is that the relay process may potentially send the same messages several times because a process could die at any random point in execution. To handle this receiving service must be idempotent, i.e., when the same messages arrive several times there shouldn’t be a different behaviour.
In concluding this pattern could come handy in situations where atomicity needs to be guaranteed across service calls. Personally, I’ve used this pattern with great success in several use cases. Have you used this pattern before? Please let me know in the comments.