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.
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.
Ubiquitous Language
Bounded Context
Domain Model
# 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 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 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 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 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 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
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 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()
Rich Domain Models
Clear Boundaries
Ubiquitous Language
Flexibility
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.
System Architecture Patterns
System architecture patterns provide proven solutions for organizing complex Django applications. These patterns help create maintainable, scalable, and testable systems by defining clear boundaries between components and establishing consistent communication patterns. This comprehensive guide covers the most important architectural patterns for Django applications.
Building Large Scale Django Projects
Large-scale Django projects require careful architecture, organization, and development practices to remain maintainable as they grow. This comprehensive guide covers strategies for organizing massive Django applications, managing complexity, scaling development teams, and maintaining code quality across hundreds of models and thousands of views.