Skip to content

flango-dev/pattern-language

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pattern-language

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)".

Project type patterns

project type patterns

The overall project type is important cause it implies the need for or unsuitability of patterns in a cross-cutting manner.

Multi user groups

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

Single user group

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.

Multi-tenancy

Context:

  • You've little requirements w.r.t. security and data separation.

Single tenancy

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

Subdomain (strategic pattern)

Core, generic, supporting.

Core subdomain (stategic pattern)

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,

Generic subdomain (strategic pattern)

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:‚

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.

Bounded context (strategic pattern)

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.

bounded context patterns

Django single app bounded context

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

Django multi app bounded context

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

Django bounded context data separation patterns

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.

Bounded context specific database table namespace

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

Bounded context specific database schema

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:

Bounded context specific database

Solution:

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.

Further reading

Further reading

Repository patterns

Django ORM repository

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:

JSON field repository

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:

Cached repository pattern

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:

Time series data repository patterns

time series data repository patterns

OLTP use cases

Context:

Example:

  • You want to create a web application for data management and mutation of time series data.

Implementation example:

OLAP use cases

Context:

Example:

  • You want to create a web application dashboard for business analysis.

Implementation example:

Inspiration

Inspiration

Repository specification 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:

Unique identifier alternatives

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.

Domain specific unique identifiers

Random ID formats

  • uuid-utils: Wrapper for Rust implementations of uuid1, uuid2, uuid3, uuid4, uuid5, uuid6, uuid7 and uuid8.
  • py-nanoid: Python implementation of Nano IDs.

Further reading:

Bounded context integration patterns

Context mapping (stategic pattern)

Integration between subdomains/bounded contexts involves the strategic pattern context mapping.

ddd-crew/context-mapping

Partnership

Solution:

  • Bounded contexts are integrated in an ad hoc manner.

Shared kernel

Context:

  • Bounded contexts share a limited overlapping model.

Conformist

Solution:

  • The consumer conforms to the service provider’s model.

Anticorruption layer

Solution:

  • The consumer translates the service provider’s model into a model that fits the consumer’s needs.

Open-host service

Solution:

  • The service provider implements a published language—a model optimized for its consumers’ needs.

Separate ways

Solution:

  • Duplicate particular functionality (violate against DRY principle by intention) in several subdomains/bounded contexts.

Further reading

In-memory database

For various functionalities like e.g. caching we depend on in-memory databases.

Redis

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.

Dragonfly

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.

Inspiration

Django specific (DDD)

Django specific (general)

Python specific

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

Generic DDD

  • 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

Other programming languages

  • MassTransit: Distributed Application Framework for .NET
  • Apache RocketMQ: Financial grade transactional messages.
  • Apache Pulsar: Distributed pub-sub messaging with strong ordering and consistency guarantees.

Deployment

Stategic DDD

  • DDD RE-DISTILLED: Slides summarizing the concepts contained in the "Domain-driven Design Distilled" book.