---
since: 1.2.1
---
# DjangoPlay — Django App Design Pattern
---
## 1. Overview
DjangoPlay follows a **modular monolith architecture**.
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:
```text
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.
```mermaid
flowchart TD
PLATFORM["DjangoPlay
Modular Monolith"]
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 external
```
A 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.
```text
DjangoPlay
│
├── Domain App A
│ ├── Models
│ ├── Services
│ ├── API
│ └── Tests
│
├── Domain App B
│ ├── Models
│ ├── Services
│ ├── API
│ └── Tests
│
└── Shared Platform Infrastructure
```
Applications 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:
```text
app/
├── models.py
├── views.py
├── urls.py
├── admin.py
├── apps.py
└── tests/
```
A more complex domain app may evolve into:
```text
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:
```mermaid
flowchart TD
REQUEST["HTTP / API Request"]
ROUTING["URL Routing"]
DELIVERY["Views / API Views"]
VALIDATION["Input Validation
Serializers / Forms"]
SERVICES["Application Services
Business Workflows"]
POLICIES["Policies / Permissions
Authorization"]
QUERIES["Selectors / Query Services
Complex Data Access"]
MODELS["Django Models
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 external
```
Not 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:
```text
Request
│
▼
View
│
▼
Service
│
▼
Response
```
Avoid:
```text
Request
│
▼
View
├── Business Rules
├── Database Workflow
├── External API Calls
├── Email Logic
├── Permission Rules
└── Background Processing
```
Thin 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:
```text
API Request
│
▼
Serializer
│
│ validated data
▼
Service
│
▼
Domain Operation
```
Business 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:
```text
SignupFlowService
SSOOnboardingService
ChatService
Issue workflow services
```
The 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:
```python
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:
```text
Signal
│
▼
Service / Task
│
▼
Business Operation
```
rather than:
```text
Signal
│
├── Database Workflow
├── External API
├── Email
├── File Processing
└── Long-running Logic
```
This 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:
```text
Application Service
│
▼
Celery Task
│
▼
Background Operation
```
Tasks should not become an alternative location for arbitrary business logic.
Where practical, the task should delegate domain behavior to an application
service.
```text
Celery Task
│
▼
Service
│
▼
Domain Operation
```
This 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:
```text
Request
│
▼
Service
│
▼
┌─────────────────────────┐
│ Database Transaction │
│ │
│ Operation A │
│ Operation B │
│ Operation C │
└─────────────────────────┘
│
▼
Commit
```
For work that must happen only after a successful database commit, Django's
transaction lifecycle should be used.
For example:
```text
Database Transaction
│
▼
COMMIT
│
▼
transaction.on_commit(...)
│
▼
External / Background Operation
```
This 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:
```text
Application Service
│
▼
Integration Layer
│
▼
External System
```
Application 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.
```mermaid
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 data
```
The 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:
```text
DjangoPlay-specific integration
│
▼
Generic Issue Tracker capability
```
---
# 17. Authentication Architecture
Authentication is separated from ordinary application domain logic.
DjangoPlay integrates with AuthX as part of its identity architecture.
Conceptually:
```text
Browser / API Client
│
▼
DjangoPlay
│
▼
AuthX
│
▼
Identity Data
```
Authentication-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.
```text
Client
│
▼
URL Router
│
▼
DRF View / ViewSet
│
▼
Serializer / Validation
│
▼
Service
│
├── Policy
├── Query
├── Model
└── Integration
│
▼
Response
```
---
# 19. API Versioning
DjangoPlay uses URL-based API versioning where versioned APIs are required.
Typical structure:
```text
/api/v1/...
/api/v2/...
```
A version may have its own:
* URL configuration
* Views
* Serializers
* API-specific behavior
For example:
```text
app/
└── api/
├── v1/
│ ├── urls.py
│ ├── views.py
│ └── serializers.py
│
└── v2/
├── urls.py
├── views.py
└── serializers.py
```
The 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:
```text
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:
```text
Project URLs
│
├── Web URLs
│
├── API URLs
│ │
│ ├── v1
│ └── v2
│
├── Authentication URLs
│
└── Application-specific URLs
```
Applications 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:
```mermaid
flowchart TD
CLIENT["Client"]
URL["URL Routing"]
MIDDLEWARE["Django Middleware
Request Context / Security"]
VIEW["Web View / DRF View"]
VALIDATION["Validation
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 data
```
This is a conceptual lifecycle rather than a requirement that every request
execute every layer.
---
# 22. Background Processing Lifecycle
A typical asynchronous workflow is:
```mermaid
flowchart TD
REQUEST["Web / API Request"]
SERVICE["Application Service"]
COMMIT["Database Commit"]
BROKER["Redis
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 external
```
Where 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:
```text
Views / API
│
▼
Services
│
├── Policies
├── Queries
├── Models
└── Integrations
```
Avoid:
```text
Model
│
└── calls View
Service
│
└── depends on HTTP response construction
```
The 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:
```text
DjangoPlay
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
Domain App A Domain App B Domain App C
│ │ │
└───────────────┼───────────────┘
▼
Shared Infrastructure
│
┌────────────────┼────────────────┐
▼ ▼ ▼
Authentication Logging Configuration
│
▼
AuthX
```
---
# 25. Testing Architecture
Testing should follow application boundaries.
A complex application service should be testable independently from the HTTP
delivery layer.
Typical testing layers include:
```text
Tests
│
┌───────────┼───────────┐
│ │ │
▼ ▼ ▼
Unit Tests Service API Tests
Tests
│ │ │
└───────────┼───────────┘
▼
Integration
Tests
```
Examples:
### 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:
```text
Simple Django App
│
▼
Domain Module
│
▼
Explicit Service Boundary
│
▼
Independent Runtime
│
▼
Service
```
Service 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:
```text
DjangoPlay
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
Web / API Services Events
│ │ │
│ ├──────┐ ▼
│ │ │ Celery
▼ ▼ ▼
Validation Policies Integrations
│ │ │
└────────────┼──────┘
▼
Models
│
▼
PostgreSQL
```
The architecture follows several core rules:
1. **Django applications represent meaningful domains or platform
capabilities.**
2. **Views remain thin.**
3. **Business workflows live in services when they become substantial.**
4. **Models represent persistence and domain state rather than entire
application workflows.**
5. **Selectors are introduced for complex or reusable query logic.**
6. **Authorization is enforced server-side through permissions and policies.**
7. **Signals remain lightweight and delegate substantial work elsewhere.**
8. **Celery handles asynchronous and long-running operations.**
9. **External integrations have explicit boundaries.**
10. **Transactions define consistency boundaries for related database
operations.**
11. **API versions represent external contracts rather than duplicated
business logic.**
12. **Architecture should be introduced progressively according to actual
complexity.**
13. **The modular monolith remains the default deployment architecture.**
14. **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**.