Skip to content

Where to put Django Business Logic?

Video: Django: Where to put business logic

Notes on abstraction

  • If you're putting something the database, use as little abstraction as possible
    • no DRF modelviewsets
  • getting something out of database
    • use as much abstraction as needed

Boxes that Django gives us

  • Models
  • Views/APIs
  • Templates
  • Forms / Serializers
  • Tasks

Models

  • small @property
    • has_started
    • has_finished
  • validation logic with clean

Why business logic doesn't belong in Models

  • fat models aren't maintainable
  • God object

Testing Models

  • custom validations or properties
  • tests don't hit db -> fast tests 🏃

Custom Model Managers and QuerySets

Django Style guide: business logic should not live in custom managers or query sets

When to use custom model managers and querysets?

  • like model properties
  • use this for simple stuff
    • there might be more complex logic like 3rd party API calls
    • where you should move the logic to services.py

Custom Model Manager example

Custom Model Manager & Model QuerySet

class PostManager(models.Manager):
    def sorted(self):
        return self.get_queryset().sorted()
Post.objects.sorted()

get_queryset is like Post.objects.all()

QuerySet Manager example

Implement your own get_queryset

class PostQuerySet(models.QuerySet):
    def sorted(self):
        return self.order_by('-created_at')

so that you can

Post.objects.all().sorted()

Views & APIs

Django Rest Framework (DRF)

need to put it in the serializer

model view set???

Serializers

  • Python/ORM objects <--> JSON

Why business logic doesn't belong to serializer

Adding 1 property --> breaking 5 other APIs because they all use the serializer

Services

What is a service

  • simple, type annotated function
  • speaks the domain language
  • handles
    • permissions
    • cross-cutting concerns
    • calls other services/tasks
    • works mainly with models

selectors.py

  • business logic around fetching
  • not always necessary handle permissions, filtering, ...

Rule of thumb

  • if you get the N + 1 problem
    • If your model property starts doing queries on the model's relations
    • which can't be solved with select_related or prefetch_related
  • it should be a selector

example

class Lecture(models.Model):
    @property
    def not_present_students(self):
        present_ids = self.present_students.values_list('id', flat=True)

        return self.course.students.exclude(id__in=present_ids)

Testing services

  • where the business logic is
  • hit the db
  • mocking

Services Examples

from .selectors import get_courses

class CourseListApi(SomeAuthenticationMixin, APIView):
    class OutputSerializer(serializers.ModelSerializer):
        class Meta:
            model = Course
            fields = ('id', 'name', start_date', 'end_date')

    def get(self, request):
        courses = get_courses()
        data = self.OutputSerializer(courses, many=True)
        return Response(data)
from .services import create_course

class CourseCreateApi(SomeAuthMixin, APIView):
    class CreateInputSerializer(serializers.Serializer):
        name = serializer.CharField()
        start_date = serializers.DateField()
        end_date = serializers.DateField()

    def post(self, request):
        serializer = self.CreateInputSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        create_course(**serializer.validated_data)
        return Response(status=status.HTTP_201_CREATED)
class UpdateInputSerializer(serializers.Serializer):
    name = serializer.CharField(required=False)
    start_date = serializers.DateField(required=False)
    end_date = serializers.DateField(required=False)

Why nested Serializers in the API?

We don't want to reuse serializers, especially for InputSerializers

  • output serializer is okay sometimes

Editing a base serializer will break a ton of APIs - need to reuse serializers with great care


Last update: 2022-11-04