基于Django Rest Framework实现DDD洋葱架构的可行性与映射问询
Great question! I’ve spent months tinkering with DDD, Onion Architecture, and Django REST Framework (DRF) together, so I’m excited to share what I’ve learned. The short answer: Absolutely, you can map Onion Architecture concepts to DRF and build a fully DDD-aligned system. Let’s break this down step by step.
First: Why This Works
Onion Architecture’s core rule is dependency inversion: inner layers (domain, application) never depend on outer layers (infrastructure, interfaces). Instead, outer layers depend on abstractions defined in inner layers. DRF is flexible enough to fit this pattern—you just need to decouple your business logic from Django’s built-in MVC-style structure.
Mapping Onion Layers to DRF Project Structure
Here’s how to translate each Onion layer into a Django project layout:
| Onion Layer | Django Project Structure & Responsibilities |
|---|---|
| Core Domain Layer | core/domain/: Pure Python classes for entities, value objects, domain services, and abstract repositories.No Django/DRF dependencies here—this is your business logic heart. |
| Application Layer | core/application/: Use cases (application services) that coordinate domain logic, DTOs for cross-layer communication.Depends only on the domain layer, not infrastructure. |
| Infrastructure Layer | infrastructure/: Django ORM models, concrete repository implementations, DRF serializers.Depends on inner layers, handles framework-specific work. |
| Interface Layer | infrastructure/api/views/: DRF views/viewsets, URL routes.Handles HTTP requests/responses, delegates to application services. |
Example project structure:
your_project/ ├── core/ │ ├── domain/ │ │ ├── entities/ │ │ ├── value_objects/ │ │ ├── services/ │ │ └── repositories/ # Abstract base classes │ └── application/ │ ├── services/ # Use cases │ └── dtos/ ├── infrastructure/ │ ├── persistence/ │ │ ├── models/ # Django ORM models │ │ └── repositories/ # Concrete repo implementations │ └── api/ │ ├── serializers/ │ └── views/ ├── config/ # Django settings, urls.py
Key Implementation Steps
Let’s walk through critical parts of the code to make this concrete.
1. Isolate the Domain Layer (No Django Dependencies!)
Domain entities are pure Python classes—no inheriting from models.Model. This keeps your business logic framework-agnostic.
# core/domain/entities/order.py from dataclasses import dataclass from core.domain.value_objects.money import Money @dataclass(frozen=True) class OrderItem: product_id: str quantity: int price: Money @dataclass class Order: id: str customer_id: str items: list[OrderItem] status: str = "CREATED" def add_item(self, item: OrderItem): # Enforce domain rule: No zero-quantity items if item.quantity <= 0: raise ValueError("Quantity must be positive") self.items.append(item)
2. Use the Repository Pattern
Define abstract repositories in the domain layer, then implement them with Django ORM in the infrastructure layer. This decouples domain logic from database specifics.
Abstract Repository (Domain Layer):
# core/domain/repositories/order_repository.py from abc import ABC, abstractmethod from core.domain.entities.order import Order class OrderRepository(ABC): @abstractmethod def save(self, order: Order) -> None: pass @abstractmethod def get_by_id(self, order_id: str) -> Order | None: pass
Django ORM Implementation (Infrastructure Layer):
# infrastructure/persistence/repositories/django_order_repository.py from core.domain.repositories.order_repository import OrderRepository from core.domain.entities.order import Order, OrderItem from infrastructure.persistence.models import DjangoOrder, DjangoOrderItem class DjangoOrderRepository(OrderRepository): def save(self, order: Order) -> None: django_order, _ = DjangoOrder.objects.update_or_create( id=order.id, defaults={"customer_id": order.customer_id, "status": order.status} ) # Sync order items existing_product_ids = [item.product_id for item in django_order.items.all()] new_product_ids = [item.product_id for item in order.items] # Update or create new items for item in order.items: DjangoOrderItem.objects.update_or_create( order=django_order, product_id=item.product_id, defaults={ "quantity": item.quantity, "price_amount": item.price.amount, "price_currency": item.price.currency } ) # Remove items no longer in the domain order DjangoOrderItem.objects.filter( order=django_order, product_id__not_in=new_product_ids ).delete() def get_by_id(self, order_id: str) -> Order | None: try: django_order = DjangoOrder.objects.prefetch_related("items").get(id=order_id) items = [ OrderItem( product_id=item.product_id, quantity=item.quantity, price=Money(item.price_amount, item.price_currency) ) for item in django_order.items.all() ] return Order( id=django_order.id, customer_id=django_order.customer_id, status=django_order.status, items=items ) except DjangoOrder.DoesNotExist: return None
3. Build Application Use Cases
Application services (use cases) coordinate domain logic and repositories. They’re the "glue" between your domain and infrastructure, with no framework dependencies.
# core/application/services/create_order_use_case.py from core.domain.entities.order import Order, OrderItem from core.domain.repositories.order_repository import OrderRepository from core.application.dtos.create_order_dto import CreateOrderDTO from core.domain.value_objects.money import Money class CreateOrderUseCase: def __init__(self, order_repository: OrderRepository): self.order_repository = order_repository def execute(self, dto: CreateOrderDTO) -> Order: # Convert DTO to domain entities items = [ OrderItem( product_id=item.product_id, quantity=item.quantity, price=Money(item.price_amount, item.price_currency) ) for item in dto.items ] order = Order( id=dto.order_id, customer_id=dto.customer_id, items=items ) # Enforce additional domain rules total = sum(item.price.amount for item in order.items) if total <= 0: raise ValueError("Order total must be positive") # Persist the order self.order_repository.save(order) return order
4. Connect to DRF (Interface Layer)
DRF views call application use cases, and serializers handle converting between API data and DTOs/domain entities.
Serializers:
# infrastructure/api/serializers/order_serializers.py from rest_framework import serializers from core.application.dtos.create_order_dto import CreateOrderDTO from core.domain.entities.order import Order class CreateOrderItemSerializer(serializers.Serializer): product_id = serializers.CharField() quantity = serializers.IntegerField(min_value=1) price_amount = serializers.DecimalField(max_digits=10, decimal_places=2) price_currency = serializers.CharField(max_length=3) class CreateOrderSerializer(serializers.Serializer): order_id = serializers.CharField() customer_id = serializers.CharField() items = CreateOrderItemSerializer(many=True) def create(self, validated_data): return CreateOrderDTO(**validated_data) class OrderResponseSerializer(serializers.Serializer): id = serializers.CharField() customer_id = serializers.CharField() status = serializers.CharField() items = CreateOrderItemSerializer(many=True)
ViewSet:
# infrastructure/api/views/order_views.py from rest_framework import viewsets, status from rest_framework.response import Response from infrastructure.api.serializers.order_serializers import CreateOrderSerializer, OrderResponseSerializer from core.application.services.create_order_use_case import CreateOrderUseCase from infrastructure.persistence.repositories.django_order_repository import DjangoOrderRepository class OrderViewSet(viewsets.ViewSet): def __init__(self, **kwargs): super().__init__(**kwargs) self.order_repo = DjangoOrderRepository() self.create_order_use_case = CreateOrderUseCase(self.order_repo) def create(self, request): serializer = CreateOrderSerializer(data=request.data) if serializer.is_valid(): try: order = self.create_order_use_case.execute(serializer.validated_data) response_serializer = OrderResponseSerializer(order) return Response(response_serializer.data, status=status.HTTP_201_CREATED) except ValueError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
How This Compares to Frameworks Like Apache Isis
Apache Isis is a purpose-built DDD framework—it comes with out-of-the-box support for entities, repositories, and event sourcing. DRF, by contrast, is a general-purpose API framework, so you’ll need to manually implement DDD patterns like repositories and use cases. The tradeoff is flexibility: you have full control over every layer, and you can adapt the pattern to fit your exact needs, rather than being constrained by a framework’s opinions.
Final Takeaways
- Stick to the dependency rule: Inner layers (domain, application) should never import from outer layers (infrastructure, DRF).
- Test your domain logic in isolation—you don’t need to spin up Django to test core business rules.
- Use dependency injection (even simple constructor injection, as shown above) to keep your code modular and testable.
This approach takes a bit more upfront work, but it pays off in maintainability, testability, and the ability to evolve your system without being tied to Django’s implementation details.
内容的提问来源于stack exchange,提问作者Thinker

