Skip to content

dvf/opinionated-django

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

op-django

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.

A Layered Approach Using Encapsulation

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

DX

Concern Tool
Packaging uv
Settings python-decouple
Linting & formatting ruff
Type checking pyrefly · django-stubs
Testing pytest · pytest-django · pytest-celery · freezegun

Install

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-lint

Your agent will pick them up automatically on its next run. You can also clone the repo and point your agent at skills/ directly.

image

The Skills

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.

🏗️ dj-scaffold

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.

🔑 dj-prefixed-ulids

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.

🧩 dj-services

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.

📐 dj-models

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).

🏛️ dj-architecture

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.

📡 dj-signals

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.

⚙️ dj-settings

Keeps settings.py organized with banner-style section headers in a predictable order. Use whenever settings are added, removed, or restructured.

🧪 dj-pytest

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.

dj-lint

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.

The Patterns at a Glance

  • ModelsMeta first (verbose names, indexes, constraints). Fields ordered: identifiers → time → status → domain → relations. Uniqueness via UniqueConstraint, indexes via Meta.indexes — both optimized for actual query patterns. No business logic, no custom save(), no properties that compute. Multi-word fields get verbose_name, obscure fields get help_text.
  • DTOs — Pydantic v2 with from_attributes=True. All IDs are str. ORM objects never leave the repository.
  • Repositories — The only layer that touches the ORM. Returns DTOs. @transaction.atomic for 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 are ninja.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-decouple for env vars.

Example Project

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.

License

MIT. Use them, fork them, make them yours.

About

An opinionated Django project with Repository pattern, Pydantic DTOs, svcs DI, and Stripe-style ULID IDs

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages