Django model Guideline

Jair Verçosa
4 min readDec 29, 2018

I’ve worked for several years on projects that used Django now and I’ve seen many different implementations using this framework.

Historically, a lot of articles on the internet have been led you to use Fat Models, which makes you push most (if not all) of your business logic/rules into your Django Models. I strongly disagree with that and in this article, I will tell you why.

I will also give you 5 items for you to keep in mind when designing your models. First, let me tell you why I think fat models are a bad thing.

Fat models create a lot of dependencies overtime

Because most of your “business logic” is actually flows, you will end up making calls to methods on different models and functions from the Model you’ve started the flow. It creates unnecessary dependencies especially for the models that are part of the core of your application.

Coupling those components like this forces you to change your code if any change happens on other models such as a method signature or a side effect that was added into it.

There’s no reason for that. Over time, it will create a tail of calls that you will never be able to track.

Fat models are hard to test

If your models have big flows, certainly your tests are going to be big and require a lot of mocking.

Keep your methods simple, avoid dependencies, and you should enjoy an easier life testing your stuff. Trust me, I’ve been there.

Fat models violate the Single Responsibility Principle (SRP)

Fat Models make you to add more responsibility to a particular class. Frequently I see in projects people making models such as "Customer", to send emails and notifications. The responsibility of a model is to handle the data, not to send emails.

Fat models make you lazy

That sounds strange but it's just too easy to add another method on the model to facilitate your life. Then you don’t need to import another function or class to do what you want.

Therefore, your class acts more like a facade than a model, doing all sort of things. Try to test it! it's a nightmare.

Here's what I consider good practices

Models handle their own data, and that’s it!

Django models are an implementation of the Active Record pattern. It should contain only the logic that handles its own data.

Add behaviors to models, not flows

After all, Django models are just python classes. On Object-Oriented Programming, the object’s state should be changed only by its behaviors. In this case, I’d add only methods that actually validate and change the state. Thus you reduce the risk of the state getting corrupted.

Imagine you have a model called Customer.

from django.db import models
class Customer(models.Model):
name = models.CharField(max_length=90)
active = models.BooleanField(default=True)
work_email = models.EmailField()
personal_email = models.EmailField(null=True, blank=True)

Now, let’s say you want to deactivate a particular customer. The way you do it is by adding behavior to that model.

from django.db import models
class Customer(models.Model):
name = models.CharField(max_length=90)
active = models.BooleanField(default=True)
work_email = models.EmailField()
personal_email = models.EmailField(null=True, blank=True)
def deactivate(self):
self.active = False
self.save(update_fields=[‘active’])

Let’s suppose now, you want to validate if the work_email is different from personal_email. Here's how it'd look like.

from django.db import models
class Customer(models.Model):
name = models.CharField(max_length=90)
active = models.BooleanField(default=True)
work_email = models.EmailField()
personal_email = models.EmailField(null=True, blank=True)
def deactivate(self):
self.active = False
self.save(update_fields=[‘active’])
def set_personal_email(self, email):
if self.work_email == email:
raise ValueError(
“Personal email and work email are equal"
)
self.personal_email = email
self.save(update_fields=[‘personal_email’])

By the way, I’m persisting in the state every time a behavior gets called on purpose. You don’t have to do it. You could literally call the save method from outside of the class. However, I’d argue that this way you can actually know exactly where you’re persisting the data and how it is happening.

Avoid changing the state directly

Do not change the state of the class directly. Always use the behaviors. This is important! Changing the state in many different places makes it really hard to track the object in large applications.

# Do not do this
customer.name = ‘John Smith’
customer.save()
# This is better
customer.set_name(‘John Smith’)

Avoid dependencies

Don’t make your model call methods on other models or classes. What you really want is a flow, you can have a function calling both models and performing the flow.

def create_customer(self, name, work_email, personal_email=None):
customer = Customer.factory(
name=name,
work_email=work_email,
personal_email=personal_email
)
WelcomeEmailSender.send(customer.name, customer.work_email)
return customer

Make use of Model Managers

I would also avoid having queries all over the place. If you concentrate them on your Manager you can test them better and make changes only in one place.

from django.db import models
class CustomerManager(models.Manager):
def find_by_email(self, email):
return self.filter(
models.Q(work_email=email) |
models.Q(personal_email=email)
)

def find(self, id):
return self.get(pk=id)

class Customer(models.Model):

objects = CustomerManager()

Conclusion

These are just a few ideas that could make your models better on your Django project. I’d actually have my own Domain models in plain old python objects, but I understand that Django makes your life really easy when it comes down to get things ready.

Just keep in mind though that embracing a framework does not mean that you can break software architecture principles. Aways seek for low coupling and high cohesion among your components.

These are my ideas based on the large Django projects I've worked on. I've seen the issues I pointed out here becoming really challenging from the architectural point of view, making the extension of the "current system" a really hard task.

You're free to disagree with me on this. In software architecture, there's no right answer to a problem, but rather options. However, you must live with the consequences of it.

--

--

Jair Verçosa

CTO @ Flieber, Mentor @ Latitud, and Tech advisor. Building the next generation of supply-chain automation and helping companies to master the zero to one game!