Service Layer
Purpose: provide every domain model with a predictable “service layer” so business rules live next to data and are invoked as naturally as
Task.objects
.
1 · Why a Service Layer?
Django’s flexibility often scatters business logic across views, utils, managers, and helpers. A dedicated service layer fixes that by pinning every rule to the model that owns it. Each model exposes a services
descriptor that:
- groups validation, persistence, and side‑effects in one place
- easy access from model
task.services.notification.send_created()
- keeps code modular for easy testing
- avoids circular‑import headaches through future annotations and
TYPE_CHECKING
guards
The example below uses a simple Task model.
2 · What the BaseService
Gives You
Method | Purpose |
---|---|
validate_model_obj() |
Runs full_clean() on the target object and raises if validation fails. |
save_model_obj() |
Calls validate_model_obj() and then save() |
3 · Building a TaskService
3.1 Files & Directories
tasks/
├── models.py
└── services/
├── service.py # TaskService (primary)
└── notification_service.py # TaskNotificationService (secondary)
3.2 The Model
# tasks/models.py
from __future__ import annotations
from typing import TYPE_CHECKING
from django.db import models
if TYPE_CHECKING:
from app.tasks.services.service import TaskService
class Task(models.Model):
title = models.CharField(max_length=200)
is_done = models.BooleanField(default=False)
services: TaskService = TaskService()
def __str__(self) -> str:
return self.title
3.3 The Service (and sub‑service!)
# tasks/service/services.py
from __future__ import annotations
from typing import TYPE_CHECKING
from django_spire.contrib.service import BaseDjangoModelService
if TYPE_CHECKING:
from app.tasks.models import Task
from app.tasks.service.notification_service import TaskNotificationService
from app.tasks.service.processor_service import TaskProcessorService
class TaskService(BaseDjangoModelService):
# target model — must be first
task: Task
# followed by all sub services
notification: TaskNotificationService = TaskNotificationService # <- passing class
processor: TaskProcessorService = TaskProcessorService
# tasks/service/services.py
from __future__ import annotations
from typing import TYPE_CHECKING
from django_spire.contrib.service import BaseDjangoModelService
if TYPE_CHECKING:
from app.tasks.models import Task
class TaskProcessorService(BaseDjangoModelService):
task: Task
def mark_done(self) -> Task:
self.task.is_done = True
self.task.save()
return self.task
4 · Common Service Files
File path | Class | Responsibility |
---|---|---|
service/services.py |
TaskService |
Parent service class that links sub services |
service/notification_service.py |
TaskNotificationService |
Deliver messages triggered by task events |
service/transformation_service.py |
TaskTransformationService |
Turn objects into new forms of other objects |
service/processor_service.py |
TaskProcessorService |
Processes actions on that object |
Each secondary service begins with task: Task
so it plugs into the same descriptor system.
5 · Class‑ vs Instance‑Level Access
from app.tasks.models import Task
# Instance‑level use – operate on one concrete record
task = Task.objects.get(pk=42)
after = task.services.mark_done()
# Class‑level use – no row yet, or act on many rows
# The descriptor fabricates a "null" Task (pk = None) behind the scenes,
# applies defaults, then runs the service logic.
Task.services.automation.clean_dead_tasks()
When to pick which
Use‑case | Call form | Why it makes sense |
---|---|---|
Work on one existing row | task.services.mark_done() |
You already have the instance; the service mutates it and persists changes. |
Run bulk / maintenance logic or logic before a row exists | Task.services.automation.clean_dead_tasks() |
You need the behaviour but not a specific row to start from; the service will create its own temporary Task or iterate over many. |