Supercharging Django Productivity
Kyle Hanson is an engineer at eShares, an equity management platform for companies, investors, and employees.
A good framework can be measured by what functionality you get out of the box and by how much friction it removes when customizing it. eShares is the source of truth for financial agreements backing billions of dollars. Code clarity, consistency, and safety are critical. For us, Django is more than boilerplates of models, views, and templates; it is a cohesive easy-to-extend abstraction of how data interacts with the world. Using Python principles and inheriting Django classes, we create sophisticated services with minimal code.
An App’s First Steps
All of our models inherit from one source. We are able to centralize global functionality in one place. Making an application inherit from one model is powerful. The same is true for views. When you first start a project, the purpose of these classes might seem unclear. If you build your project from the beginning with the methodology that it will become a big app, it makes scaling your code much easier when the time comes.
Simple base classes should not be underestimated. While they don’t enable functionality, they give you the mindset from the start that your models and views are really your models and views. Managing site-wide behavior becomes trivial.
One of the features we wanted from the beginning was a default policy against deleting anything from the database. It helps with reversing mistakes and separates active data from stuff we want to keep but don’t want to see. The challenge in such a system is how to reliably keep all the objects that were soft-deleted out of our queries.
A boolean is_void flag on our base model makes the magic happen. All of our querysets exclude this flag by default, but they can be accessed by using Model.objects.all_objects_including_void() (a purposefully long name). On the model and queryset we override the delete() method to instead set the is_void flag to true and void any dependents. Hard deleting behavior lives at purge_from_db(), making it obvious to developers what is happening.
We only need logical deletion on a few tables, but inheritance makes implementing it for one model the same amount of effort as standardizing it across the application. Storage is cheap. It’s better to keep it and never need it than to ditch it and regret it.
A more comprehensive implementation with cascading delete and exceptions and can be found on github.
Inheriting from one view, we enable uniform site wide permissions. A common class creates composable permission sets. Our permissions are specialized around what legal entity the page is showing. They inspect the URL and POST body parameters to ensure all object parameters are consistent with the legal entity and if the user has access to those items. Broadly, a permission check is anything that returns a boolean.
The SuperUserGuard permission makes our views secure by default. Any more relaxed permissions must be set explicitly. It gives us a safety net so we don’t accidentally publish a view.
A historical record needs to exist over the lifetime of a financial security. We need to track every page view, every change to a table, and every action on the site.
Django opens the possibility to hook into every part of an application by extending a few base classes. Hooking into models and views creates a unified system for linking the contexts of web requests with actions.
The event system is implemented using a dedicated insert-only single-table Postgres instance. The table contains fixed columns (such as username, user id, url path, time, browser, etc.) and a JSON field for arbitrary context data. By having a central event stream, we can create arbitrary event types and then have celery post-process the events. We use this to post messages to slack, send emails, interact with analytics services, and more.
To track the model instance changes, we extended the queryset’s update(), delete(), create() methods as well as the model’s save() method. Anytime fields are changed through update() or save(), we generate a diff of what has changed and push it into the event database. Creation and deletion are also tracked. While it doesn’t track changes made directly in psql, it can alert us to data inconsistencies and point us to look at other logs.
Django’s virtue is how easy it is to quickly implement complex behavior. All of the above projects were originally done in one day and have stood up over the years with minimal maintenance. Many more projects like querysets that recreate past history, object-oriented caching, and customer centric labeling add critical functionality without much maintenance or upfront development. A good chunk of the productivity can be attributed to Python itself.
This was important for a start-up with limited resources that needed to maintain a secure financial application. The low cost of implementation meant we were able to include these features from the beginning.
These design patterns also solidified a mentality within our team that there is an internal standard of handling models and views. We can extend our base classes to tackle any business requirement. Our codebase is constantly evolving, both in new features and how we model the world. Django has proven to be a valuable tool that lets us aim big and iterate often.