How Dify Implements Domain-Driven Design: Layered Architecture and Code Patterns
Dify implements Domain-Driven Design through a strict layered architecture that isolates pure domain models in api/core/entities/, orchestrates business logic via application services in api/services/, and enforces dependency direction through import-linter rules and repository protocols.
Dify, the open-source LLM application development platform by LangGenius, demonstrates practical Domain-Driven Design (DDD) principles through its Python codebase. The repository separates business logic from framework concerns using a clean architecture that isolates pure domain models, orchestrates use cases through service layers, and implements dependency inversion via repository protocols. This article examines the specific file structures, import rules, and code patterns that enforce these DDD boundaries.
DDD Layered Architecture in Dify
Dify organizes its codebase into distinct layers that mirror classic DDD tactical patterns, ensuring that business logic remains independent of frameworks and infrastructure concerns.
Domain Layer: Pure Business Logic
The domain layer resides in api/core/workflow/entities/ and api/core/entities/, containing pure business objects that model workflows, nodes, variables, and providers. These entities have no knowledge of persistence, Flask, or any framework.
In api/core/workflow/entities/workflow_execution.py, the WorkflowExecution class is implemented as a Pydantic BaseModel that only describes the state of a workflow run. It includes methods like new() for entity creation without any database or HTTP dependencies, keeping the domain logic clean and highly testable.
Application Layer: Service Orchestration
The application layer lives in api/services/ (e.g., api/services/workflow_service.py) and contains use-case-oriented services that orchestrate domain objects. The WorkflowService validates workflows, loads variables, and coordinates execution through WorkflowEntry without directly touching HTTP handlers or database connections.
Methods such as sync_draft_workflow, publish_workflow, and run_draft_workflow_node receive plain data objects and return domain entities, ensuring that business rules execute within the service layer rather than in controllers.
Infrastructure and Ports
Dify implements the ports and adapters pattern through repository protocols defined in api/core/workflow/repositories/. The WorkflowExecutionRepository protocol acts as a port that the service layer depends on, while concrete implementations reside in api/core/db/ or api/extensions/.
The infrastructure layer includes api/controllers/ for Flask request handling, api/core/db/ for SQLAlchemy persistence, and api/extensions/ for external integrations. These components depend on the application layer but never on the domain layer directly, maintaining proper dependency direction.
Enforcing DDD Boundaries
Dify employs specific technical mechanisms to prevent architectural violations and keep the domain isolated.
Import Architecture Rules
The codebase enforces a strict import hierarchy through linter rules documented in api/core/workflow/README.md. The dependency direction flows as: graph_engine → graph_events → graph → nodes → node_events → entities.
Domain entities never import from infrastructure layers, ensuring that api/core/workflow/entities/ remains free of Flask or SQLAlchemy imports. This prevents accidental coupling between business logic and technical implementation details.
Repository Pattern with Protocols
Instead of concrete database classes, Dify defines abstract contracts using Python's Protocol class. The WorkflowExecutionRepository in api/core/workflow/repositories/workflow_execution_repository.py declares methods like save() and get_by_id() without implementation details.
This decouples the domain from SQLAlchemy-specific code, allowing services to depend on abstractions while concrete repository implementations handle the actual persistence logic.
Domain-Only Models
Entities such as Agent, ProviderConfig, and ModelSettings are simple Pydantic models with type hints and validation logic but no ORM code. Validation rules—for example, credential policy compliance—live inside domain-level helper methods rather than in controllers or database layers.
These models reside exclusively in the entities/ packages, ensuring that configuration and validation logic remains part of the domain rather than infrastructure.
Cross-Cutting Concerns
Dify implements a layer system described in api/core/workflow/README.md that provides injection points for debugging, execution limits, and event publishing. This middleware approach wraps the core graph engine without contaminating domain logic, handling concerns like logging and performance monitoring through the layer system rather than embedding them in entity classes.
Code Implementation Examples
Creating Domain Entities
Domain entities provide factory methods that construct instances without framework dependencies:
from core.workflow.entities import WorkflowExecution
from core.workflow.enums import WorkflowType
from libs.datetime_utils import naive_utc_now
execution = WorkflowExecution.new(
id_="exec-001",
workflow_id="wf-123",
workflow_type=WorkflowType.CHAT,
workflow_version="v1.0",
graph={"nodes": [], "edges": []},
inputs={"question": "What is DDD?"},
started_at=naive_utc_now(),
)
The new classmethod lives entirely within the domain model in api/core/workflow/entities/workflow_execution.py, free from any database or web framework code.
Defining Repository Ports
Repository interfaces declare contracts without implementation:
from typing import Protocol
from core.workflow.entities import WorkflowExecution
class WorkflowExecutionRepository(Protocol):
"""Port used by services to persist WorkflowExecution."""
def save(self, execution: WorkflowExecution) -> None: ...
def get_by_id(self, execution_id: str) -> WorkflowExecution | None: ...
Only the contract is declared in api/core/workflow/repositories/workflow_execution_repository.py; concrete SQLAlchemy implementations exist elsewhere in the infrastructure layer.
Service Layer Coordination
The service layer orchestrates domain logic and persistence:
from services.workflow_service import WorkflowService
from api.core.db.session_factory import session_factory
# inject concrete repository implementation via factory
service = WorkflowService(session_maker=session_factory)
# business use-case: publish a draft workflow
published = service.publish_workflow(
session=db.session,
app_model=my_app,
account=current_user,
marked_name="Release 1.0",
)
WorkflowService coordinates domain validation, repository persistence, and side effects while remaining agnostic to HTTP transport details.
Controller Delegation
Flask controllers act as thin adapters that translate HTTP requests to service calls:
from flask import Blueprint, request, jsonify
from services.workflow_service import WorkflowService
bp = Blueprint("workflow", __name__)
@bp.post("/apps/<app_id>/workflows/publish")
def publish_workflow(app_id: str):
data = request.json
service = WorkflowService()
workflow = service.publish_workflow(
session=db.session,
app_model=get_app(app_id),
account=get_current_account(),
marked_name=data.get("name", ""),
marked_comment=data.get("comment", ""),
)
return jsonify(workflow.dict())
The controller simply translates HTTP requests to service method calls; all business logic remains encapsulated in the service layer.
Summary
- Domain Isolation: Business logic lives in
api/core/entities/as pure Pydantic models without framework dependencies. - Dependency Direction: Import-linter rules enforce that domain layers never depend on infrastructure, following the hierarchy ending at
entities. - Repository Abstraction: Protocol classes in
api/core/workflow/repositories/decouple services from SQLAlchemy implementations. - Service Orchestration: The
api/services/layer coordinates use cases without direct HTTP or database interaction. - Framework Decoupling: Flask controllers in
api/controllers/act as thin adapters, keeping business rules testable and independent of web transport.
Frequently Asked Questions
How does Dify prevent domain models from depending on infrastructure?
Dify enforces strict import architecture rules documented in api/core/workflow/README.md that prohibit domain entities in api/core/entities/ from importing Flask, SQLAlchemy, or other infrastructure concerns. The linter validates that dependencies flow only in one direction toward the domain layer, ensuring entities remain pure Pydantic models.
What pattern does Dify use for database access in DDD?
Dify implements the repository pattern using Python's Protocol classes. Services depend on abstract repository interfaces (e.g., WorkflowExecutionRepository) declared in api/core/workflow/repositories/, while concrete SQLAlchemy implementations live in api/core/db/. This decouples domain logic from persistence technology.
How are business rules enforced in Dify's architecture?
Business rules execute within the application service layer in api/services/. The WorkflowService validates workflows, checks credentials through domain methods, and coordinates execution before calling repositories. This keeps business logic out of controllers and ensures rules are tested independently of HTTP transport.
Where does input validation occur in Dify's DDD implementation?
Input validation occurs at multiple levels: Pydantic models in the domain layer enforce type constraints and business invariants, while domain-specific helpers validate credential policies and configuration rules. The service layer performs use-case validation before persisting changes, ensuring all business rules execute within the domain or application layers rather than in infrastructure controllers.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →