DjangoPlay — Django App Design Pattern
DjangoPlay follows a modular monolith architecture.
1. Overview
Each Django application represents a meaningful domain or platform capability, while the overall system remains deployed as a single Django application.
The architecture separates:
- HTTP and API delivery
- Domain and business logic
- Data access
- Authorization and policy enforcement
- Background processing
- External integrations
- Application configuration
- Events and signals
The objective is not to enforce an identical directory structure on every Django app.
Instead, DjangoPlay follows a set of architectural responsibilities that are applied according to the complexity and requirements of each app.
The general principle is:
HTTP / API Layer
│
▼
Application / Service Layer
│
├───────────────┐
▼ ▼
Data Access Policies
│ │
▼ │
Models ◄────────────┘
│
▼
PostgreSQL
Services / Events
│
▼
Celery
│
▼
External Systems
`2. Modular Monolith
DjangoPlay is organized as a collection of Django applications within a single deployable platform.
flowchart TD
PLATFORM["DjangoPlay<br/><b>Modular Monolith</b>"]
PLATFORM --> USERS["Users / Authentication"]
PLATFORM --> AICORE["AI / aicore"]
PLATFORM --> APIDOCS["API Documentation"]
PLATFORM --> ISSUES["Issue Tracker Integration"]
PLATFORM --> OTHER["Other Domain / Platform Apps"]
USERS --> SHARED["Shared Django Infrastructure"]
AICORE --> SHARED
APIDOCS --> SHARED
ISSUES --> SHARED
OTHER --> SHARED
SHARED --> DB["PostgreSQL"]
SHARED --> REDIS["Redis"]
SHARED --> CELERY["Celery"]
SHARED --> EXTERNAL["External Services"]
classDef platform fill:#e5f6f1,stroke:#159a7a,stroke-width:2px,color:#075d4b
classDef app fill:#e6f1fb,stroke:#2474b5,stroke-width:2px,color:#174d7a
classDef infra fill:#fff2d9,stroke:#d89b18,stroke-width:2px,color:#76520b
classDef external fill:#eeeeff,stroke:#5a55c9,stroke-width:2px,color:#403b91
class PLATFORM platform
class USERS,AICORE,APIDOCS,ISSUES,OTHER app
class SHARED,DB,REDIS,CELERY infra
class EXTERNAL externalA domain app should own its domain-specific behavior while using shared platform infrastructure where appropriate.
This provides domain separation without introducing network boundaries between every application.
3. Application Boundaries
A Django app should represent a meaningful domain or platform capability.
Examples within DjangoPlay include application areas such as:
- Users and authentication
- AI functionality
- API documentation
- Issue tracking integration
- Other domain-specific functionality
An application boundary should answer:
"Which part of the system owns this behavior?"
The boundary is primarily a code and responsibility boundary, not necessarily a deployment boundary.
DjangoPlay
│
├── Domain App A
│ ├── Models
│ ├── Services
│ ├── API
│ └── Tests
│
├── Domain App B
│ ├── Models
│ ├── Services
│ ├── API
│ └── Tests
│
└── Shared Platform InfrastructureApplications should avoid unnecessarily reaching into the internal implementation of another domain app.
Where cross-domain behavior is required, prefer an explicit service, integration, or well-defined interface.
4. Application Structure
There is no requirement for every Django app to contain every possible module.
A simple app may be:
app/
├── models.py
├── views.py
├── urls.py
├── admin.py
├── apps.py
└── tests/A more complex domain app may evolve into:
app/
├── models.py
├── admin.py
├── apps.py
├── urls.py
├── views.py
├── serializers.py
├── permissions.py
├── signals.py
├── tasks.py
│
├── services/
│ ├── __init__.py
│ ├── domain_service.py
│ └── workflow_service.py
│
├── selectors/
│ ├── __init__.py
│ └── query_service.py
│
├── policies/
│ ├── __init__.py
│ └── access_policy.py
│
├── api/
│ └── v1/
│ ├── urls.py
│ ├── views.py
│ └── serializers.py
│
└── tests/The second structure is a capability-based pattern, not a mandatory template.
Directories should be introduced when the complexity of the app justifies them.
Avoid creating empty abstraction layers simply to conform to a directory template.
5. Internal Application Architecture
The internal architecture can be represented as:
flowchart TD
REQUEST["HTTP / API Request"]
ROUTING["URL Routing"]
DELIVERY["Views / API Views"]
VALIDATION["Input Validation<br/>Serializers / Forms"]
SERVICES["Application Services<br/>Business Workflows"]
POLICIES["Policies / Permissions<br/>Authorization"]
QUERIES["Selectors / Query Services<br/>Complex Data Access"]
MODELS["Django Models<br/>Domain Persistence"]
DB["PostgreSQL"]
EVENTS["Signals / Domain Events"]
TASKS["Celery Tasks"]
EXTERNAL["External Services"]
RESPONSE["HTTP / API Response"]
REQUEST --> ROUTING
ROUTING --> DELIVERY
DELIVERY --> VALIDATION
VALIDATION --> SERVICES
SERVICES --> POLICIES
SERVICES --> QUERIES
QUERIES --> MODELS
SERVICES --> MODELS
MODELS --> DB
SERVICES --> EVENTS
EVENTS --> TASKS
SERVICES --> TASKS
TASKS --> EXTERNAL
SERVICES --> RESPONSE
classDef entry fill:#e6f1fb,stroke:#2474b5,stroke-width:2px,color:#174d7a
classDef application fill:#e5f6f1,stroke:#159a7a,stroke-width:2px,color:#075d4b
classDef data fill:#fff2d9,stroke:#d89b18,stroke-width:2px,color:#76520b
classDef external fill:#eeeeff,stroke:#5a55c9,stroke-width:2px,color:#403b91
class REQUEST,ROUTING,DELIVERY,VALIDATION,RESPONSE entry
class SERVICES,POLICIES,EVENTS,TASKS application
class QUERIES,MODELS,DB data
class EXTERNAL externalNot every request follows every box.
For example, a simple read endpoint may not require a service layer, while a complex workflow should generally be implemented through a service.
The architecture therefore defines responsibilities and dependency direction, rather than forcing every request through every layer.
6. Models
Django models represent persistent domain data.
Models are responsible for:
- Database fields
- Relationships
- Model-level constraints
- Simple validation
- Domain-relevant helper behavior
- Persistence-related behavior
- Timestamps and status fields where applicable
Models should not become the primary location for large application workflows.
For example, a model should not normally coordinate:
- Multiple external services
- Complex multi-step workflows
- Email delivery
- Long-running processing
- Cross-domain orchestration
Those responsibilities belong in application services or background tasks.
7. Views and API Views
Views are the delivery layer.
Their primary responsibilities are:
- Receive the request
- Resolve request context
- Invoke validation
- Enforce or delegate authorization
- Call application logic
- Construct the HTTP/API response
Views should remain as thin as practical.
Preferred:
Request
│
▼
View
│
▼
Service
│
▼
ResponseAvoid:
Request
│
▼
View
├── Business Rules
├── Database Workflow
├── External API Calls
├── Email Logic
├── Permission Rules
└── Background ProcessingThin views make the underlying application behavior easier to test and reuse.
8. Serializers and Input Validation
DjangoPlay uses serializers where the API requires structured request and response handling.
Serializers are responsible for concerns such as:
- Input validation
- Field validation
- Data transformation
- Representation of API responses
- Nested representations where required
Serializers should not become a substitute for the application service layer.
For example:
API Request
│
▼
Serializer
│
│ validated data
▼
Service
│
▼
Domain OperationBusiness workflows that span multiple objects, services, integrations, or transactions should remain outside the serializer.
9. Services
The service layer contains application and domain workflows that are too substantial for views, serializers, or models.
Services may coordinate:
- Business rules
- Transactions
- Multiple model operations
- Workflow transitions
- Cross-component operations
- External integrations
- Background task dispatch
- Notifications
- Audit/event generation
Typical examples include:
SignupFlowService
SSOOnboardingService
ChatService
Issue workflow servicesThe exact service names and locations depend on the domain.
The important principle is:
Complex application behavior should have an explicit home outside the HTTP delivery layer.
Services may call models directly for simple operations and may use selectors for reusable or complex read logic.
10. Selectors and Query Services
Selectors or query services provide reusable data-access operations.
They are particularly useful when query logic becomes:
- Complex
- Reused by multiple callers
- Performance-sensitive
- Difficult to express cleanly in a view
- Closely associated with a domain query
Example:
IssueQueryService.get_issues_for_user(user)
IssueQueryService.get_open_issues()Selectors are not mandatory for every query.
A simple query may remain directly in a service or view when doing so is clearer.
The purpose of selectors is query reuse and separation, not abstraction for its own sake.
11. Policies and Permissions
Authorization is a cross-cutting application concern.
DjangoPlay uses Django/DRF permission mechanisms together with application specific policy and access-control logic where required.
Policies may determine:
- Who can view an object
- Who can create an object
- Who can update an object
- Who can delete an object
- Role-based access
- Ownership
- Visibility
- Organization or domain restrictions
Authorization should not be implemented only in the UI.
A UI restriction is not a security boundary.
The effective authorization boundary must exist on the server.
For complex domains, authorization logic can be centralized in dedicated policy or access-control services.
12. Signals and Events
Signals provide event-driven integration points inside Django.
They are appropriate for events such as:
- Application lifecycle events
- Audit/event recording
- Notifications
- Triggering background processing
- Synchronizing related state
Signals should remain lightweight.
Avoid placing a large business workflow directly inside a signal handler.
Preferred:
Signal
│
▼
Service / Task
│
▼
Business Operationrather than:
Signal
│
├── Database Workflow
├── External API
├── Email
├── File Processing
└── Long-running LogicThis keeps event handling predictable and testable.
13. Celery Tasks
DjangoPlay uses Celery for asynchronous and background processing.
Tasks are appropriate for work such as:
- Email delivery
- Long-running processing
- File processing
- External API operations
- Synchronization
- Deferred workflows
The normal relationship is:
Application Service
│
▼
Celery Task
│
▼
Background OperationTasks should not become an alternative location for arbitrary business logic.
Where practical, the task should delegate domain behavior to an application service.
Celery Task
│
▼
Service
│
▼
Domain OperationThis allows the same business operation to be invoked from both synchronous and asynchronous execution paths.
14. Transaction Boundaries
Database transactions should be placed around meaningful business operations.
A service that performs multiple related database changes should establish an appropriate transaction boundary.
Conceptually:
Request
│
▼
Service
│
▼
┌─────────────────────────┐
│ Database Transaction │
│ │
│ Operation A │
│ Operation B │
│ Operation C │
└─────────────────────────┘
│
▼
CommitFor work that must happen only after a successful database commit, Django's transaction lifecycle should be used.
For example:
Database Transaction
│
▼
COMMIT
│
▼
transaction.on_commit(...)
│
▼
External / Background OperationThis prevents asynchronous or external side effects from being triggered for operations that ultimately roll back.
15. External Integrations
External systems should be treated as explicit integration boundaries.
DjangoPlay integrations can include:
- AuthX
- Email providers
- Cloudflare R2
- AI providers
- GenericIssueTracker
- Other external APIs
The preferred flow is:
Application Service
│
▼
Integration Layer
│
▼
External SystemApplication code should avoid scattering provider-specific API calls across views and models.
Where an integration is substantial, isolate its implementation behind a dedicated integration or service boundary.
16. GenericIssueTracker Integration
DjangoPlay integrates the reusable GenericIssueTracker package as an application capability.
It is not treated as a separate microservice.
flowchart TD
REQUEST["Issue Request"]
VIEW["Issue Views / DRF API"]
INTEGRATION["DjangoPlay IssueTracker Integration"]
TRACKER["GenericIssueTracker"]
ACCESS["Issue Access Control"]
DB["DjangoPlay PostgreSQL"]
EVENTS["Issue Events / Signals"]
TASKS["Celery Tasks"]
REQUEST --> VIEW
VIEW --> INTEGRATION
INTEGRATION --> ACCESS
ACCESS --> TRACKER
TRACKER --> DB
TRACKER --> EVENTS
EVENTS --> TASKS
classDef entry fill:#e6f1fb,stroke:#2474b5,stroke-width:2px,color:#174d7a
classDef app fill:#e5f6f1,stroke:#159a7a,stroke-width:2px,color:#075d4b
classDef data fill:#fff2d9,stroke:#d89b18,stroke-width:2px,color:#76520b
class REQUEST,VIEW entry
class INTEGRATION,TRACKER,ACCESS,EVENTS,TASKS app
class DB dataThe integration layer allows DjangoPlay to apply application-specific requirements such as:
- Identity resolution
- Visibility rules
- Allowed roles
- UI integration
- Helpdesk migration compatibility
- Application-specific access control
The reusable package remains responsible for its own issue-domain behavior.
This preserves a clean boundary between:
DjangoPlay-specific integration
│
▼
Generic Issue Tracker capability17. Authentication Architecture
Authentication is separated from ordinary application domain logic.
DjangoPlay integrates with AuthX as part of its identity architecture.
Conceptually:
Browser / API Client
│
▼
DjangoPlay
│
▼
AuthX
│
▼
Identity DataAuthentication-related application behavior may involve:
- Login
- Signup
- Password reset
- Email verification
- Social login / SSO
- Session handling
- Identity onboarding
DjangoPlay-specific onboarding and account workflows should remain in application services rather than being embedded directly inside views.
18. API Architecture
DjangoPlay exposes APIs through Django REST Framework.
The API layer is responsible for:
- Routing
- Request parsing
- Validation
- Authentication
- Authorization
- Serialization
- Response formatting
- API documentation
The API should delegate application behavior to domain services where the operation is non-trivial.
Client
│
▼
URL Router
│
▼
DRF View / ViewSet
│
▼
Serializer / Validation
│
▼
Service
│
├── Policy
├── Query
├── Model
└── Integration
│
▼
Response19. API Versioning
DjangoPlay uses URL-based API versioning where versioned APIs are required.
Typical structure:
/api/v1/...
/api/v2/...A version may have its own:
- URL configuration
- Views
- Serializers
- API-specific behavior
For example:
app/
└── api/
├── v1/
│ ├── urls.py
│ ├── views.py
│ └── serializers.py
│
└── v2/
├── urls.py
├── views.py
└── serializers.pyThe API version should describe the external contract.
Internal business logic should not be unnecessarily duplicated merely because the API version changed.
A preferred structure is:
v1 View ──┐
├──► Shared Service ──► Domain
v2 View ──┘when both versions can safely use the same business behavior.
When the external contract requires different behavior, version-specific services or adapters may be introduced.
20. API Routing
Project-level routing establishes the top-level URL structure.
Conceptually:
Project URLs
│
├── Web URLs
│
├── API URLs
│ │
│ ├── v1
│ └── v2
│
├── Authentication URLs
│
└── Application-specific URLsApplications should own their domain-specific URL configuration where practical.
This prevents the project-level URL configuration from becoming a large collection of application implementation details.
21. Request Lifecycle
A typical DjangoPlay request can be represented as:
flowchart TD
CLIENT["Client"]
URL["URL Routing"]
MIDDLEWARE["Django Middleware<br/>Request Context / Security"]
VIEW["Web View / DRF View"]
VALIDATION["Validation<br/>Serializer / Form"]
AUTH["Authentication / Authorization"]
SERVICE["Application Service"]
QUERY["Selector / Query"]
MODEL["Django Model"]
DB["PostgreSQL"]
RESPONSE["HTTP Response"]
CLIENT --> URL
URL --> MIDDLEWARE
MIDDLEWARE --> VIEW
VIEW --> VALIDATION
VALIDATION --> AUTH
AUTH --> SERVICE
SERVICE --> QUERY
QUERY --> MODEL
SERVICE --> MODEL
MODEL --> DB
DB --> SERVICE
SERVICE --> RESPONSE
classDef entry fill:#e6f1fb,stroke:#2474b5,stroke-width:2px,color:#174d7a
classDef application fill:#e5f6f1,stroke:#159a7a,stroke-width:2px,color:#075d4b
classDef data fill:#fff2d9,stroke:#d89b18,stroke-width:2px,color:#76520b
class CLIENT,URL,MIDDLEWARE,VIEW,VALIDATION,RESPONSE entry
class AUTH,SERVICE application
class QUERY,MODEL,DB dataThis is a conceptual lifecycle rather than a requirement that every request execute every layer.
22. Background Processing Lifecycle
A typical asynchronous workflow is:
flowchart TD
REQUEST["Web / API Request"]
SERVICE["Application Service"]
COMMIT["Database Commit"]
BROKER["Redis<br/>Celery Broker"]
WORKER["Celery Worker"]
TASK["Background Task"]
EXTERNAL["External Service"]
REQUEST --> SERVICE
SERVICE --> COMMIT
COMMIT --> BROKER
BROKER --> WORKER
WORKER --> TASK
TASK --> EXTERNAL
classDef entry fill:#e6f1fb,stroke:#2474b5,stroke-width:2px,color:#174d7a
classDef app fill:#e5f6f1,stroke:#159a7a,stroke-width:2px,color:#075d4b
classDef infra fill:#fff2d9,stroke:#d89b18,stroke-width:2px,color:#76520b
classDef external fill:#eeeeff,stroke:#5a55c9,stroke-width:2px,color:#403b91
class REQUEST entry
class SERVICE,WORKER,TASK app
class COMMIT,BROKER infra
class EXTERNAL externalWhere a task depends on committed database state, dispatch should occur after the transaction has successfully committed.
23. Dependency Direction
DjangoPlay favors dependencies flowing toward application/domain behavior rather than allowing the HTTP layer to become the center of the system.
Preferred:
Views / API
│
▼
Services
│
├── Policies
├── Queries
├── Models
└── IntegrationsAvoid:
Model
│
└── calls View
Service
│
└── depends on HTTP response constructionThe application layer should remain usable independently of a particular HTTP endpoint wherever practical.
This is especially valuable for:
- Unit testing
- Celery execution
- Management commands
- CLI workflows
- Future API versions
- Administrative operations
24. Cross-Cutting Concerns
Some concerns apply across multiple applications.
These include:
- Authentication
- Authorization
- Logging
- Configuration
- Security
- Caching
- Background processing
- Error handling
- Audit/event recording
Cross-cutting concerns should be implemented through appropriate shared infrastructure rather than duplicated independently inside every app.
Conceptually:
DjangoPlay
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
Domain App A Domain App B Domain App C
│ │ │
└───────────────┼───────────────┘
▼
Shared Infrastructure
│
┌────────────────┼────────────────┐
▼ ▼ ▼
Authentication Logging Configuration
│
▼
AuthX25. Testing Architecture
Testing should follow application boundaries.
A complex application service should be testable independently from the HTTP delivery layer.
Typical testing layers include:
Tests
│
┌───────────┼───────────┐
│ │ │
▼ ▼ ▼
Unit Tests Service API Tests
Tests
│ │ │
└───────────┼───────────┘
▼
Integration
TestsExamples:
Unit tests
Test isolated:
- Business rules
- Validators
- Policies
- Utility functions
Service tests
Test:
- Workflows
- Transactions
- Domain operations
- Cross-model behavior
API tests
Test:
- Routing
- Authentication
- Permissions
- Request validation
- Response contracts
Integration tests
Test:
- External service boundaries
- Celery workflows
- Database behavior
- Application integrations
The exact test structure can vary by application.
26. When to Introduce an Abstraction
DjangoPlay does not require every application to use every architectural pattern.
Introduce a service when:
- Business logic is becoming substantial
- Multiple entry points need the same operation
- A workflow spans multiple models
- The operation requires a transaction boundary
- External integrations are involved
Introduce a selector when:
- Query logic is complex
- The query is reused
- Query performance needs dedicated attention
Introduce a policy when:
- Authorization rules are complex
- The same access rule is reused
- Visibility logic needs centralization
Introduce a task when:
- Work does not need to block the request
- The operation is long-running
- External I/O should be asynchronous
- Retry behavior is required
Introduce a dedicated integration layer when:
- Provider-specific behavior is significant
- An external API is used from multiple locations
- Retry/error handling needs isolation
- Provider replacement is a realistic requirement
The principle is:
Add an architectural boundary when it solves a real complexity problem.
27. What the Architecture Does Not Require
DjangoPlay does not require:
- Every app to have
services/ - Every app to have
selectors/ - Every app to have
policies/ - Every app to have Celery tasks
- Every API endpoint to use a service
- Every query to use a selector
- Every model operation to be wrapped in a service
- Every application to become a microservice
The architecture is intentionally progressive.
Simple code should remain simple.
Complex behavior should receive the appropriate architectural boundary.
28. Modular Monolith and Future Extraction
The modular-monolith architecture does not prevent future service extraction.
If a domain eventually requires:
- Independent deployment
- Independent scaling
- Independent data ownership
- Separate operational ownership
- Different availability requirements
then that domain can be evaluated for extraction.
The progression should generally be:
Simple Django App
│
▼
Domain Module
│
▼
Explicit Service Boundary
│
▼
Independent Runtime
│
▼
ServiceService extraction is therefore an architectural option rather than a requirement.
The internal boundaries established inside DjangoPlay make selective extraction easier if it becomes necessary.
29. Application Architecture Principles
| Principle | DjangoPlay Approach |
|---|---|
| Architecture | Modular monolith |
| Domain boundaries | Django applications |
| HTTP layer | Thin views / API views |
| Business logic | Application/domain services |
| Data access | Models + selectors/query services where useful |
| Authorization | Django/DRF permissions + application policies |
| Events | Django signals / application events |
| Background work | Celery |
| Broker / cache | Redis |
| Persistence | PostgreSQL |
| External systems | Explicit integration boundaries |
| API framework | Django REST Framework |
| API evolution | Versioned API contracts where required |
| Testing | Unit, service, API, and integration testing |
| Deployment | Single Django application with separately scalable runtime workloads |
| Service extraction | Selective, requirement-driven |
30. Summary
DjangoPlay's application architecture is based on a modular monolith with clear internal responsibility boundaries.
The important boundaries are:
DjangoPlay
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
Web / API Services Events
│ │ │
│ ├──────┐ ▼
│ │ │ Celery
▼ ▼ ▼
Validation Policies Integrations
│ │ │
└────────────┼──────┘
▼
Models
│
▼
PostgreSQLThe architecture follows several core rules:
- Django applications represent meaningful domains or platform capabilities.
- Views remain thin.
- Business workflows live in services when they become substantial.
- Models represent persistence and domain state rather than entire application workflows.
- Selectors are introduced for complex or reusable query logic.
- Authorization is enforced server-side through permissions and policies.
- Signals remain lightweight and delegate substantial work elsewhere.
- Celery handles asynchronous and long-running operations.
- External integrations have explicit boundaries.
- Transactions define consistency boundaries for related database operations.
- API versions represent external contracts rather than duplicated business logic.
- Architecture should be introduced progressively according to actual complexity.
- The modular monolith remains the default deployment architecture.
- Independent service extraction is reserved for domains with a concrete operational or scaling requirement.
The goal is not maximum abstraction.
The goal is clear ownership, predictable dependency direction, testable business behavior, and an architecture that can grow without forcing unnecessary distributed-system complexity.