Django promotes the Model-View-Controller (some call it MVT for template in the Django world) pattern but encapsulates the pattern within each distinct apps. It’s a great approach because it means that reusable apps can deliver functionality at any or all of the MVC layers. But MVC applied to Django says nothing about how to structure the relationships between apps.
At Power of Two we’ve started to get hit by a lack of app-level structure as our site grows in complexity. With each new app comes a potential birds nest of dependencies which, if left unchecked, would reduce the agility of our site and businesss.
I brought this challenge up with Carl Meyer as part of an ongoing conversation we have about best practices in Django. Carl is a core developer of Django and a mantainer of pip. He has tremendous experience and deep insight into creating solid, maintainable web app in Django.
The questions that I posed was where to put functionality that spans across apps. For example, Power of Two has an activity stream, we send have a mailer, we have a reporting system and we have staff management pages. I asked when should an app import another app’s API to push data through to it it. Alternatively, when should we use signals to decouple apps from each other.
Here’s Carl’s clarifying response:
What you mostly need to keep in mind is your dependency graph. Draw out an actual import graph between your apps if it helps you visualize. Mentally classify your apps into “layers”: “core/utility” apps at the bottom bedrock layer, user-facing apps that import and use the utility/core apps above that… however many layers you have.
what you don’t want is an app in a lower layer importing and using an app from a higher layer. Unidirectional coupling is much preferable, maintenance wise, to bidirectional coupling.
It’s ok to have a utility app that almost every other app in your system imports and uses, but you don’t want your overall dependency graph to just be a mess with import dependencies in all directions and no structure to anything. Ideally your module dependency graph has no cycles in it.
Applying this advice to our code base we concluded that we have four layers.
- Foundation and utility apps – this layer includes almost all of the external installed apps that we use plus a number of self built tools for things like managing A/B tests. None of these modules import any modules from the higher levels.
- User facing apps – this layer is the bulk of our custom code. These apps often import the foundation and utility apps and try to minimize the imports of each other. In order to achieve this we’ve had to split apart some over-reaching apps to drop the utility functionality down a layer. In doing this we have the realized the bonus of more reusable utility modules.
- Staff facing apps – our internal staff have to oversee whats happening on our site. To achieve this we have built some admin-like apps which by definition import from all the user-facing apps. What’s helpful though is to keep a clear division between staff facing apps which do import user facing apps, and user facing apps which should not import each other.
- Reporting apps – our final layer are reporting tools. These are drop-on systems driven by signals or asynchronous events. None of the lower layers import these apps, so we are free to rapidly evolve them to meet the ever changing need for metrics. A future goal is to entirely decouple this layer, which basically means swapping synchronous signals for asynchronous ones, so that there is no risk of this layer introducing bugs that impact the user or staff facing apps.
After completing this refactor we took the final step of ordering our settings file so that INSTALLED_APPS is split into these layers. It’s just a small extra reminder to think about where an app fits into the larger scheme.
The conclusion here is that it is worth thinking about the principles behind the structure of a code base. When functionality gets splattered all over the place and dependencies become circular it makes maintaining and extending much more difficult. While the Model-View-Controller pattern helps within an app, I find it helpful to understand the underlying principles of well structured code so that I can apply them appropriately to each unique situation.