This pattern describes how to accomplish transaction processing within the context of autonomous services. The pattern is based on the idea that - by design - every action has a compensating action to undo the original action.
The autonomous services fulfill action requests from the transaction controller (see "1" in figure). If one or more services reply not to have been able to fulfill the request (see "2" in figure, fault feedback) then the transaction controller will send requests for compensating actions to the services that did fulfill the original requests (see "3" in figure).
The concerning services do not distinguish between a normal action and a compensating action. To the services compensating actions are normal actions like any other. The services are not even aware of being part of a transaction.
Examples
- A service is designed to make, change and cancel reservations for hotel rooms. These are three equivalent types of actions. In the context of compensating actions, a cancellation can be considered a compensation for making a reservation. This compensating action results in undoing the original make reservation action.
- A service is designed to increase or decrease the balance of an account. These are two equivalent actions. Both can be seen however as compensating actions for each other (mutual com-pensating actions). An action that increases the balance with 100 euros can be compensated with an action the decreases the balance with 10 euros.
It need not only be fault feedbacks that lead to compensation. The transaction controller may make use of a timer to detect if an acknowledgement has been received within a preset time-frame. If one of the services doesn’t respond with this preset time-frame, requests for compensating actions will be sent to the other services. If a service sends the acknowledgement after the preset time elapsed and compensation action requests have been sent to the other services, this late service will be sent a compensating action request as well. Or one might choose to resend the original action requests to the other services if that is appropriate.
Design considerationsThe designer of the transaction system must for every action always design a compensating action. This is not trivial. Consider the second of the above mentioned examples. Suppose the service was not developed to increase or decrease an amount, but to replace the old amount with a new one. Compensation would then not be possible. If the action would replace the old amount with a new amount of 10 euros, this can not be reverted without endanger the integrity of the data. Not even if the transaction controller would be aware of the old amount (or if the old amount would have been returned to the transaction controller). Other amount changes (triggered by other sources) may have occurred after the replacement transaction has been executed. If afterwards a compensating action would replace the amount back to the original amount, the amount would be corrupted.
Why?In situations where by nature the transaction processing cycle is long (minutes, hours, days, weeks, …) the described pattern of loosely coupled transaction processing allows for an adequate alternative to the well known two-phase-commit mechanism. Also in
loosely coupled of
decoupled environments (B2B) where minimal dependency (maximal autonomy) is pursued, this pattern offers a more appropriate solution then two-phase-commit.
Traditionally transaction systems use the two-phase-commit mechanism. All participating software components must support and adhere to this mechanism. All components reply whether or not they can honour the request of the transaction controller and - if so - retain the action until a commit- or rollback command has been received. A commit command is issued by the transaction controller as soon as all concerning components have replied that they made their preparations and are ready to commit. If one or more of the components deny the request, the transaction controller will send a roll-back command to the other components in order to allow them to release their retention state.
Because after a commit command the participating components
must actual perform the action if they replied to be ready to commit, these components must - awaiting the commit - place locks on the data that is involved in the transaction. This puts a claim on the performance of the system. Such a mechanism is therefore only appropriate for (very) short running processes where a participating component can rely on the performance of the other participating components and on the performance of transaction controller.
If the processing time to complete the transaction takes relatively a long period of time, the inherent locking strategy of the two-phase-commit mechanism is not appropriate because it can hang the complete system. Also in loosely coupled of decoupled environments such as B2B the two-phase-commit mechanism puts undesirable dependencies on the environment and the proposed loosely coupled transaction processing will be more appropriate.