How do you structure a large-scale Django project modularly?
Django Developer
answer
A scalable Django project structure organizes code by domain apps and a thin service layer that encapsulates business rules outside views/models. Keep views I/O-only (HTTP/serialization), services orchestration-only, and models persistence-only. Extract cross-cutting code into reusable libraries (auth, payments, emails). Use settings modules per environment, typed configs, and strict boundaries (DTOs, repositories). Add Celery for async jobs, DRF for APIs, and contract tests to ensure maintainability.
Long Answer
A durable large-scale Django project balances clear domain boundaries, decoupled business logic, and operational guardrails. The aim is predictable change: teams can add features without breaking others; performance scales linearly; and production incidents are diagnosable.
1) High-level layout and boundaries
Organize code by domain apps rather than technical layers. Example repository:
/project
/config/ # settings, URLs, ASGI/WSGI, logging
/apps/
/accounts/ # users, orgs, roles, SSO
/billing/ # products, invoices, payments
/catalog/ # items, categories, search
/orders/ # carts, orders, fulfillment
/shared/ # cross-cutting libs (emails, storage, utils)
/interfaces/
api/ # DRF viewsets/serializers/routers
web/ # Django views/templates
admin/ # Django admin customizations
/services/ # orchestration layer (use-cases)
/tasks/ # Celery tasks using services
/schemas/ # pydantic/dataclasses DTOs
/infra/ # repositories, cache, external clients
Principle: views and serializers are adapters, not business logic. Services orchestrate domain behavior by calling repositories and domain methods; models remain persistence-centric and expressive, but not bloated with workflow code.
2) Apps and domain modeling
Keep each app cohesive: models, signals, domain services (within the app), and admin. Use bounded contexts (e.g., orders doesn’t reach into billing models directly). If data is needed across contexts, expose it via a query service or repository in infra/. Prefer explicit relations and through tables for rich associations. Add domain events (e.g., OrderPlaced) as simple dataclasses published from services; consumers (emails, analytics) subscribe via Celery or Django signals wrapped by a dispatcher to avoid tight coupling.
3) The service layer
Create use-case services in /services (or within apps for small teams). Each use case takes DTOs, not HttpRequest, and returns DTOs or result objects. Services enforce invariants, run transactions (@transaction.atomic), call repositories, and emit events. This separation enables CLI jobs, tasks, and APIs to reuse the same business logic.
4) Persistence and repositories
Keep ORM usage inside repositories (infra/repositories.py or per-app). Repositories return domain entities/DTOs, hide query details, and centralize performance tuning (select_related, prefetch_related, indexes, query hints). For reporting/read models, add query objects optimized for views; avoid leaking N+1 patterns into the interface layer.
5) APIs and serialization
For HTTP APIs, use Django REST Framework. Serializers convert DTOs/entities to wire formats; viewsets remain thin (auth, throttling, pagination). Version the API (/api/v1) and generate contracts (OpenAPI). For GraphQL, keep resolvers as adapters that delegate to the same services.
6) Asynchrony and workflows
Use Celery + Redis/RabbitMQ for async work (emails, invoices, exports). Tasks import services and are idempotent (idempotency keys stored in DB). For long workflows (refunds, fulfillment), implement a simple saga using a state table or a workflow engine; steps run in bounded transactions with compensations on failure.
7) Configuration, settings, and typing
Split settings (config/settings/base.py, dev.py, test.py, prod.py) and load secrets via env or a vault. Centralize logging, caches, and DBs in settings. Adopt mypy/pyright with stubs for Django/DRF; annotate services, repositories, and schemas for safer refactors. Validate settings at startup with pydantic-based config objects.
8) Security and validation
Enforce global security middleware (CSP, SecurityMiddleware, X-Frame, HSTS). Validate input at the edge (serializers/forms) and revalidate invariants in services. Implement RBAC/ABAC in accounts (permissions, object-level rules). Add problem-details error responses for APIs and consistent exception mapping in a DRF exception handler.
9) Performance & caching
Cache at multiple levels: CDN for assets, per-view/per-serializer caching, and cache-aside for hot queries (Redis). Use select_related/prefetch_related in repositories, add composite indexes via migrations, and keep denormalized read models for dashboards. For real-time features, add Django Channels with Redis/ASGI; keep channels adapters thin.
10) Observability & operations
Emit OpenTelemetry traces (ASGI, DB, Celery), structured logs with correlation/tenant IDs, and metrics (Prometheus/StatsD). Health, readiness, and metrics endpoints live in interfaces/admin/ops. Add SLOs (availability and p95 latency) and error-budget burn alerts. Blue-green or canary deployments plus expand–migrate–contract DB changes guard releases.
11) Testing strategy
- Unit: services with mocked repositories; repositories with a real DB.
- Contract: API schema tests, serializer compatibility, and event payload snapshots.
- Integration: end-to-end flows via API/client with test fixtures and factory_boy.
- Performance: query count and N+1 guards using pytest plugins.
Use pytest with transactional DB and a fast pytest-django setup.
12) Reuse and packaging
Extract cross-cutting packages (shared.mailer, shared.audit, shared.payments) into internal wheels or a shared/ app with clear APIs. Keep dependencies slim; isolate third-party clients in adapters so vendor swaps don’t touch domains.
This shape yields a Django project structure that scales: domains are cohesive, business logic is reusable, performance is tunable in one place, and operations are observable.
Table
Common Mistakes
- Fat views mixing HTTP, business rules, and ORM calls; impossible to reuse in tasks or CLI.
- Models overloaded with workflow logic/signals; side effects scattered and untestable.
- Cross-app imports (orders hitting billing models directly) instead of services or repositories.
- No transaction boundaries; partial writes under errors, duplicates without idempotency.
- N+1 queries from serializers; missing select_related/prefetch_related.
- One giant settings.py with hard-coded secrets; env drift and unsafe defaults in prod.
- Async jobs calling ORM across processes without idempotency or retries; ghost invoices.
- Lack of API versioning; breaking clients silently.
- No observability—just print logs; slow incident diagnosis.
- DB migrations that contract before readers change; outages during deploys.
Sample Answers
Junior:
“I’d split the project into domain apps (accounts, orders) and keep views thin. DRF serializers validate input, services do the work, and models stay clean. I’d use Celery for background jobs and separate settings for dev/test/prod.”
Mid:
“My Django project structure is domain-first with repositories and a service layer. Views/serializers call services; services run transactions and emit events. I add cache-aside for hotspots, DRF versioning, and pytest tests at unit, integration, and contract levels. Settings are env-based with secrets from env vars.”
Senior:
“I enforce boundaries: adapters (DRF/HTML/Channels), services with DTOs and idempotency, and repositories that own query performance. Cross-app comms go through domain events. Celery consumes services; workflows use sagas. We ship with OpenAPI contracts, canary deploys, expand–migrate–contract migrations, OTEL traces, and SLOs. This keeps the system modular and scalable.”
Evaluation Criteria
- Modularity: Domain apps with clear boundaries; no cross-app ORM access; adapters/services/repositories separation.
- Service layer: Use-cases with transactions, idempotency, and events; reusable by views, tasks, and CLI.
- Persistence: Repository pattern with tuned queries, indexes, and read models; no N+1 in serializers.
- APIs: DRF or GraphQL adapters are thin; versioning and OpenAPI contracts exist; consistent error handling.
- Async: Celery tasks are idempotent, small, and call services; long workflows modeled as sagas.
- Settings/Security: Environment-specific settings, secrets management, CSP/HSTS, RBAC/ABAC.
- Performance: Caching strategy, query optimization, and pagination defaults.
- Observability: OTEL traces, structured logs, metrics, SLOs, and burn-rate alerts.
- Delivery: Safe migrations, blue-green/canary, CI with unit/contract/integration/perf tests.
Red flags: Fat views/models, cross-app imports, unversioned APIs, no idempotency, monolithic settings, weak observability.
Preparation Tips
- Build a reference repo: config, apps/*, services, infra, interfaces/api. Add one feature end-to-end (create order) with DTOs, a service, repository queries, and DRF viewset.
- Add Celery; make the email/receipt flow idempotent. Simulate retries and verify no duplicates.
- Write tests: unit (service), repository (query count), contract (OpenAPI), and API integration. Add a perf test asserting max queries per endpoint.
- Introduce cache-aside for a hot listing; verify stale/refresh behavior.
- Split settings (dev/test/prod) and load secrets from env; validate with a typed config.
- Add OTEL tracing, JSON logs, and Prometheus metrics; create a dashboard with p95 latency and error rates.
- Practice expand–migrate–contract: add a column, backfill, switch reads, then drop.
- Document boundaries in a short ADR; define codeowners per app; add pre-commit (black, ruff, mypy).
Real-world Context
- Marketplace: Moving business logic from views into services cut per-request LOC by 35% and enabled Celery reuse. Repositories centralized select_related and indexes; p95 latency fell 27%.
- Fintech: Introduced idempotency + outbox in payment flows; duplicate charges dropped to zero under retries. API versioning with OpenAPI unlocked safe mobile releases.
- SaaS analytics: Extracted cross-cutting emails and billing into shared packages; teams shipped independently. Query objects fed cached read models; dashboards loaded in 1–2 queries.
- EdTech: Adopted typed settings and SLO dashboards; incident MTTR fell from 40→12 minutes. A staged migration avoided downtime while renaming columns across services. These shifts made the Django project structure easier to scale organizationally and technically.
Key Takeaways
- Structure by domain apps; keep views/adapters thin and logic in services.
- Use repositories to own queries and performance; avoid cross-app ORM.
- Make tasks idempotent; use sagas for long workflows.
- Version APIs; validate with contracts; secure with strong defaults.
- Observe with traces/logs/metrics and ship via safe migrations and canaries.
Practice Exercise
Scenario:
You’re inheriting a monolith where views perform business logic and direct ORM queries, Celery tasks duplicate work under retries, and releases cause downtime due to risky migrations. You must refactor towards a Django project structure that is modular, maintainable, and scalable—without stopping feature work.
Tasks:
- Introduce a service layer: pick two flows (checkout, password reset). Define DTOs, move logic into services/, wrap in @transaction.atomic, and emit domain events. Update views/DRF to call services only.
- Create repositories for orders and billing: add select_related/prefetch_related, composite indexes, and query-count tests. Replace direct ORM in views/serializers.
- Add idempotency to Celery tasks with a DB table keyed by operation; enforce at service entry. Prove safe retries.
- Split settings into base/dev/test/prod; load secrets from env; add typed validation.
- Add OpenAPI generation, API versioning, and a consistent exception handler returning problem-details.
- Wire OTEL tracing, JSON logs, and metrics; create an SLO dashboard (availability, p95 latency) and a burn-rate alert.
- Practice expand–migrate–contract by renaming a field: expand schema, dual-write, migrate readers, contract. Add a migration runbook.
- Document boundaries (apps, services, infra) in an ADR. Assign codeowners and pre-commit hooks (black, ruff, mypy).
Deliverable:
A short plan + PR series demonstrating refactored flows, repository adoption, idempotent tasks, safer settings, observable endpoints, and a zero-downtime migration—proving the path to a maintainable, scalable large-scale Django project.

