I'd like to track financial transactions (in an in-app currency) in a set of insert-only tables (with postgres backend). A minimal example of how we define it is here:
class Balance(db.Model, CreatedTimestampMixin):
"""Balances table"""
__tablename__ = "balances"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
balance = db.Column(db.Integer, default=0)
# one-to-one each balance change has a transaction
transaction = db.relationship(
'Transaction', uselist=False, backref=db.backref('balance'))
def __repr__(self):
return f"<Balance {self.id}>"
class Transaction(db.Model, CreatedTimestampMixin):
""" Transactions that cause balance changes """
__tablename__ = "transactions"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
balance_id = db.Column(db.Integer, db.ForeignKey(
"balances.id"), nullable=False)
amount = db.Column(db.Integer, nullable=False)
def __repr__(self):
return f"<Transaction {self.id}>"
In this picture when a transaction occurs, the following steps are taken:
- Fetch the last balance for a given user
- Create a new row in balances with a balance that is last balance + amount (amount can be negative)
- Create a new row in transactions with a foreign key to the new balance and the amount.
However, if two different transaction requests come at the same time, this could create a double spend (the old balance is fetched, and then two transactions are entered). I would also ensure that old balance + amount >=0 but if I allow double spend then I could allow dipping below 0.
How do I avoid double spend/ensure that transactions are processed one after another? I kind of understand that solutions could be either by creating some kind of lock or by enforcing parent/child ordering, but not sure what is the correct way to implement it....