Business logic in a Django project
Have you ever wondered where in the hello world is your business logic meant to reside in a Django project? In the models, you can argue, maybe in the managers, who really knows? Let’s talk a little about it.
Active Record
Quoting Django’s design philosophy models should:
Include all relevant domain logic
Models should encapsulate every aspect of an “object,” following Martin Fowler’s Active Record design pattern
Mmmm, so Active Record uh, a popular design implementation used in a great variety of ORMs. Also claimed an antipattern by a relevant part of the community.
Quoting Robert Martin in Clean Code:
Active Records are special forms of DTOs [Data Transfer Objects]. They are data structures with public variables; but they typically have navigational methods like save and find. Typically these Active Records are direct translations from database tables, or other data sources.
Unfortunately we often find that developers try to treat these data structures as though they were objects by putting business rule methods in them. This is awkward because it creates a hybrid data structure and an object.
So yeah, we see some contradictions here. As Martin Fowler described them: Active Records are objects that encapsulate data access and add domain logic to that data. The problem is that this kind of structure flushes the sink any sign of SRP you could expect in it. And that could be a problem.
Fat models
So how this recommended structure is really implemented in practice? Following that model, we start building what the community calls fat models (and thin views). An extended practice recommended in a lot of learning material like django-best-practices or ultimatedjango.
Using this pattern, we implement all the business logic in the models and managers making them fat. That’s not really a good idea because that makes the testing really difficult and very coupled to the ORM (creating you a mocking mayhem and the need of a big number of fixtures and data to create different test scenarios). Following this practice makes your business logic nearly impossible to test without using a database.
Growing your models to infinity makes maintaining them really difficult too. With every new use case, your model will grow one or several new methods and it can quickly and dangerously become a god object.
So what can we do to avoid this kind of problems?
Quoting again Robert Martin’s Clean Code:
The solution, of course, is to treat the Active Record as a data structure and to create separate objects that contain the business rules and that hide their internal data (which are probably just instance of the Active Record).
The business layer
The answer is the almighty business layer where our business rules live. Sometimes divided into subsets like domain logic and application logic, it is the layer where we shall implement our business flows, actions and logic.
David Winterbottom in his great and funny article Why your Django models are fat teaches us some good lessons like:
As a rule-of-thumb, your application logic should live in modules that aren’t Django-specific modules (eg not in
views.py
,models.py
orforms.py
). If I had my way, Django would create an emptybusiness_logic.py
in each new app to encourage this.
So yeah, a business logic module sounds like a great idea. A place where you implement your commands, use cases, actions or flows (they have so many names) and where they remain loosely coupled to your data sources.
Just be careful of not taking too much away from your models or you will be left with anemic domain models. Only business logic is meant to leave, the domain logic like validations, calculations, etc are already at home.
The data layer
And how we can maintain this loose coupling between business logic and the dangerous and awesome Django ORM? Because it is soo easy and wonderful to use, it’s a pity that you can fall into it so easily.
The answer is treating it like a data repository. A really not used pattern to a big part of the Django developers I used to know (highly influenced by using the Django ORM everywhere).
In reality, this solution is the recommended way of doing things, although it is not advertised as a data repository at all. In every Django tutorial, you are suggested to write your queries inside custom managers to avoid repetition and writing the same query every time. But you are gaining a lot more than that, implementing the queries there decouples the ORM implementation from its use in the application, a win-win situation.
Following this principle, you can then use your Model.objects as it was your data repository in the business logic, reducing the coupling to the ORM and allowing you to do integration tests for the database interactions only. In the next example, we’ll see how to use it.
Real example
Now let’s see an example to understand the situation better.
The example is very simple: we have an Address model that has an is_default field that indicates if that address is the default one of its user.
And then we have an action that makes an address instance the default one, setting its is_default to True and setting it to False to the other user’s addresses. After setting the default address, we publish the changes to anyone interested.
Here first we have the example done using fat models (high coupling and low cohesion):
Some important things to notice are:
- The model indeed contains the business action set_default.
- It has dependencies with other models (in this case itself).
- There are side-effects in the action inside the model, breaking the SRP.
- To achieve unit tests for our business logic we need to mock up all the dependencies.
In the last point, we have something important to discuss. You can follow 2 approaches: do an integration test where you validate the business flow with all its dependencies or do a unit test mocking all the external calls.
The problem with the integration test is that you have to test the behaviour using a real database as all the dependencies will end using the ORM, slowing down your tests and not validating the logic flow independently of the dependencies. The problem with the unit test is that you lose the integration validation of database behaviours and you have to mock everything.
So let’s look to the same example but applying a business logic layer and abstracting the database interactions to a manager (low coupling and high cohesion):
As you can see we have easier tests and more maintainable code. In this case, we implement unit tests for our business logic that are fast and validate its flow. And then integration tests for the manager that validate the data operations and its dependencies with the database.
The key in this structure is that our business logic is not aware of data integrity or how it is stored, that is the job of the managers. This way we can unit test every flow and leave the integrity checks for the integration tests of the data layer.
Final words
Django is a great framework, but after all, it is only a framework. It’s your job to use it correctly and structure your application to your needs.
Fat models in Django can make sense for properties (like full_address) and object validations, but including business logic and side effects into your Active Record objects is not really a good idea, some kind of service (or business) layer is my recommended way to go.
Be careful not to abuse the Django ORM and rely on it for everything. It is one of the best ORMs out there but as a consequence, it makes it too easy to end up with a giant, coupled application if you don’t pay attention.
And to conclude just say that these are my always evolving ideas about how to structure logic into Django projects. There are a lot of ways of doing things and every situation is different so there is no correct answer for it.
I hope you found this interesting! If you want to read about other topics like product or data science you can check my e-commerce recommendations series. I hope to see you soon in future articles!