tactical-model · temario
3.5tema 5 de 5

Módulos y Packaging

La estructura de paquetes refleja el modelo del dominio: cohesión alta, acoplamiento bajo, capas con dirección de dependencia explícita.

El packaging en DDD no es una decisión técnica arbitraria: es la proyección del modelo del dominio en el sistema de archivos. La estructura de carpetas y paquetes debe comunicar los conceptos del negocio, no los detalles técnicos. Un desarrollador nuevo que mira la estructura de carpetas debe entender qué hace el sistema antes de leer una sola línea de código.

El primer nivel de organización es el Bounded Context. Cada contexto tiene su propio paquete bajo contexts/, con su propio modelo, su propio lenguaje ubicuo y sus propias capas. Los paquetes de distintos contextos se comunican a través de interfaces explícitas (Anti-Corruption Layer, eventos de integración), nunca importando directamente del modelo interno del otro.

Dentro de cada Bounded Context, las capas (domain, application, infrastructure) se separan en sub-paquetes distintos. La regla de dependencia es absoluta: domain no importa nada de application ni de infrastructure; application importa domain pero no infrastructure; infrastructure importa a ambas para implementar sus puertos.

El shared_kernel es un paquete especial que contiene conceptos genuinamente compartidos entre contextos: clases base (Entity, AggregateRoot, DomainEvent), tipos utilitarios (Result, errors base) y abstracciones de la capa de aplicación (Command, Query, EventBus port, UnitOfWork port). Debe mantenerse al mínimo: cuanto más crece el shared_kernel, más acoplados están los contextos.

La cohesión y el acoplamiento son las métricas que guían las decisiones de packaging. Alta cohesión: elementos que cambian juntos por las mismas razones viven juntos. Bajo acoplamiento: paquetes que son independientes no se importan entre sí. Si cambiar un módulo obliga a cambiar otro no relacionado, hay un problema de acoplamiento.

El src-layout (situar el código fuente bajo src/<project>/) es la convención recomendada con uv y pyproject.toml. Facilita la separación entre código de producción y tests, evita problemas de importación ambigua y hace explícito qué es el paquete instalable. Los tests viven fuera de src/, en su propia estructura unit/integration/e2e.

structure.txt
# Árbol canónico con src-layout (uv / pyproject.toml)
# Cada carpeta es un paquete Python (__init__.py omitido para claridad)

<project>/
├── pyproject.toml              # uv: deps + config (ruff, mypy, pytest)
├── uv.lock
├── .python-version
├── Makefile
├── src/
│   └── <project>/
│       ├── shared_kernel/      # conceptos compartidos entre contextos (mínimo)
│       │   ├── domain/
│       │   │   ├── value_object.py   # base VO
│       │   │   ├── entity.py         # base Entity
│       │   │   ├── aggregate_root.py # base Aggregate Root + pull_events
│       │   │   ├── domain_event.py   # base Domain Event
│       │   │   ├── result.py         # Result/Either (flujo sin excepciones)
│       │   │   └── errors.py         # errores base de dominio
│       │   └── application/
│       │       ├── command.py        # Command + CommandHandler (ABC/Protocol)
│       │       ├── query.py          # Query + QueryHandler (ABC/Protocol)
│       │       ├── event_bus.py      # EventBus (PUERTO)
│       │       └── unit_of_work.py   # UnitOfWork (PUERTO)
│       ├── contexts/
│       │   └── <bounded_context>/    # un paquete por bounded context
│       │       ├── domain/           # SIN dependencias de framework/infra
│       │       │   ├── model/
│       │       │   │   ├── <aggregate>.py
│       │       │   │   ├── <entity>.py
│       │       │   │   └── <value_object>.py
│       │       │   ├── events/
│       │       │   │   └── <something_happened>.py
│       │       │   ├── repositories/
│       │       │   │   └── <aggregate>_repository.py  # PUERTO (ABC)
│       │       │   └── services/
│       │       │       └── <domain_service>.py
│       │       ├── application/      # depende de domain, NO de infra
│       │       │   ├── commands/
│       │       │   │   └── <do_something>/
│       │       │   │       ├── command.py
│       │       │   │       └── handler.py
│       │       │   └── queries/
│       │       │       └── <get_something>/
│       │       │           ├── query.py
│       │       │           ├── handler.py
│       │       │           └── read_model.py
│       │       └── infrastructure/   # adapters: implementan puertos
│       │           ├── persistence/
│       │           │   ├── models.py          # ORM — NO son el dominio
│       │           │   ├── mappers.py
│       │           │   └── sql_<aggregate>_repository.py
│       │           └── api/
│       │               ├── controllers.py     # FastAPI (driving adapter)
│       │               └── schemas.py         # Pydantic request/response
│       └── main.py              # Composition Root (DI / wiring de adapters)
└── tests/
    ├── unit/
    ├── integration/
    └── e2e/
project/
<project>
shared_kernel
contexts
domain
application
infrastructure

Debugging lab

Detecta y corrige el error o la violación de diseño.

0/5 tests passing0%
  1. 3.5.5.1

    # contexts/<bc>/domain/model/<aggregate>.py from sqlalchemy.orm import Session # importar infra en domain class <Aggregate>(AggregateRoot): def save(self, session: Session) -> None: session.add(self) # persistir desde el Aggregate

  2. 3.5.5.2

    # contexts/catalog/domain/model/product.py from contexts.inventory.domain.model.stock import Stock # importar modelo de otro contexto class Product(AggregateRoot): stock: Stock # referencia al modelo interno de otro contexto

  3. 3.5.5.3

    # Estructura de paquetes: src/<project>/ ├── models.py # todos los modelos ORM aquí ├── schemas.py # todos los Pydantic schemas aquí ├── handlers.py # todos los handlers aquí └── routes.py # todas las rutas FastAPI aquí

  4. 3.5.5.4

    # shared_kernel/domain/catalog_product.py from dataclasses import dataclass from decimal import Decimal @dataclass(frozen=True) class CatalogProduct: id: str name: str price: Decimal category: str # lógica de negocio específica de Catalog en el shared_kernel

  5. 3.5.5.5

    # main.py — composition root from contexts.catalog.application.commands.create_product.handler import CreateProductHandler # En el handler: from sqlalchemy import create_engine # infraestructura en application engine = create_engine('sqlite:///app.db') session = Session(engine)