djangoplay-web / Architecture / DjangoPlay — Django App Design Pattern
DocsDjangoPlay WebArchitectureDjangoPlay — Django App Design Pattern

DjangoPlay — Django App Design Pattern

DjangoPlay follows a modular monolith architecture.

17 min readApplies to v1.2.2
On this page ▾
  1. 1. Overview
  2. Unit tests
  3. Service tests
  4. API tests
  5. Integration tests

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:

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.

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:

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.

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:

This is a conceptual lifecycle rather than a requirement that every request execute every layer.


22. Background Processing Lifecycle

A typical asynchronous workflow is:

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.