This guide covers the implementation of an DDD application (tactical design) in Django and with an emphasis on implementation variants Django developers are used to.
This guide does not cover the modelling of an DDD application (strategic design).
To learn more about strategic design I can highly recommend the guides and templates of the Domain-Driven Design Crew on GitHub.
The guide covers how to map strategic design into an implementation in the Django context however. To make this explicit corresponding sections are post-fixed with "(strategic pattern)".
The overall project type is important cause it implies the need for or unsuitability of patterns in a cross-cutting manner.
Context:
- You want to provide a web application to several user groups (e.g. related to different companies).
Example:
- SaaS provider of cloud computing resources for different companies (e.g. codesphere.com).
Context:
- You want to provide a web application to a single user group.
Examples:
- You are running a learning website for end users (e.g. udemy.com).
- Internal tool for data management in a single team within a company.
Context:
- You've little requirements w.r.t. security and data separation.
Context:
- You've a multi user groups use case and are working in a restricted domain with strict security and data separation requirements (e.g. healthcare, finance).
- You've a single user group use case.
Examples:
- You are provider of cloud resources in the finance domain for companies in the finance industry (e.g. metalstack.cloud).
- You are running a learning website for end users (e.g. udemy.com).
Core, generic, supporting.
Context:
- You've identified a high complexity subdomain with your domain experts and your companies market analysts which does differentiate you from competing companies and/or brings you ahead of a competing company.
Problem:
- What tactical DDD patterns should I use to implement a core domain?
Solution:
- In many cases core domains require an implementation with advanced patterns like Event Sourcing, CQRS,
Context:
- You've identified a low or medium complexity subdomain with your domain experts which does not differentiate you from competing companies.
Problem:
- Generic subdomains are already/should already be solved.
Solution:‚
- Search reusable Django apps. Good resources are e.g. Django Packages, best-of-django, wsvincent/awesome-django, shahraizali/awesome-django.
- Evaluate if the Django app solves your needs, integrate and customize it (e.g. packaged views) if necessary.
Resulting context (advantages):
- You depend on a third party package. You don't have to implement, test and maintain the subdomain yourself.
Resulting context (disadvantage):
- You depend on a third party package. The project could end in an unmaintained state.
Implementation examples:
- In many applications Djangos built-in authentication system is not sufficient. This subdomain can be implemented using the de-facto standard Django-app allauth.
- Djangos built-in support for emails is suitable for development only. The Django-app django-anymail can be used to abstract away email service provider specifics behind a unified API.
Context:
- You've applied domain modelling and came up with a model for a subdomain.
Problem:
- You don't know how to map the bounded context into an implementation in Django.
Context:
- The bounded context is rather simple.
Solution:
- Map the bounded context to a single Django app.
Example implementation:
src
|_ single_app_bounded_context
|_ admin.py
|_ models.py
|_ ...
|_ django_project
|_ settings
|_ base.py
|_ ...
|_ urls.py
|_ ...
|_ manage.py
|_ ...
Context:
- The bounded context is rather complex.
Solution:
- Map the bounded context to several Django apps.
Example implementation:
src
|_ multi_app_bounded_context
|_ app_1
|_ admin.py
|_ models.py
|_ ...
|_ app_2
|_ admin.py
|_ models.py
|_ ...
|_ django_project
|_ settings
|_ base.py
|_ ...
|_ urls.py
|_ ...
|_ manage.py
|_ ...
Context:
- Data should be private to a single bounded context.
- You want to persist data using Djangos built-in support for SQL databases (SQLite, Postgres, MySQL, ...).
Problem:
- Per-default Django uses a single, shared database. Bounded context specific data is not separated.
Solution:
- Prefix Django database tables (e.g. via corresponding Django models) with an application specific prefix.
Resulting context:
- It's obvious what data relates to what bounded context.
- Data is only "private by convention" (comparable with Python class "dunder" methods).
Context (additional):
- You use a database for persisting data with support for database schemas: Postgres, MySQL, ... (You don't use SQLite.)
Solution:
- Use a dedicated database schema (collection of tables) for a single bounded context.
Implementation examples:
Databases with built-in Django support:
- Postgres: schema
- MariaDB: ?
- MySQL "database"
- Oracle: ?
- SQLite: SQLite does not support collecting tables relating to a single bounded context in a schema in a database. https://stackoverflow.com/a/33962375/5308983
- In-memory SQLite: Same as for SQLite. The in-memory database does not support database schemas.
The same what applies to SQLite applies to quite a lot distributed, relational databases as well:
- rqlite github - rqlite/rqlite
- Fly.io LiteFS
- dqlite github - canonical/dqlite
- Turso libsql / github - tursodatabase/libsql
- Cloudflare D1
Solution:
- Use Djangos built-in support for multiple databases.
Example use case:
- Usually personal data is required to be handled with care. In many applications the user data of the authentication subdomain is persisted in a separate database.
Resulting context:
- It's obvious what data relates to what bounded context.
- Highest degree of data separation.
- Data can be separated physically.
- Offers most control w.r.t. developer side data access priviledges.
- Distributed SQLite: Paradigm shift or hype?: About super fast distributed SQLite, eventual consistency and why the blog auther sticks to Postgres.
- Emulate database schemas using SQLite
Context:
- The bounded context you want to model is no CRUD domain.
Solution:
- Add domain models (DTOs) for entities/aggregates (and value objects). One domain model for each entity/aggregate.
- Add Django ORM models for entities/aggregates. One Django ORM model for each entity/aggregate.
- Map fields of entities/aggregate from/to Django models.
Resulting context:
- Django ORM models are not used as domain models as well (Active Record pattern).
- Django ORM models depend on the domain models (dependency inversion).
- Django ORM models are not tightly coupled to data persistence implementations (in-memory, SQLite, Postgres, ...) supported by the Django ORM.
- Application code does not contain persistence specifics anymore.
Resulting context (advantages):
- Application code is decoupled from the data persistence implementation.
- No "fat" Django ORM models anymore.
Example implementation:
Further reading:
- The Repository Pattern: Article about an concrete product repository in Python and the impact on client code (in service) readability.
- Evolution of a Django Repository pattern: Django repository pattern implementation idea using Custom QuerySets as fluent API instead of many getter methods.
Context:
- Mapping between entities/aggregate DTOs and Django ORM models introduces lot of repetitive boilerplate.
Solution:
- Use a single
data = models.JSONField
in the Django model to wrap all entity/aggregate DTO data.
Resulting context (advantages):
- A generic repository can be used for several entities and aggregates.
- There is little need for SQL schema database migrations. Migrating JSON data can be easier.
- Works with noSQL databases as well.
- Writing entities/aggregates should be faster (when using a performant implementation making use of e.g.
pydantic
).
Resulting context (disadvantages):
- Benefits of the Django ORM w.r.t. development workflow (migrations, queries, ...) cannot be used anymore.
- Filtering data from JSONFields might be harder and slower.
Example implementations:
- Another way to persist DDD Aggregates in Django:
Little boilerplate Django ORM to/from
pydantic
entity/aggregate mapping by using amodels.JSONField
to wrap all aggregate/entity data.
Context:
- Data is mutated rather infrequently.
Problem:
- Every time data is requested from the repository it needs to be fetched from the database.
Solution:
- Cache the data when it's fetched after the first data mutation after cache invalidation.
- Invalidate the cache if data is mutated via the write part of the repository.
Resulting context (advantages):
- Cached data reads are way faster.
Resulting context (disadvantages):
- Latency for data fetches varies significantly.
- Data writes might be a bit slower.
Example implementations:
Context:
- You've a Online transaction processing (OLTP) use case (need for data manipulation like creation, update and deletes). CRUD use cases with moderate need for R.
Example:
- You want to create a web application for data management and mutation of time series data.
Implementation example:
- Implementation making use of QuestDB and Python Client for data persistency.
Context:
- You've a Online analytical processing (OLAP) use case (a lot of data, fast read performance, no need for data manipulation). CRUD use cases without CUD and high need for R.
Example:
- You want to create a web application dashboard for business analysis.
Implementation example:
- Implementation making use of TimescaleDB and
psycopg2
lib for data persistency. - Implementation making use of CrateDB and Python Client for data persistency.
- Architecture patterns with Python: A non-generic repository pattern and Unit of work pattern for Django.
- The repository pattern via CQRS with Python/Django/Elasticsearch: Compound repository pattern separates read and write part of a repository. Specification pattern for narrowing the interface of the repository
find
method. - Red Bird: Repositories (SQL -> SQLAlchemy, MongoDB, in-memory, CSV, ... ) for Python.
- .NET Entity Framework Core 8.0 DbContext: A DbContext instance represents a session with the database and can be used to query and save instances of your entities. DbContext is a combination of the Unit Of Work and Repository patterns.
- Spring Data: Provides repository and object-mapping abstractions and implementations (CrudRepository, PagingAndSortingRepository, ReactiveCrudRepository, ...) for the Java Spring framework.
- DAO vs Repository Patterns: A try to distinguish the DAO (Data Object) pattern from repository pattern.
Problem:
The interface of repository classes (abstract + concrete) tends to get broad.
Solution:
Keep the interface of the repository class interface and it's concrete implementations narrow.
Do not implement several get()
and get_all()
query methods or adding several paramaters to single get()
and get_all()
query methods.
Define a single get()
and get_all()
method with a single parameter specification
instead and pass the complete filter and sorting meta data via this parameter.
Inspiration for implementation variants:
- fractal-specifications: Implementation of the specification pattern with Django support for narrowing the repository pattern interface.
- Specification: Base class for adding specifications to a DDD model in C# .NET (EF6 and EF Core).
Keep an eye out for domain specific identifiers. In most cases it makes sense to use them to identify model instances or (aggregate root) entities in the domain model. Fallback to some kind of randomly generated ID format as fallback.
- Books are uniquely identified by International Standard Book Number (ISBN). The ISBN can be used as unique identifier in the e-commerce domain.
- Car models are uniquely identified by Vehicle Identification Number (VIN). The VIN can be used in domains where car models are relevant.
- ...
- uuid-utils: Wrapper for Rust implementations of
uuid1
,uuid2
,uuid3
,uuid4
,uuid5
,uuid6
,uuid7
anduuid8
. - py-nanoid: Python implementation of Nano IDs.
Further reading:
- UUIDs Are Bad for Database Index Performance, enter UUID7!: Insert performance (disk writes vs rows written ) comparison for SQLite, MariaDB (MySQL) and PostgreSQL (code).
- The Problem with Using a UUID Primary Key in MySQL: Bad insert performance. Higher storage utilization.
Integration between subdomains/bounded contexts involves the strategic pattern context mapping.
Solution:
- Bounded contexts are integrated in an ad hoc manner.
Context:
- Bounded contexts share a limited overlapping model.
Solution:
- The consumer conforms to the service provider’s model.
Solution:
- The consumer translates the service provider’s model into a model that fits the consumer’s needs.
Solution:
- The service provider implements a published language—a model optimized for its consumers’ needs.
Solution:
- Duplicate particular functionality (violate against DRY principle by intention) in several subdomains/bounded contexts.
For various functionalities like e.g. caching we depend on in-memory databases.
One first implementation is Redis which has been and is probably still the de-facto standard. For production we can choose from a variety of fully managed service providers.
- Redis Cloud
- Upstash for Redis (PaaS fly.io provides integration)
dragonfly is a drop-in replacement for Redis. Dragonfly is compatible with the Redis API. Dragonfly is easier to manage and seems to be way faster however. For local development you can install it as binary, use the Docker container e.g. with a Docker Compose file or in a local Kubernetes cluster with Helm Chart.
For production it's easiest to use the fully managed Dragonfly Cloud service.
- Decoding DDD: A Three-Tiered Approach to Django Projects - Lessons learned from refactoring a big learning management system (LMS) using DDD in a 3 level approach: level 0 (ubiquitous language, bounded contexts), level 1 (anemic domain model, value object, aggregate, domain service), level 2 (repository pattern).
- Saving Django Legacy Projects Using Domain-Driven Design: Article with diagrams and file structure.
- DjangoCon Video about DDD with GraphQL backend/frontend integration implementation
- DjangoCon video about integrating some DDD tactical patterns in OSIS, a long-term open-source project for UCLouvain university
- Introduction slides into DDD with Django using a ride sharing use case as example
- Intro slides into how to implement Domain Driven Design from the Université catholique de Louvain student management system maintainer
- Django API Domains
- django-ddd: Django the DDD way
- django-cqrs: Django application, that implements CQRS data synchronization between several Django micro-services
- django-outbox-pattern: Django application implementing the transactional outbox pattern.
- lily: DDD inspired microservice framework based on Django and DRF
- ddddjango: Slides on implemeting DDD in Django.
- ddd-python-django: Attempt to implement DDD and hexagonal architecture in Python using Django
-
django-th: A communication bus in Django.
-
falco: Skaffolding modern Django projects.
-
hydra: A django/htmx/alpine/tailwind project template.
-
django-microservices: An attempt to build microservices with Django.
-
django-jaiminho: A broker agnostic implementation of outbox and other message resilience patterns for Django apps.
-
Django + Vue + Vite: REST Not Required: An intro about cookiecutter-vue-django and how to use it to create API-less Django projects with vite + vue.
- sutoppu: Python specification pattern implementation
- eventsourcing: Eventsourcing in Python.
- py_assimilator: A collection of patterns (Repository, Unit of work, specification, specialization list, lazy command) for in-memory dict, Redis (in-memory), SQLAlchemy (SQL), MongoDB (NoSQL) databases.
- ddd-for-python: A domain-driven design framework using Sanic.
- python-ddd: FastAPI Domain-Driven-Design (DDD) Example.
- dddpy: FastAPI DDD example.
- Minos: Framework for creating reactive microservices in Python
- Building Python Microservices with FastAPI
- clean-code-python: Clean Code concepts (SRP, OCP, LSP, ISP, DIP, DRY) adapted for Python.
- dtm: A distributed transaction framework, supports workflow, saga, tcc, xa, 2-phase message, outbox patterns, supports many languages (including Python API).
- BullMQ: Message Queue and Batch processing for NodeJS and Python based on Redis.
- NSQ: Realtime distributed messaging (at least once, un-ordered) platform usable with Python client libs.
- NATS: Distributed messaging (at most once / at least once / exactly once) usable with Python client.
- awesome-ddd: A curated list of Domain-Driven Design (DDD), Command Query Responsibility Segregation (CQRS), Event Sourcing, and Event Storming resources.
- ddd-dynamic: Resources about Domain Driven Design in Python, Ruby and other dynamic languages.
- awesome-software-architecture
- MassTransit: Distributed Application Framework for .NET
- Apache RocketMQ: Financial grade transactional messages.
- Apache Pulsar: Distributed pub-sub messaging with strong ordering and consistency guarantees.
- DDD RE-DISTILLED: Slides summarizing the concepts contained in the "Domain-driven Design Distilled" book.