Once, we did something like that, for a complex financial system with many constraints on data. What follows is not about distributed transactions, it is about rolling-back mistaken operations. The approach was the following :
As the system was young and immature (the company is a FinTech), business people were hesitating when managing business-rules, special use-cases, etc... in this situation, we had many problems to integrate this uncertainty into our accounting domain. In the accounting domain, it is not possible to change the already processed business events, you modify them by appending new events, that cancel the preceding ones. We implemented a basic (async) notifications system with a basic DB persisted event-store, with correlation ids to represent the distributed transaction. All handlers were implementing the command pattern Do()/Undo().
Given a sequence of mistaken notifications, we were able to go back in time based on the database timeline, and apply the "Undo()" method for every single notification associated with the correlation id.
It was twice the job to develop, because each command needed its own "nemesis". Doing this, we had to write sufficiently ACID "Do()" methods in the handler to ensure the "Undo()" to be possible. Once, it perfectly worked for a massive malfunction in the system.