What is important is that the success of the second request is independent of the success/failure of the first request, and vice-versa the success of the first request is independent of the success/failure of the second request.
You could achieve this by reserving resources demanded from the account. This could be achieved at the database with an update commit, it should also add a record identifying whom has reserved it. Failure to reserve implies an insufficiency on the account that cannot currently satisfy the request.
Resources into the account don't participate because if their request fails, for any technical or business reason, then those resources where never available, and so should never be relied on for another request.
Once a reservation was successfully committed, then the request knows that it can process the rest of the request.
- If that results in a rejection of the request, it need only remove the reservation.
- If that results in accepting the request, the reservation transmutes into the transaction record itself.
Claim first, Verify next
You can achieve this by treating the account as a stream of events in a sort of event log. The request appends a record to the event stream, by definition this serialises the record. Then each thread/process is responsible for taking its current view of that account and reading each appended record from the last event to be added to the view up to the event that it itself requested to be appended.
- Any record that changes the account in an invalid way, is rejected.
- Any record that changes the account in a valid way, is accepted.
A Request for X dollars which leaves an account with a positive balance might be deemed valid, hence the request was accepted. Alternately perhaps the balance would have become negative, and that account has no over-draft, hence that action is invalid and the request would then have been rejected.
The view of the account at specific points in the log can be cached for speed, and importantly can be updated independently of other process working on the same account. There is a little overhead in comprehending the logged events but then again it does provide independence and serialisation.
Each account is a service
Alternately instead of having everyone work out independently what happened, treat each account as an actual service. When a request against that account occurs, they are all forwarded to the same single threaded process. This has the benefit of collocating all the knowledge and rules, but the disadvantage of not scaling well, there is literally one process/thread per account.
You could make the service responsible for a portfolio of accounts, but as it must be single-threaded all of these accounts must serialise their transaction when they don't have to, also load-balancing becomes an issue (2 process with very active accounts and 100 process with relatively passive accounts).