3 pointsby vanflymen2 hours ago1 comment
  • vanflymen2 hours ago
    I've been building in Django for a long time and I love the framework, but some things have always bugged me:

    - Typing is exceptionally painful...and getting proper type safety across a Django project is a fight

    - the ORM leaks throughout your project: it's easy to chain ORM methods in a view, a serializer, a template tag, a Celery task, etc and N+1s end up being difficult to find

    - models become god objects: custom managers, save overrides, signal handlers, computed properties, validation logic, your models attract everything until they're 500 lines of tangled responsibilities.

    - tests become slow over time because you either hit the DB for everything or mock too aggressively: this is because there's no clear encapsulated layers.

    and a ton of other stuff including aesthetics...

    Anyway, the short version of my solution is to push all ORM work into repositories that return Pydantic DTOs. Services get repos via constructor injection and contain pure business logic with zero model imports. Your views become one-liners. Every layer boundary is typed, and each layer is independently testable.

    Some of the opinions:

    - Prefixed ULID primary keys: double-clickable Stripe-style IDs like ord_01jq3v... on every model.

    - Models are dumb but aesthetic: Meta first, fields in a strict order (identifiers, times, status, domain, relations), all indexes and constraints declared explicitly in Meta, zero business logic.

    - Repositories return DTOs, never querysets so the ORM never leaks past this layer. This is where 90% of Django's typing hassles end.

    - Services own the business logic injected via https://svcs.hynek.me, tested without a database by mocking the repo. This makes tests super fast and composable.

    - Thin django-ninja routes give you input validation and OpenAPI for free, handlers are just calling services get(MyService).do_thing() in a DI-esque way.

    - Reliable signals via Celery side-effects enqueued inside the DB transaction so rollbacks are respected. This gets us pretty close to a DBOS or Temporal. Adapted from https://hakibenita.com/django-reliable-signals.

    Three test layers the repos test against a real DB, service against mocked repos, API through mocked services. This keeps the tests fast and clean.

    Disclaimer: These are super opinionated. They won't be everyone's cup of tea, and that's fine. But they've held up well on large-scale projects and I thought they might be useful to others who are looking for a clean way to structure Django beyond the "everything in views.py" stage.