--- 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**.