We recently added a bank account like functionality into one of our products. During the development we encountered some textbook problems and I thought it can be a good opportunity to go over some of the patterns we use in our Django models.

This article was written in the order in which we usually address new problems:

Define the business requirements. Write down a naive implementation and model definition. Challenge the solution. Refine and repeat.

A Bank Account

Business Requirements

Each user can have only one account but not every user must have one.

The user can deposit and withdraw from the account up to a certain amount.

The account balance cannot be negative.

There is a max limit to the user’s account balance.

The total amount of all balances in the app cannot exceed a certain amount.

There must be a record for every action on the account.

Actions on the account can be executed by the user from either the mobile app or the web interface and by support personnel from the admin interface.

Now that we have the business requirements we can start with a model definition.

Account Model

# models.py import uuid from django.conf import settings

from django.db import models class Account(models.Model):

class Meta:

verbose_name = 'Account'

verbose_name_plural = 'Accounts' MAX_TOTAL_BALANCES = 10000000 MAX_BALANCE = 10000

MIN_BALANCE = 0 MAX_DEPOSIT = 1000

MIN_DEPOSIT = 1 MAX_WITHDRAW = 1000

MIN_WITHDRAW = 1 id = models.AutoField(

primary_key=True,

)

uid = models.UUIDField(

unique=True,

editable=False,

default=uuid.uuid4,

verbose_name='Public identifier',

)

user = models.OneToOneField(

settings.AUTH_USER_MODEL,

on_delete=models.PROTECT,

)

created = models.DateTimeField(

blank=True,

)

modified = models.DateTimeField(

blank=True,

)

balance = models.PositiveIntegerField(

verbose_name='Current balance',

)

Let’s break it down:

We use two unique identifiers — A private identifier which is an auto generated number (id) and a public id which is a uuid (uid). It’s a good idea to keep enumerators private — they expose important information about our data such as how many accounts we have and we don’t want that.

— A private identifier which is an auto generated number (id) and a public id which is a uuid (uid). It’s a good idea to — they expose important information about our data such as how many accounts we have and we don’t want that. We use OneToOneField for the user. It’s like a ForeignKey but with a unique constraint. This ensures a user cannot have more than one account.

It’s like a ForeignKey but with a unique constraint. This ensures a user cannot have more than one account. We set on_delete=models.PROTECT. Starting with Django 2.0 this argument will be mandatory. The default is CASCADE — when the user is deleted the related account is deleted as well. In our case this doesn’t make sense — imagine the bank “deleting your money” when you close an account. Setting on_delete=models.PROTECT will raise an IntegrityError when attempting to delete a user with an account.

Starting with Django 2.0 this argument will be mandatory. The default is CASCADE — when the user is deleted the related account is deleted as well. In our case this doesn’t make sense — imagine the bank “deleting your money” when you close an account. Setting on_delete=models.PROTECT will raise an IntegrityError when attempting to delete a user with an account. You probably noticed that the code is very… “vertical”. This is not just because Medium has such poor support for source code — we write like that because it makes git diffs look nicer.

Account Action Model

Now that we have an account model we can create a model to log actions made to the account:

# models.py class Action(models.Model):

class Meta:

verbose_name = 'Account Action'

verbose_name_plural = 'Account Actions' ACTION_TYPE_CREATED = 'CREATED'

ACTION_TYPE_DEPOSITED = 'DEPOSITED'

ACTION_TYPE_WITHDRAWN = 'WITHDRAWN'

ACTION_TYPE_CHOICES = (

(ACTION_TYPE_CREATED, 'Created'),

(ACTION_TYPE_DEPOSITED, 'Deposited'),

(ACTION_TYPE_WITHDRAWN, 'Withdrawn'),

) REFERENCE_TYPE_BANK_TRANSFER = 'BANK_TRANSFER'

REFERENCE_TYPE_CHECK = 'CHECK'

REFERENCE_TYPE_CASH = 'CASH'

REFERENCE_TYPE_NONE = 'NONE'

REFERENCE_TYPE_CHOICES = (

(REFERENCE_TYPE_BANK_TRANSFER, 'Bank Transfer'),

(REFERENCE_TYPE_CHECK, 'Check'),

(REFERENCE_TYPE_CASH, 'Cash'),

(REFERENCE_TYPE_NONE, 'None'),

) id = models.AutoField(

primary_key=True,

)

user_friendly_id = models.CharField(

unique=True,

editable=False,

max_length=30,

)

user = models.ForeignKey(

settings.AUTH_USER_MODEL,

on_delete=models.PROTECT,

help_text=’User who performed the action.’,

)

created = models.DateTimeField(

blank=True,

)

account = models.ForeignKey(

Account,

)

type = models.CharField(

max_length=30,

choices=ACTION_TYPE_CHOICES,

)

delta = models.IntegerField(

help_text=‘Balance delta.',

)

reference = models.TextField(

blank=True,

)

reference_type = models.CharField(

max_length=30,

choices=REFERENCE_TYPE_CHOICES,

default=REFERENCE_TYPE_NONE,

)

comment = models.TextField(

blank=True,

) # Fields used solely for debugging purposes.



debug_balance = models.IntegerField(

help_text='Balance after the action.',

)

What do we have here?

Each record will hold a reference to the associated balance and the delta amount. A deposit of 100$ will have a delta of 100$, and a withdrawal of 50$ will have a delta of -50$. This way we can sum the deltas of all actions made to an account and get the current balance . This is important for validating our calculated balance.

. This is important for validating our calculated balance. We follow the same pattern of adding two identifiers — a private and a public one. The difference here is that reference numbers for actions are often used by users and support personnel to identify a specific action over the phone or in emails. A uuid is not user friendly — it’s very long and it’s not something users are used to see. I found a nice implementation of user-friendly ID’s in django-invoice.

— it’s very long and it’s not something users are used to see. I found a nice implementation of user-friendly ID’s in django-invoice. Two of the fields are only relevant for one type of action, deposit — reference and reference type. There are a lot of ways to tackle this issue — table inheritance and down casting, JSON fields, table polymorphism and the list of overly complicated solutions goes on. In our case we are going to use a sparse table.

Note about the design: Maintaining calculated fields in the model is usually bad design. Calculated fields such as the account’s balance should be avoided whenever possible.

However, in our “real life“ implementation there are additional action types and thousands of actions on each account — we treat calculated attribute as an optimization. Maintaining state poses some interesting challenges and we thought it can serve the purpose of this post so we decided to present it as well.

Challenges

Multiple Platforms

We have three client applications we need to support:

Mobile app — use an API interface to manage the account.

— use an API interface to manage the account. Web client — can use either an API interface (if we have some sort of SPA) or good old server side rendering with Django forms.

— can use either an API interface (if we have some sort of SPA) or good old server side rendering with Django forms. Admin interface — uses Django’s admin module with Django forms.

Our motivation is to keep things DRY and self contained as possible.

Validation

We have two types of validations hiding in the business requirements:

Input validation such as “amount must be between X and Y”, “balance cannot exceed Z”, etc — these types of validation are well supported by Django and can usually be expressed as database constraints or django validations.

The second validation is a bit more complicated. We need to ensure the total amount of all balances in the entire system does not exceed a certain amount. This forces us to validate an instance against all other instances of the model.

Atomicity

Race conditions are a very common issue in distributed systems and ever more so in models that maintain state such as bank account (you can read more about race conditions in Wikipedia).

To illustrate the problem consider an account with a balance of 100$. The user connects from two different devices at the exact same time and issue a withdraw of 100$. Since the two actions were executed at the exact same time it is possible that both of them get a current balance of 100$. Given that both session see sufficient balance they will both get approved and update the new balance to 0$. The user withdrawn a total of 200$ and the current balance is now 0$ — we have a race condition and we are down 100$.

Logging / History

The log serves two purposes:

Log and Audit — Information about historical actions — dates, amounts, users etc.

— Information about historical actions — dates, amounts, users etc. Check Consistency — We maintain state in the model so we want to be able to validate the calculated balance by aggregating the action deltas.

The history records must be 100% immutable.