Skip to main content

Domain Driven Design Library

Project description

DDDesign

pypi downloads versions codecov license

DDDesign is a Python library designed to implement Domain-Driven Design (DDD) principles in software projects. It provides a set of tools and structures to help developers apply DDD architecture. This library is built on top of Pydantic, ensuring data validation and settings management using Python type annotations.

Installation

Install the library using pip:

pip install dddesign

DDD Components

Application

Application is a programmatic interface for accessing business logic. It serves as the primary entry point for all domain operations.

Entry points:

  • Ports: HTTP interfaces, background tasks, CLI commands, and other interaction points.
  • Other Applications: Within the same Context, leading and subordinate Applications may coexist, creating a layered structure. Leading Applications manage core business logic, while subordinate Applications handle narrower delegated tasks.

This approach ensures clear separation of responsibilities and helps maintain a well-organized architecture.

Adapter

Adapter is a component designed to retrieve data from external sources. Based on the Adapter Pattern described in "Gang of Four" (GoF), it bridges the interface of a system with the one expected by another.

Characteristics:

  • Belongs to the infrastructure layer and isolates interactions with external interfaces.
  • Divided into integration with internal (InternalAdapter) and third-party (ExternalAdapter) services.

By encapsulating external dependencies, Adapter keeps the core application logic decoupled and modular.

Repository

Repository, a subtype of Adapter, is a specialized infrastructure layer component introduced in "Domain-Driven Design" by Eric Evans. It isolates interactions with data storage systems (e.g., PostgreSQL, ClickHouse, Redis, and others) and provides an abstraction for managing persistence.

Characteristics:

  • Single Responsibility: Each Repository is typically designed to work with a single table (Entity).
  • Separation of Concerns: Keeps domain logic independent of storage implementations.
  • Application Usage: If an Application uses more than one Repository, this indicates a design issue. In such cases, consider creating another Application.

This abstraction ensures that persistence logic is modular and aligns with DDD principles.

Service

Service is used to handle business logic not tied to a specific domain object.

Characteristics:

  • Purpose: Used when a method spans multiple domain objects.
  • Clear Naming: The name should clearly describe its purpose, as it always implements a single handle method.
  • Input / Output: Can return a new object or modify an input object in place.
  • Dependency Management: Relies only on provided inputs, avoiding direct infrastructure dependencies, ensuring easy unit testing.

Data Transfer Object (DTO)

Data Transfer Object is a simple, immutable data structure used for transferring data between application layers. Introduced by Martin Fowler in "Patterns of Enterprise Application Architecture", it acts as a data contract.

Characteristics:

  • Data Contracts: Defines clear structures for exchanging data between layers.
  • Immutability: DTOs cannot be modified after creation.
  • Application Access: Any additional data fetching required to fulfill a contract should be handled by the Application, as it has access to Repositories or subordinate Applications.

Value Object

Value Object is an object defined solely by its properties and has no unique identifier. Originating in object-oriented programming, it was refined by Eric Evans in "Domain-Driven Design" to reduce domain model complexity.

Characteristics:

  • No Identity: Identified by attributes, not a unique identifier.
  • Immutability: Cannot be modified after creation, ensuring consistency.
  • Equality: Two Value Objects are equal if all their properties match.

Examples:

  • Address: street, city, postal code, country.
  • Money: amount, currency.

Entity

Entity is a domain object identified by a unique property (typically a primary key in the database). It represents a single record in a table and encapsulates related data and behavior.

Characteristics:

  • Unique Identity: Ensures one-to-one correspondence with a database record.
  • Field Consistency: Fields in the Entity should align with the database schema.

Notes:

  • Fields such as created_at and updated_at, often managed by ORMs, can be omitted from the Entity if they are not required in the business logic.
  • Ideally, each Entity should have a dedicated Repository and possibly its own Application.

Aggregate

Aggregate is a collection of related Entity objects that work together within a single bounded context.
By exposing controlled methods for interaction, it ensures consistency and atomicity of operations under shared rules.

Characteristics:

  • Consistency: Ensures domain rules are followed by exposing public methods for interaction, ensuring all internal Entities remain in valid states.
  • Atomicity: Treats operations on the aggregate as a single unit, ensuring consistent changes across all entities.

Usage:

  • Ports create a DTO, which is passed to the Application. The Application builds Entities and groups them into an Aggregate, validating rules and contracts.
  • Aggregates can also act as simple containers for related Entities within a single HTTP request, avoiding the need for multiple REST calls, thereby reducing network overhead.

Component Interaction Flowchart

Component Interaction Flowchart

Factories

ApplicationFactory

Facilitates creating application instances with specific dependencies. Useful when multiple interfaces with different dependencies share the same application logic.

Example:

from dddesign.structure.applications import Application, ApplicationDependencyMapper, ApplicationFactory

from app.account_context.applications.account import AccountApp, account_app_impl
from app.account_context.applications.social_account import SocialAccountApp, social_account_app_impl
from app.account_context.domains.constants import SocialDriver
from app.account_context.infrastructure.adapters.external import social


class AuthSocialApp(Application):
    account_app: AccountApp = account_app_impl
    social_account_app: SocialAccountApp = social_account_app_impl
    social_adapter: social.SocialAdapterInterface
    
    ...


auth_social_app_factory = ApplicationFactory[AuthSocialApp](
    application_class=AuthSocialApp,
    dependency_mappers=(
        ApplicationDependencyMapper(
            application_attribute_name='social_adapter',
            request_attribute_value_map={
                SocialDriver.APPLE: social.apple_id_adapter_impl,
                SocialDriver.GOOGLE: social.google_adapter_impl,
                SocialDriver.FACEBOOK: social.facebook_adapter_impl,
            },
        ),
    ),
)

# note: the argument name must match the lowercase version of the Enum class name
auth_apple_app_impl = auth_social_app_factory.get(social_driver=SocialDriver.APPLE)

AggregateListFactory

Converts a list of Entity into Aggregate objects.

from dddesign.structure.infrastructure.adapters.internal import InternalAdapter

from app.account_context.domains.dto.media import Media, MediaId
from app.media_context.applications.media import MediaApp, media_app_impl


class MediaAdapter(InternalAdapter):
    media_app: MediaApp = media_app_impl
    
    def get(self, media_id: MediaId | None) -> Media | None:
        if media_id is None:
            return None

        medias = self.get_map((media_id,))
        return next(iter(medias.values()), None)

    def get_map(self, media_ids: tuple[str, ...]) -> dict[str, Media]:
        if not media_ids:
            return {}

        medias = self.media_app.get_list(media_ids=media_ids)
        return {MediaId(media.media_id): Media(**media.model_dump()) for media in medias}


media_adapter_impl = MediaAdapter()
from dddesign.structure.domains.aggregates import Aggregate
from pydantic import model_validator

from app.account_context.domains.dto.media import Media
from app.account_context.domains.entities.profile import Profile


class ProfileAggregate(Aggregate):
    profile: Profile
    icon: Media | None = None

    @model_validator(mode='after')
    def validate_consistency(self):
        if self.profile.icon_id:
            if self.icon is None:
                raise ValueError('`icon` field is required when `profile` has `icon_id`')
            if self.profile.icon_id != self.icon.media_id:
                raise ValueError('`profile.icon_id` is not equal to `icon.media_id`')
        elif self.icon is not None:
            raise ValueError('`icon` field is not allowed when `profile` has no `icon_id`')

        return self

Example 1: Retrieving multiple related objects

from dddesign.structure.domains.aggregates import AggregateDependencyMapper, AggregateListFactory

from app.account_context.domains.aggregates.profile import ProfileAggregate
from app.account_context.infrastructure.adapters.internal.media import media_adapter_impl


aggregate_list_factory = AggregateListFactory[ProfileAggregate](
    aggregate_class=ProfileAggregate,
    aggregate_entity_attribute_name='profile',
    dependency_mappers=(
        AggregateDependencyMapper(
            method_getter=media_adapter_impl.get_map,
            entity_attribute_name='icon_id',
            aggregate_attribute_name='icon',
        ),
    ),
)

aggregates: list[ProfileAggregate] = aggregate_list_factory.create_list([...])  # list of Profile Entity

Example 2: Retrieving a single related object

from dddesign.structure.domains.aggregates import AggregateDependencyMapper, AggregateListFactory

from app.account_context.domains.aggregates.profile import ProfileAggregate
from app.account_context.infrastructure.adapters.internal.media import media_adapter_impl


aggregate_list_factory = AggregateListFactory[ProfileAggregate](
    aggregate_class=ProfileAggregate,
    aggregate_entity_attribute_name='profile',
    dependency_mappers=(
        AggregateDependencyMapper(
            method_getter=media_adapter_impl.get,
            entity_attribute_name='icon_id',
            aggregate_attribute_name='icon',
        ),
    ),
)

aggregates: list[ProfileAggregate] = aggregate_list_factory.create_list([...])  # list of Profile Entity

Enums

BaseEnum

BaseEnum is a foundational enum class that should be used across the application. It extends the standard Python Enum and provides additional functionality:

  • __str__ method: Converts the enum’s value to a string representation, making it more readable in logs, responses, or debugging output.
  • has_value class method: Allows you to check whether a specific value is defined in the enum. This is particularly useful for validation purposes.

ChoiceEnum

ChoiceEnum is an extension of BaseEnum designed for scenarios where enumerations need both a machine-readable value and a human-readable title. It adds utility methods for creating user-friendly choices.

Error Handling

BaseError

BaseError is a foundational exception class that standardizes error handling by providing structured information for errors. It simplifies the creation of domain-specific exceptions and ensures consistency across the application.

CollectionError

CollectionError is an exception class designed to aggregate multiple instances of BaseError. It simplifies error handling in scenarios where multiple errors need to be captured and processed together.

Errors

Errors is a Data Transfer Object that transforms a CollectionError into a structured format for 4XX HTTP responses. It ensures domain-level errors are serialized and returned to the client in a meaningful way, avoiding 500 responses.

wrap_error

wrap_error is a utility function designed to convert a Pydantic ValidationError into a CollectionError, enabling a standardized way of handling and aggregating validation errors. It ensures that detailed error information is preserved while providing a structured format for further processing.

create_pydantic_error_instance

create_pydantic_error_instance is a utility function for dynamically creating custom PydanticErrorMixin instances, allowing to define detailed and context-aware Pydantic validation errors.

Testing and State Management

MagicMock

MagicMock is an enhanced version of unittest.mock.MagicMock that adds compatibility with BaseModel. It streamlines testing by handling Pydantic models more effectively in mocked environments.

TrackChangesMixin

TrackChangesMixin is a mixin for BaseModel that tracks changes made to model fields. It allows to monitor field modifications, compare current and initial values, and manage the state of the model.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dddesign-1.1.7.tar.gz (20.7 kB view details)

Uploaded Source

Built Distribution

dddesign-1.1.7-py3-none-any.whl (28.1 kB view details)

Uploaded Python 3

File details

Details for the file dddesign-1.1.7.tar.gz.

File metadata

  • Download URL: dddesign-1.1.7.tar.gz
  • Upload date:
  • Size: 20.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for dddesign-1.1.7.tar.gz
Algorithm Hash digest
SHA256 b7cff0e12d3e92350df83673a8d326d931606ad0514edd0f75bfbf5196ecd2fc
MD5 d58505ac232bb349276f2939b3215f22
BLAKE2b-256 568d6c7f3209305ea13e191e83e03d1809e21a39924569f58cf8831bf6ff9ced

See more details on using hashes here.

Provenance

The following attestation bundles were made for dddesign-1.1.7.tar.gz:

Publisher: publish_python_package.yml on davyddd/dddesign

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file dddesign-1.1.7-py3-none-any.whl.

File metadata

  • Download URL: dddesign-1.1.7-py3-none-any.whl
  • Upload date:
  • Size: 28.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for dddesign-1.1.7-py3-none-any.whl
Algorithm Hash digest
SHA256 e8ed27b2ccfc0360a4aea115305e12f7dc8c2ee8024e6cdeae9fc9a5b8493ca5
MD5 3afe8657d1598a9462515c95457282ca
BLAKE2b-256 12e323f3cedb9bc4d29ff7753ba92892edf7107bb699f6daa5cd4bd8ede1289c

See more details on using hashes here.

Provenance

The following attestation bundles were made for dddesign-1.1.7-py3-none-any.whl:

Publisher: publish_python_package.yml on davyddd/dddesign

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page