Django Implementation Guide¶
The layer concepts are framework-agnostic; this page covers the Django-specific details.
Project layout¶
GLIMPSE layers live alongside the Django project package at the root of the repository:
myproject/
├── pacts/
├── specs/
├── mills/
├── links/
│ └── db/
│ └── django/ # ORM models + repository implementations
├── gates/
│ ├── web/
│ │ └── django/ # views, forms, URL configurations
│ └── cli/
│ └── django/ # management commands
├── inits/ # DI container + middleware
└── edges/
├── settings/
├── wsgi.py
└── asgi.py
The Django project package (containing settings.py originally) moves into edges/.
links — ORM models and repositories¶
Each entity gets its own file in links/db/django/:
# links/db/django/proposal.py
from django.db import models
from pacts.proposals import ProposalDTO, ProposalRepositoryProtocol
class Proposal(models.Model):
title = models.CharField(max_length=255)
author_id = models.IntegerField()
class Meta:
db_table = "proposals"
class ProposalRepository:
def __init__(self, storage):
self._storage = storage
def get(self, pk: int) -> ProposalDTO:
if cached := self._storage.proposals.get(pk):
return cached
orm = Proposal.objects.get(pk=pk)
dto = ProposalDTO.model_validate(orm)
self._storage.proposals[pk] = dto
return dto
gates/web/django — views and forms¶
Views type the request as RootRequestProtocol and access data through request.di.uow:
# gates/web/django/proposals.py
from django.shortcuts import render
from pacts.core import RootRequestProtocol
def detail(request: RootRequestProtocol, pk: int):
proposal = request.di.uow.proposals.get(pk)
return render(request, "proposals/detail.html", {"proposal": proposal})
URL patterns live in the same file or a urls.py alongside:
from django.urls import path
from . import proposals
urlpatterns = [
path("<int:pk>/", proposals.detail, name="proposal-detail"),
]
gates/cli/django — management commands¶
Management commands live in gates/cli/django/. They follow standard Django management command structure but type their dependencies through pacts protocols.
inits — DI container and middleware¶
inits constructs the UoW and attaches it to the request:
# inits/container.py
from functools import cached_property
from links.db.django.proposal import ProposalRepository
from links.db.django.user import UserRepository
class UnitOfWork:
def __init__(self):
self._storage = Storage()
@cached_property
def proposals(self) -> ProposalRepository:
return ProposalRepository(self._storage)
@cached_property
def users(self) -> UserRepository:
return UserRepository(self._storage)
def atomic(self):
from django.db import transaction
return transaction.atomic()
class DI:
@cached_property
def uow(self) -> UnitOfWork:
return UnitOfWork()
class DIMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.di = DI()
return self.get_response(request)
Register the middleware in edges/settings/base.py:
RootRequestProtocol¶
Define the protocol in pacts so that gates and mills can reference the DI container without importing from inits:
# pacts/core.py
from typing import Protocol
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from inits.container import UnitOfWork
class DIProtocol(Protocol):
uow: "UnitOfWork"
class RootRequestProtocol(Protocol):
di: DIProtocol
user: ...
method: str
POST: ...
GET: ...
Multi-repo writes¶
Wrap writes that span multiple repositories in uow.atomic():
# mills/proposals.py
class ProposalService:
def __init__(self, uow: UnitOfWorkProtocol) -> None:
self._uow = uow
def publish(self, proposal_id: int) -> None:
with self._uow.atomic():
proposal = self._uow.proposals.get(proposal_id)
event = DomainEvent(type="proposal.published", entity_id=proposal_id)
self._uow.proposals.mark_published(proposal_id)
self._uow.events.record(event)
Import linter¶
See the Import Linter guide for the pyproject.toml configuration that enforces GLIMPSE layer boundaries.