A collection of Agent Skills that give a coding agent architectural skills to build scalable, maintainable Django projects with clean separation of concerns, testability, and a full suite of best practices.
I love Django, but some parts of it are genuinely hard to type: querysets, model instances, related managers, F()/
Q() expressions, etc. In my experience, the cleanest way to handle this is to keep all the ORM work behind a
repository layer that returns Pydantic DTOs. Your business logic lives in services that never import a model. Your views
become one-liners, and each layer becomes very easy to mock and test.
After discussions with my friends Haki Benita and Pete Nilson, this is what I believe to be a reasonable set of patterns for flexible, testable Django projects. They're opinionated, but hold up well on large scale projects in the wild.
Each layer encapsulates the one beneath it. The API layer never touches the ORM. Services never import a model. Repositories never leak a queryset or a model instance. Every boundary between layers is crossed as a typed Pydantic DTO, so changes stay local and tests stay fast.
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ API │──▶│ Service │──▶│ DTO │──▶│ Repo │──▶│ Model │
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
thin views business typed data ORM lives
logic + DI at boundaries here only
| Layer | Role | Library |
|---|---|---|
| API | Routing, input validation, OpenAPI | django-ninja |
| Service | Business logic & orchestration with true DI | svcs |
| DTO | Typed data at every layer boundary | Pydantic v2 |
| Repository | All ORM access, transactions, prefetches | Django |
| Model | Persistence with prefixed ULID primary keys | Django · python-ulid |
| Async | Reliable signals & background tasks | Celery |
| Concern | Tool |
|---|---|
| Packaging | uv |
| Settings | python-decouple |
| Linting & formatting | ruff |
| Type checking | pyrefly · django-stubs |
| Testing | pytest · pytest-django · pytest-celery · freezegun |
Skills install with a single command:
# The whole collection
npx skills add dvf/opinionated-django
# Or just one
npx skills add dvf/opinionated-django/dj-scaffold|dj-architecture|dj-models|dj-services|dj-prefixed-ulids|dj-signals|dj-settings|dj-lintYour agent will pick them up automatically on its next run. You can also clone the repo and point your agent at
skills/ directly.
Each skill is a directory under skills/ with its own SKILL.md. They're designed to stand alone, but they compose
nicely — dj-scaffold lays the foundation, dj-architecture builds features on top, and the rest fill in the details.
Sets up a new (or existing) Django project into the op-django layout. Creates the src/project/ shell — ids.py,
services.py, api.py, a ReliableSignal base, Celery wiring — installs dependencies with uv, and lays down ruff /
pyrefly / pytest config. Run this first.
Stripe-style prefixed ULID primary keys for every model (prd_01jq3v..., ord_01jq3v...). Time-sortable, safe to
expose in URLs, debuggable in logs, and str end-to-end so there's no UUID/str coercion between layers.
Plain service classes with constructor-injected repositories, wired through an svcs registry.
Business logic lives here, zero ORM imports allowed. Resolve anywhere — views, Celery tasks, management commands,
tests — with a single generic get[T]() call.
Structures Django models with a strict internal layout: Meta first (verbose names, indexes, constraints), then fields
grouped as identifiers → time → status → domain → relations. Uniqueness lives in Meta.constraints (never
unique=True), indexes in Meta.indexes (never db_index=True) — both optimized for the queries the repository
actually runs. Multi-word fields get verbose_name, obscure fields get help_text. Every model is registered in the
admin with a clean, fast-loading config (list_per_page = 25, raw_id_fields for large FKs, extra = 0 on inlines).
The full feature blueprint. Given a description, the agent scaffolds models, Pydantic DTOs, repositories, services, django-ninja routes, admin registration, and three layers of tests (repo against a real DB, service against mocked repos, API through HTTP). Every convention is spelled out; every layer is non-negotiable.
Reliable signals for async side-effects — notifications, cache invalidation, analytics, cross-service coordination. Receivers are enqueued inside the database transaction via Celery, so rollbacks are respected and delivery is at-least-once. Pattern adapted from Haki Benita's Reliable Signals in Django.
Keeps settings.py organized with banner-style section headers in a predictable order. Use whenever settings are added,
removed, or restructured.
Three-layer pytest setup — repo against a real DB, service against mocked repos, API through HTTP — with
pytest-django, pytest-celery for reliable-signal receivers, freezegun for time-sensitive logic, and a project
conftest.py full of DTO factories and svcs-override fixtures.
Runs ruff check, ruff format --check, and pyrefly check, then fixes whatever it finds. Use before committing, or
any time you want a clean bill of health.
- Models —
Metafirst (verbose names, indexes, constraints). Fields ordered: identifiers → time → status → domain → relations. Uniqueness viaUniqueConstraint, indexes viaMeta.indexes— both optimized for actual query patterns. No business logic, no customsave(), no properties that compute. Multi-word fields getverbose_name, obscure fields gethelp_text. - DTOs — Pydantic v2 with
from_attributes=True. All IDs arestr. ORM objects never leave the repository. - Repositories — The only layer that touches the ORM. Returns DTOs.
@transaction.atomicfor multi-writes. One repo per aggregate root. - Services — Receives dependencies via
__init__. Pure business logic. Zero ORM imports. Testable without a database. - API — django-ninja routes centralized in
project/api.py. Input schemas areninja.Schema, output schemas reuse DTOs. - Reliable Signals — Side-effects enqueued inside the DB transaction via Celery. At-least-once delivery. Idempotent receivers.
- Settings — Sectioned with banner headers.
python-decouplefor env vars.
See example_project for a working Django project built with these patterns — two apps (
products, orders), full repository + service + API layering, and tests at all three levels.
MIT. Use them, fork them, make them yours.