Search code examples
springkotlinaccounting

Avoiding Deadlock while running a three legged transaction


In an accounting system, money can be transferred between accounts.

To avoid deadlock while double booking transactions (Transfers between accounts) Transfer from account to account is based on id order:

@Transactional
    override fun doubleBookPrepaid(eventId: Long, srcPurposefulAccountId: PurposefulAccountId, trgPurposefulAccountId: PurposefulAccountId, amount: Money): Pair<Money, Money>? =
        if (srcPurposefulAccountId.accountId < trgPurposefulAccountId.accountId) { // Locking minimal account ID first, to prevent deadlocks.
            val srcBooking = bookPrepaid(eventId, srcPurposefulAccountId, -amount)
            val trgBooking = bookPrepaid(eventId, trgPurposefulAccountId, amount)

            T(srcBooking, trgBooking)
        }
        else {
            val trgBooking = bookPrepaid(eventId, trgPurposefulAccountId, amount)
            val srcBooking = bookPrepaid(eventId, srcPurposefulAccountId, -amount)

            T(srcBooking, trgBooking)
        }

How can I accomplish the same result for a three-leg transaction? In this kind of transaction, one account will transfer money to two accounts in the same transaction:

data class PurposefulAccountTransfer(val trgPurposefulAccountId: PurposefulAccountId, val amount: Money)
    @Transactional
    fun threeLegBookPrepaid(eventId: Long, srcPurposefulAccountId: PurposefulAccountId, purposefulAccountTransfer: PurposefulAccountTransfer, secondPurposefulAccountTransfer: PurposefulAccountTransfer) {
        val srcBooking = bookPrepaid(eventId, srcPurposefulAccountId, -(purposefulAccountTransfer.amount + secondPurposefulAccountTransfer.amount))
        val trgFirstBooking = bookPrepaid(eventId, purposefulAccountTransfer.trgPurposefulAccountId, purposefulAccountTransfer.amount)
        val trgSecondBooking = bookPrepaid(eventId, secondPurposefulAccountTransfer.trgPurposefulAccountId, secondPurposefulAccountTransfer.amount)
    }

Solution

  • You just need to sort all of the commands to ensure that there is no cyclic dependencies (assuming T() accepts vararg):

    fun threeLegBookPrepaid(eventId: Long, ...) {
       val txs = sortedMapOf(
          srcAccountId  to bookPrepaid(eventId, srcAccountId , ...),
          trg1AccountId to bookPrepaid(eventId, trg1AccountId, ...),
          trg2AccountId to bookPrepaid(eventId, trg2AccountId, ...)
       )
       .map { it.component2() }
       .toTypedArray()
    
       T(*txs)
    }
    

    This can be easily generalized to any number of accounts.