I have app with paymemt/subscription service, pretty much how the payment works is:
Initial payment
- User initiate payment on the web ( click on “subscribe”)
- Back-End send init request to the gateway – it returns payment id ( also used for subscribe payments )
- Back-End send process request to the gateway
- Gateway returns url to redirect
- User is redirected to the gateway, fills credit card etc and pays
- Gateway redirects back to the back end
In 6th step, after gateway redirects back to the back-end with POST request, i match returned payment id with payment id stored in DB, mark it as done and add membership to the user.
For membership i use the bable
create table membership
(
id bigserial primary key,
start_date date not null,
end_date date,
resub_date date,
client_id bigserial
active boolean default false
);
Now my resub mechanism is following, i have task that executes every midnight and does:
- Retrieves all memberships that have resub_date for today and are active
- init payment
- process payment
I am using ThreadPool for steps 2 and 3, to in, so each thread init and process payment for membership, then takes another etc etc.
My question/problem is with “unlucky” timing during canceling membership and subscribing again
Imagine following scenario
- Midnight of 1.10.2024
- Periodic task executes
- Task retrieves all user that should be resubscribed – including user A
- Task starts to process membership of user A – check if he is subscribed again – yes and inits payment
- User A on web cancel membership
- User A on web resubscribe again – thus leading to
**Initial payment**
flow - Task is finished with inits payment and start process payment
In this case the user would pay twice.
In both init and process i can check if the user should be resubscrbed again ( but this would mean 2 additional database hits per resubscribe ), but with some unlucky timing the payment could still be processed twice.
What are the best ways to prevent these types of scenarios? Some kind of locking on row at db level, or some kind of custom lock that would lock on user id – in this case the performance? Somehow integrate redis into this? I know the chances for this are very slim but it is still problem.
I am using java so i am tagging this question as java but the answer doesnt need to be java specific.
Thanks for the help!
1
There is no way to fully prevent a double payment. The best way to deal with it in my view is to detect the situation after it happened and pay back the payment that should not have happened.
With the scenario you described, I would deal with it in the following way:
- Any subscription, even a re-subscription, creates a new
membership
record. - When the payment process for continued subscription is initiated, it will not be stopped by changes to the corresponding
membership
record. - If a subscription is cancelled within X time of initiating payment for continuing the subscription, once the payment processes have completed, a credit-payment will be performed on the recently cancelled subscriptions.
This does not prevent someone from paying twice, or paying for a cancelled subscription, but it tries to correct for the situation. This is a form of eventual consistency: the amount payed by the customer is not at all time equal to the amount due, but eventually the two match.
3
I have doubts your logic is complete around steps 5 and 6:
- User A on web cancel membership
- User A on web resubscribe again – thus leading to Initial payment flow
When a user cancels membership, don’t you REFUND the user?
You can ensure data consistency by wrapping all the relevant steps in a transaction.
As you’re describing an interplay of multiple components, this will likely have to be a distributed transaction.
Not all technology stacks support distributed transactions. In the past, I had good success getting .Net code to participate in a transaction with a SQL Server database and an MSMQ (message queue) via msDTC (distributed transaction coordinator). This ensures that all components either commit or roll back together, maintaining consistency.
Outside the Microsoft stack, I read about (but did not use yet) a number of distributed transaction coordinators, such as Atomikos, Narayana, Bitronix Transaction Manager (BTM), SEATA (Simple Extensible Autonomous Transaction Architecture), Apache Kafka Transactions (with Kafka Streams) and OpenXA. Many of these are Open Source.
Each of these transaction coordinators can manage distributed transactions across heterogeneous systems, including databases, message queues, and services.
Radu Popa is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
1