Advanced and Expert Topics

Domain Driven Design with Django

Domain Driven Design (DDD) is a software development approach that focuses on creating a rich model of the business domain. When applied to Django applications, DDD helps create maintainable, expressive code that closely reflects business requirements and enables effective communication between developers and domain experts.

Domain Driven Design with Django

Domain Driven Design (DDD) is a software development approach that focuses on creating a rich model of the business domain. When applied to Django applications, DDD helps create maintainable, expressive code that closely reflects business requirements and enables effective communication between developers and domain experts.

Understanding Domain Driven Design

DDD emphasizes modeling the business domain accurately and placing business logic at the center of the application. This approach leads to more maintainable code, better communication with stakeholders, and systems that evolve naturally with business requirements.

Core DDD Concepts

Ubiquitous Language

  • Shared vocabulary between developers and domain experts
  • Used consistently in code, documentation, and conversations
  • Reduces translation errors and misunderstandings
  • Evolves with deeper domain understanding

Bounded Context

  • Explicit boundaries where domain models apply
  • Different contexts may have different models for same concepts
  • Prevents model corruption and confusion
  • Enables independent evolution of different parts

Domain Model

  • Rich objects that encapsulate business logic
  • Express business rules and invariants
  • Go beyond simple data containers
  • Reflect the mental model of domain experts

DDD Building Blocks

# Value Objects - Immutable objects defined by their attributes
from dataclasses import dataclass
from decimal import Decimal
from typing import Optional

@dataclass(frozen=True)
class Money:
    """Value object for monetary amounts"""
    amount: Decimal
    currency: str = 'USD'
    
    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("Money amount cannot be negative")
        
        if not self.currency or len(self.currency) != 3:
            raise ValueError("Currency must be a 3-letter code")
    
    def add(self, other: 'Money') -> 'Money':
        """Add two money amounts"""
        if self.currency != other.currency:
            raise ValueError("Cannot add different currencies")
        
        return Money(self.amount + other.amount, self.currency)
    
    def multiply(self, factor: Decimal) -> 'Money':
        """Multiply money by a factor"""
        return Money(self.amount * factor, self.currency)
    
    def is_zero(self) -> bool:
        """Check if amount is zero"""
        return self.amount == 0

@dataclass(frozen=True)
class Address:
    """Value object for addresses"""
    street: str
    city: str
    state: str
    postal_code: str
    country: str = 'US'
    
    def __post_init__(self):
        if not all([self.street, self.city, self.state, self.postal_code]):
            raise ValueError("All address fields are required")
    
    def is_same_city(self, other: 'Address') -> bool:
        """Check if addresses are in the same city"""
        return (self.city == other.city and 
                self.state == other.state and 
                self.country == other.country)

@dataclass(frozen=True)
class Email:
    """Value object for email addresses"""
    value: str
    
    def __post_init__(self):
        if not self._is_valid_email(self.value):
            raise ValueError(f"Invalid email address: {self.value}")
    
    def _is_valid_email(self, email: str) -> bool:
        """Basic email validation"""
        import re
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        return re.match(pattern, email) is not None
    
    def domain(self) -> str:
        """Get email domain"""
        return self.value.split('@')[1]

# Entities - Objects with identity that can change over time
class Customer:
    """Customer entity with business logic"""
    
    def __init__(self, customer_id: int, email: Email, name: str):
        self.id = customer_id
        self.email = email
        self.name = name
        self.addresses = []
        self.is_active = True
        self.created_at = timezone.now()
        self.credit_limit = Money(Decimal('1000.00'))
        self.current_balance = Money(Decimal('0.00'))
    
    def add_address(self, address: Address, is_primary: bool = False):
        """Add address with business rules"""
        if is_primary:
            # Only one primary address allowed
            for addr_info in self.addresses:
                addr_info['is_primary'] = False
        
        self.addresses.append({
            'address': address,
            'is_primary': is_primary,
            'added_at': timezone.now()
        })
    
    def get_primary_address(self) -> Optional[Address]:
        """Get primary address"""
        for addr_info in self.addresses:
            if addr_info['is_primary']:
                return addr_info['address']
        return None
    
    def change_email(self, new_email: Email):
        """Change email with business validation"""
        if new_email == self.email:
            return  # No change needed
        
        # Business rule: email change requires verification
        self.email = new_email
        self.is_active = False  # Deactivate until email verified
    
    def increase_credit_limit(self, amount: Money):
        """Increase credit limit with business rules"""
        if amount.amount <= 0:
            raise ValueError("Credit limit increase must be positive")
        
        # Business rule: credit limit increases require approval for large amounts
        if amount.amount > 5000:
            raise BusinessRuleError("Large credit increases require manual approval")
        
        self.credit_limit = self.credit_limit.add(amount)
    
    def can_make_purchase(self, amount: Money) -> bool:
        """Check if customer can make a purchase"""
        if not self.is_active:
            return False
        
        total_after_purchase = self.current_balance.add(amount)
        return total_after_purchase.amount <= self.credit_limit.amount
    
    def make_purchase(self, amount: Money):
        """Make a purchase with business validation"""
        if not self.can_make_purchase(amount):
            raise BusinessRuleError("Purchase exceeds credit limit")
        
        self.current_balance = self.current_balance.add(amount)

class Product:
    """Product entity with business logic"""
    
    def __init__(self, product_id: int, name: str, price: Money):
        self.id = product_id
        self.name = name
        self.price = price
        self.is_active = True
        self.created_at = timezone.now()
        self.category_id = None
        self.inventory_count = 0
    
    def change_price(self, new_price: Money):
        """Change product price with business rules"""
        if new_price.amount <= 0:
            raise ValueError("Product price must be positive")
        
        # Business rule: price changes > 50% require approval
        if abs(new_price.amount - self.price.amount) / self.price.amount > 0.5:
            raise BusinessRuleError("Large price changes require approval")
        
        self.price = new_price
    
    def is_available(self) -> bool:
        """Check if product is available for purchase"""
        return self.is_active and self.inventory_count > 0
    
    def reserve_inventory(self, quantity: int):
        """Reserve inventory with business validation"""
        if quantity <= 0:
            raise ValueError("Quantity must be positive")
        
        if quantity > self.inventory_count:
            raise BusinessRuleError("Insufficient inventory")
        
        self.inventory_count -= quantity
    
    def release_inventory(self, quantity: int):
        """Release reserved inventory"""
        if quantity <= 0:
            raise ValueError("Quantity must be positive")
        
        self.inventory_count += quantity

Aggregates - Consistency Boundaries

Aggregates are clusters of domain objects that are treated as a single unit for data changes. They enforce business invariants and maintain consistency.

# Aggregate Root - The only entry point to the aggregate
class Order:
    """Order aggregate root"""
    
    def __init__(self, order_id: int, customer_id: int):
        self.id = order_id
        self.customer_id = customer_id
        self.items = []
        self.status = OrderStatus.DRAFT
        self.total = Money(Decimal('0.00'))
        self.shipping_address = None
        self.created_at = timezone.now()
        self.confirmed_at = None
    
    def add_item(self, product: Product, quantity: int) -> 'OrderItem':
        """Add item to order with business validation"""
        if self.status != OrderStatus.DRAFT:
            raise BusinessRuleError("Cannot modify confirmed order")
        
        if not product.is_available():
            raise BusinessRuleError("Product is not available")
        
        if quantity <= 0:
            raise ValueError("Quantity must be positive")
        
        # Check if item already exists
        existing_item = self._find_item(product.id)
        if existing_item:
            existing_item.increase_quantity(quantity)
            item = existing_item
        else:
            item = OrderItem(product.id, product.name, product.price, quantity)
            self.items.append(item)
        
        # Reserve inventory
        product.reserve_inventory(quantity)
        
        # Recalculate total
        self._recalculate_total()
        
        return item
    
    def remove_item(self, product_id: int):
        """Remove item from order"""
        if self.status != OrderStatus.DRAFT:
            raise BusinessRuleError("Cannot modify confirmed order")
        
        item = self._find_item(product_id)
        if not item:
            raise ValueError("Item not found in order")
        
        self.items.remove(item)
        self._recalculate_total()
    
    def set_shipping_address(self, address: Address):
        """Set shipping address with validation"""
        if self.status not in [OrderStatus.DRAFT, OrderStatus.CONFIRMED]:
            raise BusinessRuleError("Cannot change address for shipped orders")
        
        self.shipping_address = address
    
    def confirm(self):
        """Confirm order with business validation"""
        if self.status != OrderStatus.DRAFT:
            raise BusinessRuleError("Can only confirm draft orders")
        
        if not self.items:
            raise BusinessRuleError("Cannot confirm empty order")
        
        if not self.shipping_address:
            raise BusinessRuleError("Shipping address is required")
        
        if self.total.is_zero():
            raise BusinessRuleError("Order total must be greater than zero")
        
        self.status = OrderStatus.CONFIRMED
        self.confirmed_at = timezone.now()
        
        # Raise domain event
        DomainEvents.raise_event(OrderConfirmedEvent(self))
    
    def cancel(self):
        """Cancel order with business rules"""
        if self.status not in [OrderStatus.DRAFT, OrderStatus.CONFIRMED]:
            raise BusinessRuleError("Cannot cancel order in current status")
        
        # Release reserved inventory
        for item in self.items:
            # This would need to be handled by a domain service
            # that has access to the product repository
            pass
        
        self.status = OrderStatus.CANCELLED
        
        # Raise domain event
        DomainEvents.raise_event(OrderCancelledEvent(self))
    
    def ship(self, tracking_number: str):
        """Ship order with validation"""
        if self.status != OrderStatus.CONFIRMED:
            raise BusinessRuleError("Can only ship confirmed orders")
        
        if not tracking_number:
            raise ValueError("Tracking number is required")
        
        self.status = OrderStatus.SHIPPED
        self.tracking_number = tracking_number
        
        # Raise domain event
        DomainEvents.raise_event(OrderShippedEvent(self, tracking_number))
    
    def _find_item(self, product_id: int) -> Optional['OrderItem']:
        """Find order item by product ID"""
        return next((item for item in self.items if item.product_id == product_id), None)
    
    def _recalculate_total(self):
        """Recalculate order total"""
        total_amount = sum(item.total().amount for item in self.items)
        self.total = Money(total_amount)

class OrderItem:
    """Order item entity (part of Order aggregate)"""
    
    def __init__(self, product_id: int, product_name: str, unit_price: Money, quantity: int):
        self.product_id = product_id
        self.product_name = product_name
        self.unit_price = unit_price
        self.quantity = quantity
    
    def increase_quantity(self, additional_quantity: int):
        """Increase item quantity"""
        if additional_quantity <= 0:
            raise ValueError("Additional quantity must be positive")
        
        self.quantity += additional_quantity
    
    def change_quantity(self, new_quantity: int):
        """Change item quantity"""
        if new_quantity <= 0:
            raise ValueError("Quantity must be positive")
        
        self.quantity = new_quantity
    
    def total(self) -> Money:
        """Calculate item total"""
        return self.unit_price.multiply(Decimal(self.quantity))

# Order Status Enum
from enum import Enum

class OrderStatus(Enum):
    DRAFT = "draft"
    CONFIRMED = "confirmed"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

# Domain Events
class DomainEvent:
    """Base class for domain events"""
    
    def __init__(self, aggregate_root):
        self.aggregate_root = aggregate_root
        self.occurred_at = timezone.now()

class OrderConfirmedEvent(DomainEvent):
    """Event raised when order is confirmed"""
    
    def __init__(self, order: Order):
        super().__init__(order)
        self.order_id = order.id
        self.customer_id = order.customer_id
        self.total = order.total

class OrderCancelledEvent(DomainEvent):
    """Event raised when order is cancelled"""
    
    def __init__(self, order: Order):
        super().__init__(order)
        self.order_id = order.id
        self.customer_id = order.customer_id

class OrderShippedEvent(DomainEvent):
    """Event raised when order is shipped"""
    
    def __init__(self, order: Order, tracking_number: str):
        super().__init__(order)
        self.order_id = order.id
        self.customer_id = order.customer_id
        self.tracking_number = tracking_number

# Domain Events Manager
class DomainEvents:
    """Manager for domain events"""
    
    _events = []
    
    @classmethod
    def raise_event(cls, event: DomainEvent):
        """Raise a domain event"""
        cls._events.append(event)
    
    @classmethod
    def get_events(cls) -> list:
        """Get all raised events"""
        return cls._events.copy()
    
    @classmethod
    def clear_events(cls):
        """Clear all events"""
        cls._events.clear()

Domain Services

Domain services contain business logic that doesn't naturally fit within a single entity or value object. They operate on multiple domain objects and encapsulate complex business rules.

# Domain Services - Business logic that spans multiple entities
class PricingService:
    """Domain service for pricing calculations"""
    
    def __init__(self, discount_repo: 'DiscountRepository'):
        self.discount_repo = discount_repo
    
    def calculate_order_total(self, order: Order, customer: Customer) -> Money:
        """Calculate order total with discounts and taxes"""
        subtotal = sum(item.total().amount for item in order.items)
        subtotal_money = Money(subtotal)
        
        # Apply customer discounts
        discount = self._calculate_customer_discount(customer, subtotal_money)
        discounted_total = subtotal_money.add(discount.multiply(Decimal('-1')))
        
        # Apply taxes
        tax = self._calculate_tax(discounted_total, order.shipping_address)
        
        return discounted_total.add(tax)
    
    def _calculate_customer_discount(self, customer: Customer, subtotal: Money) -> Money:
        """Calculate customer-specific discounts"""
        # VIP customers get 10% discount
        if self._is_vip_customer(customer):
            return subtotal.multiply(Decimal('0.10'))
        
        # First-time customers get $50 off orders over $200
        if self._is_first_time_customer(customer) and subtotal.amount > 200:
            return Money(Decimal('50.00'))
        
        return Money(Decimal('0.00'))
    
    def _calculate_tax(self, amount: Money, address: Address) -> Money:
        """Calculate tax based on shipping address"""
        # Simplified tax calculation
        tax_rates = {
            'CA': Decimal('0.0875'),  # California
            'NY': Decimal('0.08'),    # New York
            'TX': Decimal('0.0625'),  # Texas
        }
        
        tax_rate = tax_rates.get(address.state, Decimal('0.05'))  # Default 5%
        return amount.multiply(tax_rate)
    
    def _is_vip_customer(self, customer: Customer) -> bool:
        """Check if customer is VIP"""
        # Business rule: VIP customers have spent over $10,000
        return customer.lifetime_spent.amount > 10000
    
    def _is_first_time_customer(self, customer: Customer) -> bool:
        """Check if customer is making first purchase"""
        return customer.order_count == 0

class InventoryService:
    """Domain service for inventory management"""
    
    def __init__(self, product_repo: 'ProductRepository'):
        self.product_repo = product_repo
    
    def reserve_items_for_order(self, order: Order) -> bool:
        """Reserve inventory for all items in order"""
        # Check availability first
        for item in order.items:
            product = self.product_repo.get_by_id(item.product_id)
            if not product or product.inventory_count < item.quantity:
                return False
        
        # Reserve all items
        for item in order.items:
            product = self.product_repo.get_by_id(item.product_id)
            product.reserve_inventory(item.quantity)
            self.product_repo.save(product)
        
        return True
    
    def release_items_for_order(self, order: Order):
        """Release reserved inventory for cancelled order"""
        for item in order.items:
            product = self.product_repo.get_by_id(item.product_id)
            if product:
                product.release_inventory(item.quantity)
                self.product_repo.save(product)
    
    def check_low_inventory(self, threshold: int = 10) -> list:
        """Find products with low inventory"""
        return self.product_repo.find_by_inventory_below(threshold)

class OrderFulfillmentService:
    """Domain service for order fulfillment"""
    
    def __init__(self, 
                 shipping_service: 'ShippingService',
                 inventory_service: InventoryService,
                 notification_service: 'NotificationService'):
        self.shipping_service = shipping_service
        self.inventory_service = inventory_service
        self.notification_service = notification_service
    
    def fulfill_order(self, order: Order) -> bool:
        """Fulfill confirmed order"""
        if order.status != OrderStatus.CONFIRMED:
            raise BusinessRuleError("Can only fulfill confirmed orders")
        
        # Check inventory availability
        if not self.inventory_service.reserve_items_for_order(order):
            raise BusinessRuleError("Insufficient inventory to fulfill order")
        
        try:
            # Create shipping label
            tracking_number = self.shipping_service.create_shipment(
                order.shipping_address,
                order.items
            )
            
            # Update order status
            order.ship(tracking_number)
            
            # Send notification
            self.notification_service.send_shipping_notification(
                order.customer_id,
                order.id,
                tracking_number
            )
            
            return True
            
        except Exception as e:
            # Release inventory if fulfillment fails
            self.inventory_service.release_items_for_order(order)
            raise FulfillmentError(f"Order fulfillment failed: {e}")

Repositories - Data Access Abstraction

Repositories provide a collection-like interface for accessing domain objects, abstracting away data persistence details.

# Repository Interfaces
from abc import ABC, abstractmethod
from typing import List, Optional

class CustomerRepository(ABC):
    """Abstract repository for customer aggregate"""
    
    @abstractmethod
    def get_by_id(self, customer_id: int) -> Optional[Customer]:
        pass
    
    @abstractmethod
    def get_by_email(self, email: Email) -> Optional[Customer]:
        pass
    
    @abstractmethod
    def save(self, customer: Customer) -> Customer:
        pass
    
    @abstractmethod
    def find_vip_customers(self) -> List[Customer]:
        pass

class OrderRepository(ABC):
    """Abstract repository for order aggregate"""
    
    @abstractmethod
    def get_by_id(self, order_id: int) -> Optional[Order]:
        pass
    
    @abstractmethod
    def save(self, order: Order) -> Order:
        pass
    
    @abstractmethod
    def find_by_customer(self, customer_id: int) -> List[Order]:
        pass
    
    @abstractmethod
    def find_pending_orders(self) -> List[Order]:
        pass

class ProductRepository(ABC):
    """Abstract repository for product aggregate"""
    
    @abstractmethod
    def get_by_id(self, product_id: int) -> Optional[Product]:
        pass
    
    @abstractmethod
    def save(self, product: Product) -> Product:
        pass
    
    @abstractmethod
    def find_by_category(self, category_id: int) -> List[Product]:
        pass
    
    @abstractmethod
    def find_by_inventory_below(self, threshold: int) -> List[Product]:
        pass

# Django Repository Implementations
class DjangoCustomerRepository(CustomerRepository):
    """Django ORM implementation of customer repository"""
    
    def get_by_id(self, customer_id: int) -> Optional[Customer]:
        """Get customer by ID"""
        try:
            django_customer = CustomerModel.objects.get(id=customer_id)
            return self._to_domain_object(django_customer)
        except CustomerModel.DoesNotExist:
            return None
    
    def get_by_email(self, email: Email) -> Optional[Customer]:
        """Get customer by email"""
        try:
            django_customer = CustomerModel.objects.get(email=email.value)
            return self._to_domain_object(django_customer)
        except CustomerModel.DoesNotExist:
            return None
    
    def save(self, customer: Customer) -> Customer:
        """Save customer to database"""
        try:
            django_customer = CustomerModel.objects.get(id=customer.id)
            # Update existing customer
            django_customer.email = customer.email.value
            django_customer.name = customer.name
            django_customer.is_active = customer.is_active
            django_customer.credit_limit = customer.credit_limit.amount
            django_customer.current_balance = customer.current_balance.amount
        except CustomerModel.DoesNotExist:
            # Create new customer
            django_customer = CustomerModel(
                id=customer.id,
                email=customer.email.value,
                name=customer.name,
                is_active=customer.is_active,
                credit_limit=customer.credit_limit.amount,
                current_balance=customer.current_balance.amount,
                created_at=customer.created_at
            )
        
        django_customer.save()
        
        # Save addresses
        self._save_addresses(customer, django_customer)
        
        return customer
    
    def find_vip_customers(self) -> List[Customer]:
        """Find VIP customers"""
        django_customers = CustomerModel.objects.filter(
            lifetime_spent__gt=10000,
            is_active=True
        )
        
        return [self._to_domain_object(dc) for dc in django_customers]
    
    def _to_domain_object(self, django_customer) -> Customer:
        """Convert Django model to domain object"""
        customer = Customer(
            customer_id=django_customer.id,
            email=Email(django_customer.email),
            name=django_customer.name
        )
        
        customer.is_active = django_customer.is_active
        customer.created_at = django_customer.created_at
        customer.credit_limit = Money(django_customer.credit_limit)
        customer.current_balance = Money(django_customer.current_balance)
        
        # Load addresses
        for addr_model in django_customer.addresses.all():
            address = Address(
                street=addr_model.street,
                city=addr_model.city,
                state=addr_model.state,
                postal_code=addr_model.postal_code,
                country=addr_model.country
            )
            customer.add_address(address, addr_model.is_primary)
        
        return customer
    
    def _save_addresses(self, customer: Customer, django_customer):
        """Save customer addresses"""
        # Clear existing addresses
        django_customer.addresses.all().delete()
        
        # Save current addresses
        for addr_info in customer.addresses:
            CustomerAddressModel.objects.create(
                customer=django_customer,
                street=addr_info['address'].street,
                city=addr_info['address'].city,
                state=addr_info['address'].state,
                postal_code=addr_info['address'].postal_code,
                country=addr_info['address'].country,
                is_primary=addr_info['is_primary'],
                added_at=addr_info['added_at']
            )

class DjangoOrderRepository(OrderRepository):
    """Django ORM implementation of order repository"""
    
    def get_by_id(self, order_id: int) -> Optional[Order]:
        """Get order by ID"""
        try:
            django_order = OrderModel.objects.prefetch_related('items').get(id=order_id)
            return self._to_domain_object(django_order)
        except OrderModel.DoesNotExist:
            return None
    
    def save(self, order: Order) -> Order:
        """Save order to database"""
        with transaction.atomic():
            try:
                django_order = OrderModel.objects.get(id=order.id)
                # Update existing order
                django_order.status = order.status.value
                django_order.total = order.total.amount
                django_order.confirmed_at = order.confirmed_at
            except OrderModel.DoesNotExist:
                # Create new order
                django_order = OrderModel(
                    id=order.id,
                    customer_id=order.customer_id,
                    status=order.status.value,
                    total=order.total.amount,
                    created_at=order.created_at,
                    confirmed_at=order.confirmed_at
                )
            
            django_order.save()
            
            # Save shipping address
            if order.shipping_address:
                self._save_shipping_address(order, django_order)
            
            # Save order items
            self._save_order_items(order, django_order)
        
        return order
    
    def find_by_customer(self, customer_id: int) -> List[Order]:
        """Find orders by customer"""
        django_orders = OrderModel.objects.filter(
            customer_id=customer_id
        ).prefetch_related('items').order_by('-created_at')
        
        return [self._to_domain_object(do) for do in django_orders]
    
    def find_pending_orders(self) -> List[Order]:
        """Find orders pending fulfillment"""
        django_orders = OrderModel.objects.filter(
            status=OrderStatus.CONFIRMED.value
        ).prefetch_related('items')
        
        return [self._to_domain_object(do) for do in django_orders]
    
    def _to_domain_object(self, django_order) -> Order:
        """Convert Django model to domain object"""
        order = Order(django_order.id, django_order.customer_id)
        order.status = OrderStatus(django_order.status)
        order.total = Money(django_order.total)
        order.created_at = django_order.created_at
        order.confirmed_at = django_order.confirmed_at
        
        # Load shipping address
        if hasattr(django_order, 'shipping_address') and django_order.shipping_address:
            addr = django_order.shipping_address
            order.shipping_address = Address(
                street=addr.street,
                city=addr.city,
                state=addr.state,
                postal_code=addr.postal_code,
                country=addr.country
            )
        
        # Load order items
        for item_model in django_order.items.all():
            item = OrderItem(
                product_id=item_model.product_id,
                product_name=item_model.product_name,
                unit_price=Money(item_model.unit_price),
                quantity=item_model.quantity
            )
            order.items.append(item)
        
        return order
    
    def _save_shipping_address(self, order: Order, django_order):
        """Save shipping address"""
        addr = order.shipping_address
        OrderShippingAddressModel.objects.update_or_create(
            order=django_order,
            defaults={
                'street': addr.street,
                'city': addr.city,
                'state': addr.state,
                'postal_code': addr.postal_code,
                'country': addr.country
            }
        )
    
    def _save_order_items(self, order: Order, django_order):
        """Save order items"""
        # Clear existing items
        django_order.items.all().delete()
        
        # Save current items
        for item in order.items:
            OrderItemModel.objects.create(
                order=django_order,
                product_id=item.product_id,
                product_name=item.product_name,
                unit_price=item.unit_price.amount,
                quantity=item.quantity
            )

# Django Models for Persistence
class CustomerModel(models.Model):
    """Django model for customer persistence"""
    
    email = models.EmailField(unique=True)
    name = models.CharField(max_length=200)
    is_active = models.BooleanField(default=True)
    credit_limit = models.DecimalField(max_digits=10, decimal_places=2, default=1000)
    current_balance = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    lifetime_spent = models.DecimalField(max_digits=12, decimal_places=2, default=0)
    order_count = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

class CustomerAddressModel(models.Model):
    """Django model for customer addresses"""
    
    customer = models.ForeignKey(CustomerModel, related_name='addresses', on_delete=models.CASCADE)
    street = models.CharField(max_length=200)
    city = models.CharField(max_length=100)
    state = models.CharField(max_length=50)
    postal_code = models.CharField(max_length=20)
    country = models.CharField(max_length=50, default='US')
    is_primary = models.BooleanField(default=False)
    added_at = models.DateTimeField(auto_now_add=True)

class OrderModel(models.Model):
    """Django model for order persistence"""
    
    customer_id = models.IntegerField()
    status = models.CharField(max_length=20, default='draft')
    total = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    created_at = models.DateTimeField(auto_now_add=True)
    confirmed_at = models.DateTimeField(null=True, blank=True)
    tracking_number = models.CharField(max_length=100, blank=True)

class OrderItemModel(models.Model):
    """Django model for order items"""
    
    order = models.ForeignKey(OrderModel, related_name='items', on_delete=models.CASCADE)
    product_id = models.IntegerField()
    product_name = models.CharField(max_length=200)
    unit_price = models.DecimalField(max_digits=10, decimal_places=2)
    quantity = models.IntegerField()

class OrderShippingAddressModel(models.Model):
    """Django model for order shipping address"""
    
    order = models.OneToOneField(OrderModel, related_name='shipping_address', on_delete=models.CASCADE)
    street = models.CharField(max_length=200)
    city = models.CharField(max_length=100)
    state = models.CharField(max_length=50)
    postal_code = models.CharField(max_length=20)
    country = models.CharField(max_length=50, default='US')

Application Services - Use Case Orchestration

Application services orchestrate domain objects and services to implement specific use cases. They handle transaction boundaries and coordinate between different aggregates.

# Application Service Layer
class OrderApplicationService:
    """Application service for order use cases"""
    
    def __init__(self,
                 order_repo: OrderRepository,
                 customer_repo: CustomerRepository,
                 product_repo: ProductRepository,
                 pricing_service: PricingService,
                 inventory_service: InventoryService,
                 fulfillment_service: OrderFulfillmentService):
        self.order_repo = order_repo
        self.customer_repo = customer_repo
        self.product_repo = product_repo
        self.pricing_service = pricing_service
        self.inventory_service = inventory_service
        self.fulfillment_service = fulfillment_service
    
    @transaction.atomic
    def place_order(self, customer_id: int, items: List[dict], shipping_address: Address) -> int:
        """Place order use case"""
        # Load customer
        customer = self.customer_repo.get_by_id(customer_id)
        if not customer:
            raise ValueError("Customer not found")
        
        if not customer.is_active:
            raise BusinessRuleError("Customer account is not active")
        
        # Create order
        order = Order(self._generate_order_id(), customer_id)
        order.set_shipping_address(shipping_address)
        
        # Add items to order
        for item_data in items:
            product = self.product_repo.get_by_id(item_data['product_id'])
            if not product:
                raise ValueError(f"Product {item_data['product_id']} not found")
            
            order.add_item(product, item_data['quantity'])
        
        # Calculate total with pricing service
        total = self.pricing_service.calculate_order_total(order, customer)
        
        # Check customer credit limit
        if not customer.can_make_purchase(total):
            raise BusinessRuleError("Order exceeds customer credit limit")
        
        # Confirm order
        order.confirm()
        
        # Save order
        saved_order = self.order_repo.save(order)
        
        # Update customer balance
        customer.make_purchase(total)
        self.customer_repo.save(customer)
        
        # Process domain events
        self._process_domain_events()
        
        return saved_order.id
    
    @transaction.atomic
    def cancel_order(self, order_id: int, reason: str) -> None:
        """Cancel order use case"""
        order = self.order_repo.get_by_id(order_id)
        if not order:
            raise ValueError("Order not found")
        
        # Cancel order
        order.cancel()
        
        # Release inventory
        self.inventory_service.release_items_for_order(order)
        
        # Save order
        self.order_repo.save(order)
        
        # Process domain events
        self._process_domain_events()
    
    def _generate_order_id(self) -> int:
        """Generate unique order ID"""
        import random
        return random.randint(100000, 999999)
    
    def _process_domain_events(self):
        """Process raised domain events"""
        events = DomainEvents.get_events()
        
        for event in events:
            # Handle each event type
            if isinstance(event, OrderConfirmedEvent):
                self._handle_order_confirmed(event)
            elif isinstance(event, OrderCancelledEvent):
                self._handle_order_cancelled(event)
            elif isinstance(event, OrderShippedEvent):
                self._handle_order_shipped(event)
        
        DomainEvents.clear_events()
    
    def _handle_order_confirmed(self, event: OrderConfirmedEvent):
        """Handle order confirmed event"""
        # Send confirmation email, update analytics, etc.
        pass
    
    def _handle_order_cancelled(self, event: OrderCancelledEvent):
        """Handle order cancelled event"""
        # Send cancellation email, update analytics, etc.
        pass
    
    def _handle_order_shipped(self, event: OrderShippedEvent):
        """Handle order shipped event"""
        # Send shipping notification, update tracking, etc.
        pass

Bounded Contexts

Bounded contexts define explicit boundaries where specific domain models apply. Different contexts may have different models for the same real-world concepts.

# Sales Context - Focused on order processing
class SalesOrder:
    """Order model in sales context"""
    
    def __init__(self, order_id: int, customer_id: int):
        self.id = order_id
        self.customer_id = customer_id
        self.items = []
        self.total = Money(Decimal('0.00'))
        self.status = 'pending'
    
    def add_item(self, product_id: int, quantity: int, price: Money):
        """Add item to sales order"""
        # Sales-specific logic
        pass

# Fulfillment Context - Focused on shipping and delivery
class FulfillmentOrder:
    """Order model in fulfillment context"""
    
    def __init__(self, order_id: int):
        self.id = order_id
        self.items = []
        self.shipping_address = None
        self.status = 'pending_fulfillment'
        self.tracking_number = None
    
    def assign_to_warehouse(self, warehouse_id: int):
        """Assign order to warehouse for fulfillment"""
        # Fulfillment-specific logic
        pass
    
    def mark_as_shipped(self, tracking_number: str):
        """Mark order as shipped"""
        # Fulfillment-specific logic
        pass

# Context Mapping - Integration between contexts
class OrderContextMapper:
    """Maps between sales and fulfillment contexts"""
    
    @staticmethod
    def to_fulfillment_order(sales_order: SalesOrder) -> FulfillmentOrder:
        """Convert sales order to fulfillment order"""
        fulfillment_order = FulfillmentOrder(sales_order.id)
        
        # Map only relevant data for fulfillment
        for item in sales_order.items:
            fulfillment_order.items.append({
                'product_id': item.product_id,
                'quantity': item.quantity,
                'product_name': item.product_name
            })
        
        fulfillment_order.shipping_address = sales_order.shipping_address
        
        return fulfillment_order

DDD Best Practices in Django

Organizing DDD Code in Django

myproject/
├── domain/                    # Domain layer
│   ├── model/                # Domain models
│   │   ├── customer.py
│   │   ├── order.py
│   │   └── product.py
│   ├── services/             # Domain services
│   │   ├── pricing_service.py
│   │   └── inventory_service.py
│   ├── repositories/         # Repository interfaces
│   │   ├── customer_repository.py
│   │   └── order_repository.py
│   └── events/               # Domain events
│       └── order_events.py
├── application/              # Application layer
│   ├── services/            # Application services
│   │   ├── order_service.py
│   │   └── customer_service.py
│   └── commands/            # Command objects
│       └── order_commands.py
├── infrastructure/          # Infrastructure layer
│   ├── persistence/        # Django ORM implementations
│   │   ├── models.py
│   │   ├── django_customer_repository.py
│   │   └── django_order_repository.py
│   └── external/           # External service integrations
│       └── payment_gateway.py
└── presentation/           # Presentation layer
    ├── views/             # Django views
    ├── serializers/       # API serializers
    └── forms/             # Django forms

Testing DDD Applications

# Testing Domain Models
import pytest
from decimal import Decimal

class TestOrder:
    """Test order aggregate"""
    
    def test_add_item_to_order(self):
        """Should add item to order"""
        order = Order(1, customer_id=100)
        product = Product(1, "Test Product", Money(Decimal('10.00')))
        
        order.add_item(product, 2)
        
        assert len(order.items) == 1
        assert order.total.amount == Decimal('20.00')
    
    def test_cannot_modify_confirmed_order(self):
        """Should not allow modifying confirmed order"""
        order = Order(1, customer_id=100)
        product = Product(1, "Test Product", Money(Decimal('10.00')))
        order.add_item(product, 1)
        order.set_shipping_address(Address("123 Main St", "City", "ST", "12345"))
        order.confirm()
        
        with pytest.raises(BusinessRuleError):
            order.add_item(product, 1)
    
    def test_order_total_calculation(self):
        """Should calculate order total correctly"""
        order = Order(1, customer_id=100)
        product1 = Product(1, "Product 1", Money(Decimal('10.00')))
        product2 = Product(2, "Product 2", Money(Decimal('15.00')))
        
        order.add_item(product1, 2)
        order.add_item(product2, 3)
        
        assert order.total.amount == Decimal('65.00')  # (10*2) + (15*3)

# Testing Application Services
class TestOrderApplicationService:
    """Test order application service"""
    
    @pytest.fixture
    def service(self, mock_repos):
        """Create service with mocked dependencies"""
        return OrderApplicationService(
            order_repo=mock_repos.order_repo,
            customer_repo=mock_repos.customer_repo,
            product_repo=mock_repos.product_repo,
            pricing_service=mock_repos.pricing_service,
            inventory_service=mock_repos.inventory_service,
            fulfillment_service=mock_repos.fulfillment_service
        )
    
    def test_place_order_success(self, service, mock_repos):
        """Should place order successfully"""
        # Arrange
        customer = Customer(100, Email("test@example.com"), "Test Customer")
        product = Product(1, "Test Product", Money(Decimal('10.00')))
        product.inventory_count = 10
        
        mock_repos.customer_repo.get_by_id.return_value = customer
        mock_repos.product_repo.get_by_id.return_value = product
        mock_repos.pricing_service.calculate_order_total.return_value = Money(Decimal('20.00'))
        
        # Act
        order_id = service.place_order(
            customer_id=100,
            items=[{'product_id': 1, 'quantity': 2}],
            shipping_address=Address("123 Main St", "City", "ST", "12345")
        )
        
        # Assert
        assert order_id is not None
        mock_repos.order_repo.save.assert_called_once()
        mock_repos.customer_repo.save.assert_called_once()

Benefits of DDD in Django

Rich Domain Models

  • Business logic is centralized in domain objects
  • Code reads like business requirements
  • Easier to understand and maintain
  • Natural evolution with business changes

Clear Boundaries

  • Explicit separation between layers
  • Dependencies point inward toward domain
  • Infrastructure details don't leak into business logic
  • Easy to test business logic in isolation

Ubiquitous Language

  • Code uses business terminology
  • Better communication with stakeholders
  • Reduced translation errors
  • Documentation is self-evident

Flexibility

  • Easy to change persistence mechanisms
  • Can swap external service implementations
  • Business logic remains stable
  • Technology changes don't affect domain

Domain Driven Design transforms Django applications from data-centric CRUD systems into rich, expressive models that accurately reflect business requirements. By focusing on the domain and using DDD patterns, you create applications that are more maintainable, testable, and aligned with business needs.

The key is starting with the domain model, understanding the business deeply, and letting that understanding drive your technical decisions. DDD is not about technology—it's about solving complex business problems through better software design.