And a second question: let's say a store credits ledger is implemented as a separate sub-system, what challenges do you see in using eventual consistency in the purchase flow?
One approach might be logging a payment intent in the database and processing the transaction asynchronously. If something fails, appropriate rollbacks would be applied. Kind of like a Saga with orchestration, but without extra complexity: without changing the downstream services or building Saga middlewares.
Thanks again for your question Olek, I hope these articles are helping you as much as they're helping me writing them. Let me try to address both questions.
The reason you need distributed transactions in payment systems is that most of the time you're not approved for handling the payment yourself. Card payments, in particular, are a very regulated environment. Even digital wallets are subject to somewhat strict regulation when it comes to whether you're allowed to keep the breakage.
That's why there are companies called PayFacs, which provide the APIs to connect to card payments and all that.
What these PayFacs tend to do is imply that you shouldn't be doing payments at all, and that you should handle all payment concerns to them. They might suggest that the less you have in your systems that is payments related, the better. That's how I'm reading your second question: can we just not do this.
I believe that's not a good idea. Payments, your customers' money, that's important data for your business. It is indeed simpler to just do as little as possible and let the PayFac handle that for you. But, and this is a crucial BUT, there are fees involved in this, a power play of sorts. Giving this data to them is giving up on an important negotiation lever, because the more the PayFac does for you, the less able you are to switch providers.
So your suggestion has technical sense. You can do what you're suggesting— it's doable from a technical standpoint, and it's easier. But if I remove my tech hat and put my business hat on, I would recommend against it.
I realize now that my previous question wasn't clear. Of course, you won't get away from the distributed nature of transactions and you would want to do them via PayFacs. I only wanted to challenge the distributed solution for the store credits :)
I have in mind solution like this.
```
try {
db.transaction.begin()
stripe.paymentIntents.create(...)
debitStoreCredits(userId, amount) // store credits are stored in a DB
db.transaction.commit()
} catch(e) {
db.transaction.rollback()
}
```
Then if a card transaction on the Stripe side is declined, credit the same amount back to the user's Store Credit account.
Even if Store Credits are managed by a remote server, you could still have a solution simple enough. Using outbox pattern, for example.
Yes, there's an issue with this specific approach, and that's that payments via Stripe can finalize asynchronously (the status may go to "pending"). That's why the 2 phased commit pattern is inevitable here: you can only confirm the credits once the payment is authorized, and not a moment earlier.
Otherwise you have to engage in compensating entries in the credit ledger, which is unnecessary in my opinion.
I'm curious why a payment system isn't designed to avoid the need for complex distributed transactions? Such an approach could enhance cohesion and maintainability. A ledger with store credits could be implemented in the same service, as payments and store credits are inherently linked and share similar architectural characteristics. By separating them into distinct entities, we might gain no real advantage. Am I missing something?
And a second question: let's say a store credits ledger is implemented as a separate sub-system, what challenges do you see in using eventual consistency in the purchase flow?
One approach might be logging a payment intent in the database and processing the transaction asynchronously. If something fails, appropriate rollbacks would be applied. Kind of like a Saga with orchestration, but without extra complexity: without changing the downstream services or building Saga middlewares.
Thanks again for your question Olek, I hope these articles are helping you as much as they're helping me writing them. Let me try to address both questions.
The reason you need distributed transactions in payment systems is that most of the time you're not approved for handling the payment yourself. Card payments, in particular, are a very regulated environment. Even digital wallets are subject to somewhat strict regulation when it comes to whether you're allowed to keep the breakage.
That's why there are companies called PayFacs, which provide the APIs to connect to card payments and all that.
What these PayFacs tend to do is imply that you shouldn't be doing payments at all, and that you should handle all payment concerns to them. They might suggest that the less you have in your systems that is payments related, the better. That's how I'm reading your second question: can we just not do this.
I believe that's not a good idea. Payments, your customers' money, that's important data for your business. It is indeed simpler to just do as little as possible and let the PayFac handle that for you. But, and this is a crucial BUT, there are fees involved in this, a power play of sorts. Giving this data to them is giving up on an important negotiation lever, because the more the PayFac does for you, the less able you are to switch providers.
So your suggestion has technical sense. You can do what you're suggesting— it's doable from a technical standpoint, and it's easier. But if I remove my tech hat and put my business hat on, I would recommend against it.
Thanks for the answer!
I realize now that my previous question wasn't clear. Of course, you won't get away from the distributed nature of transactions and you would want to do them via PayFacs. I only wanted to challenge the distributed solution for the store credits :)
I have in mind solution like this.
```
try {
db.transaction.begin()
stripe.paymentIntents.create(...)
debitStoreCredits(userId, amount) // store credits are stored in a DB
db.transaction.commit()
} catch(e) {
db.transaction.rollback()
}
```
Then if a card transaction on the Stripe side is declined, credit the same amount back to the user's Store Credit account.
Even if Store Credits are managed by a remote server, you could still have a solution simple enough. Using outbox pattern, for example.
Do you see any problem with it?
Thanks!
Yes, there's an issue with this specific approach, and that's that payments via Stripe can finalize asynchronously (the status may go to "pending"). That's why the 2 phased commit pattern is inevitable here: you can only confirm the credits once the payment is authorized, and not a moment earlier.
Otherwise you have to engage in compensating entries in the credit ledger, which is unnecessary in my opinion.
Right. The compensating entries may be required in my approach.
Thanks for the interesting read, as always!
I'm curious why a payment system isn't designed to avoid the need for complex distributed transactions? Such an approach could enhance cohesion and maintainability. A ledger with store credits could be implemented in the same service, as payments and store credits are inherently linked and share similar architectural characteristics. By separating them into distinct entities, we might gain no real advantage. Am I missing something?