Django + DRF layered architecture
Portable rules distilled from Lumi ERP / Omayka backends. Apply to new and existing Django+DRF projects unless the repo explicitly overrides them.
Layer dependency (strict)
HTTP → views (DRF) → serializers (I/O edge)
→ services (use cases)
→ dtos (typed contracts)
→ models (ORM)
→ adapters/* (external I/O: HTTP, browser, LLM, files)
| Layer |
Owns |
Must not |
| views |
Auth, status codes, call service, return Response |
Business rules, ORM queries beyond get_queryset helpers, Playwright/LLM |
| serializers |
Validate request/response; map ↔ DTO |
Domain rules, calling other modules' models for decisions |
| dtos |
Dataclass / TypedDict contracts in/out of services |
Touch ORM or request |
| services |
Transactions, validation, orchestration, emit events |
Know DRF request; scrape UI; fat loops in serializers |
| models |
Persistence, constraints, indexes |
Decide multi-step workflows |
| adapters |
External systems |
Import views; leak HTTP details into services beyond DTOs |
Services receive and return DTOs, not request and ideally not raw models at the public method boundary (map model → DTO inside the service).
A service may call another service in the same app. Never import views from services. Cross-app: prefer DTOs, service APIs, or domain events — avoid importing another app's models for writes when a service exists.
API versioning
- Expose new endpoints only under
/api/v1/... (or the project's current version prefix).
- Do not add new clients to unversioned
/api/... routes.
App layout (per module)
apps/<module>/
models.py
dtos.py
services.py
serializers.py
views.py
urls.py
apps.py
tests/
adapters/ # optional package at backend root
<system>/ # e.g. lumi/, llm/, payments/
Shared bits: apps/common/ (BaseDTO, exceptions, EventBus helpers).
DTO patterns
from dataclasses import dataclass
from apps.common.dtos import BaseDTO
@dataclass
class CreateAgentDTO(BaseDTO):
name: str
role: str
@dataclass
class AgentDTO(BaseDTO):
id: str
name: str
role: str
- Serializers validate input → build DTO →
Service().method(dto) → serialize DTO/dict out.
- Type-hint service public methods.
View pattern (thin)
class AgentViewSet(viewsets.ViewSet):
def create(self, request):
ser = CreateAgentSerializer(data=request.data)
ser.is_valid(raise_exception=True)
dto = CreateAgentDTO(**ser.validated_data)
result = AgentService().create(dto)
return Response(AgentSerializer(result).data, status=201)
Service pattern
class AgentService:
@transaction.atomic
def create(self, dto: CreateAgentDTO) -> AgentDTO:
# validate + persist + map to AgentDTO
...
Adapters (external edges)
- Browser automation, third-party HTTP, LLM, filesystem: live under
adapters/.
- Services call adapters with DTOs; adapters return DTOs (
SkillResultDTO, etc.).
- Keep selectors, SDK clients, and retries inside the adapter.
Multi-tenancy (optional)
- If the project uses schema-per-tenant (
django-tenants): never cross schemas; scope queries to active tenant.
- If not multi-tenant (e.g. OrbitQA MVP): still keep services/DTO boundaries; omit
tenant_id from DTOs unless needed later.
Models checklist
- Prefer UUID PKs on new business models.
created_at / updated_at on business entities.
- Never delete old migrations; add new ones.
- List endpoints:
select_related / prefetch_related; avoid per-row DB in SerializerMethodField (batch via serializer context).
Tests
- Prefer pytest + pytest-django.
- Test behaviors via services (and API smoke), not private helpers only.
- Mock adapters / external I/O in unit tests.
Anti-patterns
- Fat
perform_create with business rules.
- Serializers that call
Model.objects.create with domain branching.
- Services that import
rest_framework request objects.
- Views that launch Playwright or call OpenAI directly.
- Cross-app model imports for writes when a service exists.
- Unversioned new API routes.
Checklist (new endpoint)
More
- Concrete snippets: examples.md
1---2name: django-drf-layered-architecture3description: Implements and reviews Django + DRF backends using strict layered decoupling (views, serializers, services, DTOs, models, adapters), versioned /api/v1/ routes, and thin HTTP boundaries. Use when scaffolding Django apps, adding endpoints, writing services/DTOs, refactoring fat views, or starting projects like OrbitQA that follow Lumi-style architecture (multi-tenant optional).4---56# Django + DRF layered architecture78Portable rules distilled from Lumi ERP / Omayka backends. Apply to **new and existing** Django+DRF projects unless the repo explicitly overrides them.910## Layer dependency (strict)1112```text13HTTP → views (DRF) → serializers (I/O edge)14 → services (use cases)15 → dtos (typed contracts)16 → models (ORM)17 → adapters/* (external I/O: HTTP, browser, LLM, files)18```1920| Layer | Owns | Must not |21|-------|------|----------|22| **views** | Auth, status codes, call service, return Response | Business rules, ORM queries beyond get_queryset helpers, Playwright/LLM |23| **serializers** | Validate request/response; map ↔ DTO | Domain rules, calling other modules' models for decisions |24| **dtos** | Dataclass / TypedDict contracts in/out of services | Touch ORM or request |25| **services** | Transactions, validation, orchestration, emit events | Know DRF request; scrape UI; fat loops in serializers |26| **models** | Persistence, constraints, indexes | Decide multi-step workflows |27| **adapters** | External systems | Import views; leak HTTP details into services beyond DTOs |2829**Services receive and return DTOs**, not `request` and ideally not raw models at the public method boundary (map model → DTO inside the service).3031A service may call another service in the same app. **Never** import views from services. Cross-app: prefer DTOs, service APIs, or domain events — avoid importing another app's models for writes when a service exists.3233## API versioning3435- Expose new endpoints only under **`/api/v1/...`** (or the project's current version prefix).36- Do not add new clients to unversioned `/api/...` routes.3738## App layout (per module)3940```text41apps/<module>/42 models.py43 dtos.py44 services.py45 serializers.py46 views.py47 urls.py48 apps.py49 tests/50adapters/ # optional package at backend root51 <system>/ # e.g. lumi/, llm/, payments/52```5354Shared bits: `apps/common/` (`BaseDTO`, exceptions, EventBus helpers).5556## DTO patterns5758```python59from dataclasses import dataclass60from apps.common.dtos import BaseDTO6162@dataclass63class CreateAgentDTO(BaseDTO):64 name: str65 role: str6667@dataclass68class AgentDTO(BaseDTO):69 id: str70 name: str71 role: str72```7374- Serializers validate input → build DTO → `Service().method(dto)` → serialize DTO/dict out.75- Type-hint service public methods.7677## View pattern (thin)7879```python80class AgentViewSet(viewsets.ViewSet):81 def create(self, request):82 ser = CreateAgentSerializer(data=request.data)83 ser.is_valid(raise_exception=True)84 dto = CreateAgentDTO(**ser.validated_data)85 result = AgentService().create(dto)86 return Response(AgentSerializer(result).data, status=201)87```8889## Service pattern9091```python92class AgentService:93 @transaction.atomic94 def create(self, dto: CreateAgentDTO) -> AgentDTO:95 # validate + persist + map to AgentDTO96 ...97```9899## Adapters (external edges)100101- Browser automation, third-party HTTP, LLM, filesystem: live under `adapters/`.102- Services call adapters with DTOs; adapters return DTOs (`SkillResultDTO`, etc.).103- Keep selectors, SDK clients, and retries inside the adapter.104105## Multi-tenancy (optional)106107- If the project uses schema-per-tenant (`django-tenants`): never cross schemas; scope queries to active tenant.108- If **not** multi-tenant (e.g. OrbitQA MVP): still keep services/DTO boundaries; omit `tenant_id` from DTOs unless needed later.109110## Models checklist111112- Prefer UUID PKs on new business models.113- `created_at` / `updated_at` on business entities.114- Never delete old migrations; add new ones.115- List endpoints: `select_related` / `prefetch_related`; avoid per-row DB in `SerializerMethodField` (batch via `serializer context`).116117## Tests118119- Prefer pytest + pytest-django.120- Test **behaviors** via services (and API smoke), not private helpers only.121- Mock adapters / external I/O in unit tests.122123## Anti-patterns124125- Fat `perform_create` with business rules.126- Serializers that call `Model.objects.create` with domain branching.127- Services that import `rest_framework` request objects.128- Views that launch Playwright or call OpenAI directly.129- Cross-app model imports for writes when a service exists.130- Unversioned new API routes.131132## Checklist (new endpoint)133134- [ ] URL under `/api/v1/`135- [ ] Serializer validates only136- [ ] DTO defined137- [ ] Service method with types + transaction if needed138- [ ] View thin139- [ ] External I/O behind adapter140- [ ] Tests for happy path + main validation error141142## More143144- Concrete snippets: [examples.md](examples.md)