浅谈 DDD 领域驱动设计架构

2026/7/17 14:18:41Henry3 阅读0 点赞0 评论

前言

在开始实习前 Mentor 给的 Landing 文档中就让我了解 DDD 架构,对于我这个只写过中小型项目的人来说,当时看的第一眼就觉得好复杂好麻烦,不过对于中大型项目来说,很多架构都是前期开发成本高,但保证了后期的可维护性,所以还是有必要了解的,而且我顺便还发现,小徐先生也发了这个文章,又可以美美开学了。

技术背景

DDD(全称 Domain-Driven Design)领域驱动设计,是一种项目设计方法论,通过关注业务领域(Domain)、拆分领域上下文(Bounded Context)、搭建领域模型(Domain Model),提高业务和开发之间的共识度与协作效率,保证项目准确反映出业务需求和规则。

我 Mentor 之前是做 Java 的,可能还是习惯对面向对象设计,我感觉 DDD 这种模式还是 Java 上应用更多,毕竟 Java 生态下,中大型项目太多了。Golang 以云原生领域作为主战场,在这块的应用还是相对较少。

层级名称 职责定位 经典三层结构 DDD 四层结构
presentation 渲染展示层,负责接口请求参数的接收与响应参数的组装。(controller)
application 业务应用层,负责对业务流程进行编排组织。(service-1)
domain 业务领域层,内聚了某个特定业务领域的功能。(service-2) -
infrastructure 基础能力层,包含数据访问对象(DAO)、第三方系统客户端(Client)以及其他通用能力

对于我这个没有仔细了解过 Java 工程化的人来说,甚至不知道 Java 中的 Controller 究竟是干什么吊用的,结果就是一个对外展示层而已,其实就是对应 Golang 中的 api/endpoint/handler/ 层,名字搞的故弄玄虚的。

传统三层结构存在的问题

在常规项目中,使用较多的是经典三层结构(Controller-Service-DAO),其中和上文提到的各层级间的映射关系为:

  • Controller——Presentation:渲染展示层
  • Service——Application:业务应用层
  • DAO——Infrastructure:基础能力层

在 Golang 开发中的常见目录结构,按照技术层进行分包,这也是我用的最多的一种结构:

TEXT
myapp/
├── go.mod
├── cmd/
│   ├── api/
│   │   └── main.go
│   ├── worker/
│   │   └── main.go
│   └── migrate/
│       └── main.go
├── internal/
│   ├── config/
│   ├── server/
│   ├── handler/
│   ├── service/
│   ├── repository/
│   ├── model/
│   ├── middleware/
│   └── app/
├── pkg/
│   ├── jwt/
│   ├── logger/
│   └── snowflake/
├── api/
│   └── proto/
├── configs/
│   └── config.yaml
├── scripts/
│   └── migrate.sh
└── docs/
    └── api.md

这种常规三层结构是非常经典实用的,但其主要适用于中小型项目,其中存在的核心问题就是技术建模和业务需求存在视角差异,导致项目模块边界不清晰、耦合度过高。

如上图所示,随着项目的迭代演进,各层级与各模块之间可能存在交叉引用的情况,一旦有迭代新的功能或者新增新的模块,会有很高的实现成本。比如下列场景:

如果需要按照业务功能进行拆分,将项目由单体服务转变为微服务,这一刀应该落在何处,是否有明确的切分轨迹,是否能够做到低成本实现?

从三层结构到 DDD

DDD 相对传统三层结构,其核心差异在于:

  • 设计视角上:架构设计环节中,需要从业务视角出发,根据业务功能归属,提前做好不同业务领域的拆分;
  • 技术实现上:
    1. 在 application 与 infrastructure 之间新增 domain 领域层;
    2. 将原本糅合在 service 层实现的业务逻辑拆分到 application 和 domain 两层;
    3. domain 层提供一系列业务子域实例(subdomain),内聚各个业务模块的内部逻辑,对外隐藏细节;
    4. application 层组装各个 subdomain,完成业务流程的编排,但不感知具体 subdomain 内部实现细节。
flowchart LR
    subgraph T["传统三层架构"]
        direction TB
        T1["对外层<br/>Controller / Handler"]
        T2["业务逻辑层<br/>Service"]
        T3["数据访问层<br/>DAO / Repository / Mapper"]
        T4[("MySQL / Redis")]

        T1 --> T2
        T2 --> T3
        T3 --> T4
    end

    subgraph D["DDD 四层架构"]
        direction TB
        D1["用户接口层<br/>Controller / API / RPC"]
        D2["应用层<br/>Application Service / Use Case"]
        D3["领域层<br/>Entity / Value Object<br/>Aggregate / Domain Service"]
        D4["基础设施层<br/>Repository Impl / MQ / Cache"]
        D5[("Database / External Services")]

        D1 --> D2
        D2 --> D3
        D2 --> D4
        D3 --> D4
        D4 --> D5
    end

    T -. "对比" .-> D

    style T1 fill:#E8F0FE,stroke:#5B8FF9
    style T2 fill:#E6F7EC,stroke:#52C41A
    style T3 fill:#FFF7E6,stroke:#FA8C16
    style T4 fill:#F9F0FF,stroke:#722ED1

    style D1 fill:#E8F0FE,stroke:#5B8FF9
    style D2 fill:#E6F7EC,stroke:#52C41A
    style D3 fill:#FFF1F0,stroke:#F5222D
    style D4 fill:#FFF7E6,stroke:#FA8C16
    style D5 fill:#F9F0FF,stroke:#722ED1

适用场景

ddd

如上图所示,DDD相对于传统架构而言,在初期的架构实现上存在更高的实现成本和项目复杂度,但是随着项目的迭代演进,其中存在的收益会愈发明显,因而 DDD 模式主要适用于业务逻辑复杂、迭代演进频繁的大型业务系统。

因此我们需要做到在合适的场景中选择合适方案,而非一味迷信或者一味抵制,一些业务逻辑复杂、迭代演进频繁的大型业务系统是应用 DDD 的主战场,而对于简单的中小系统则不建议使用该模式。

架构拆解

%%{init: {
  "flowchart": {
    "curve": "linear",
    "nodeSpacing": 45,
    "rankSpacing": 55
  }
}}%%

flowchart TB

    subgraph Presentation["presentation 表现层"]
        direction LR

        P1["Controller"]
        P2["Handler / API"]
        P3["Request / Response DTO"]
        P4["..."]

        P1 ~~~ P2 ~~~ P3 ~~~ P4
    end

    subgraph Application["application 应用层"]
        direction LR

        A1["Application Service"]
        A2["Use Case"]
        A3["Command / Query"]
        A4["..."]

        A1 ~~~ A2 ~~~ A3 ~~~ A4
    end

    subgraph Domain["domain 领域层"]
        direction LR

        D1["Aggregate / Entity"]
        D2["Value Object"]
        D3["Domain Service"]
        D4["Repository Interface"]
        D5["Domain Event"]

        D1 ~~~ D2 ~~~ D3 ~~~ D4 ~~~ D5
    end

    subgraph Infrastructure["infrastructure 基础设施层"]
        direction LR

        I1["Repository Impl"]
        I2["DAO / ORM"]
        I3["Redis / MQ"]
        I4["RPC Client"]
        I5["Database"]

        I1 ~~~ I2 ~~~ I3 ~~~ I4 ~~~ I5
    end

    Presentation -->|"调用"| Application
    Application -->|"调用领域能力"| Domain
    Domain -->|"定义抽象,基础设施实现"| Infrastructure

    classDef component fill:#ffffff,stroke:#222222,stroke-width:1.5px,color:#222222;

    class P1,P2,P3,P4 component;
    class A1,A2,A3,A4 component;
    class D1,D2,D3,D4,D5 component;
    class I1,I2,I3,I4,I5 component;

    style Presentation fill:#9fd2f4,stroke:#222222,stroke-width:2px
    style Application fill:#9fd2f4,stroke:#222222,stroke-width:2px
    style Domain fill:#9fd2f4,stroke:#222222,stroke-width:2px
    style Infrastructure fill:#9fd2f4,stroke:#222222,stroke-width:2px

如上图,DDD 四层架构层级包含了:

  • presentation 层:渲染展示层,定位是 BFF(Backend for frontend),负责感知处理与前端对接的协议细节;
  • application层:业务应用层,调用 domain 层各业务子域实例,完成业务流程编排。如果把 domain 层类比成一个积木市场的话,那么 application 层所做的事情就是“搭积木”——只需要按照预设图纸(业务需求)把积木拼接成对应的轮廓,而无需关心某个积木块内部的结构原理;
  • domain 层:业务领域层,根据领域拆分出一系列业务子域实例(subdomain),在其中内聚该领域下的核心业务逻辑——作为一个又一个点状的积木组件,供 application 层使用;
  • infrastructure 层:基础组件层,提供一系列基础能力,比如数据访问对象 DAO(Data access object),访问第三方系统的客户端 client,以及一系列通用能力。

除了 application 与 domain 层的拆分之外,在 DDD 架构中还存在几个要点:

  • domain 层 repository 定义:domain 层定义了存储接口 repository(interface),定义好与业务数据底层存储交互的抽象接口,而具体实现则由 infrastructure 层中提供对应 dao 实例;
  • infrastructure 层依赖倒置原则:infrastructrue 层逆向实现由上层定义的一系列通用能力的抽象接口,以此保证其实现细节不会侵入到上层模块,能够实现低成本替换。

DIP(Dependency Inversion Principle)依赖倒置原则核心思想是,高层模块不依赖于底层模块,二者都依赖于抽象接口,而抽象接口由高层定义,因此在依赖关系上属于底层依赖高层。

落地具体代码实现上,DDD四层结构对应的 package 依赖关系应该是:

  • presentation 依赖于 application;
  • application 依赖于 domain;
  • infrastructure 则依赖于由其余三层(定义好的 interface)。

Presentation

presentation

presentation 层定位类似于传统三层结构中的 controller,其中需要:

  • 定义 VO(view object),作为前后端交互的协议参数;
  • 参数解析/转换/响应,解析和组装api请求与响应参数;
  • 调用 application 层,执行业务流程(需要将 view object 转换为 application 层使用的 DTO)。

Application

application

application 层跨越不同业务子域 subdomain 完成整个业务流程的编排,该层只负责流程编排,而不关心某个业务领域内的具体逻辑。相对于 domain 层,application 应该是很薄的一层。

如果说执行一个业务流程类似于写一本书,那么 application 输出的内容就类似于一本书的目录,按照执行顺序把各章各节的标题以及对应页码罗列出来,至于某个具体章节的内容,则需要根据页码跳转到对应位置(subdomain)来浏览更完整的上下文内容。

此外,在 application 层中需要完成对数据转换对象 DTO(Data transfer object)的定义,DTO 比较简单,用于在业务流程中传递,不包含具体业务逻辑。

Domain

domain

domain 层是DDD架构的核心所在,在 DDD 模式落地实战过程中,决定成败的核心步骤就在于如何在 domain 层完成对业务领域的拆分,保证拆分出来的每个业务子域(subdomain)能完全内聚该领域内的业务逻辑,尽可能降低子域之间存在的互相依赖程度。

在 domain 层中,将各领域内的业务模型定义成 Entity 和 VO(value object),并且基于面向对象的思想,将相关的业务逻辑封装到类的成员方法中,以形成所谓的“充血模型”。

对于需要访问到的业务数据存储模型,子域中会定义出仓储接口 repository,并由 infrastructure 提供具体实现版本,这样能够对 domain 层屏蔽底层存储组件的具体实现细节,体现依赖倒置原则。

domain

domain 层中针对各子域在实现上设置“北向网关”和“南向网关”,本质上是两个防腐层代码:

  • 北向网关: application 层或者其他子域需要通过当前子域的北向网关来完成对该子域的请求,因此北向网关中会定义好对应当前子域的抽象接口;
  • 南向网关: 当前子域需要通过南向网关来实现对外部资源的请求,因此南向网关层中会定义好对应外部资源的抽象接口,前文提及的业务数据仓储接口 repository interface 就是定义于此。

随着项目的迭代演进,如何寻找将项目由单体服务拆分为微服务的拆分边界?

在 DDD 模式下,如果对业务领域拆分得足够合理,那么我们可以沿着每个业务子域(subdomain)南北向网关的边界,将其拆分成一个个独立的微服务。

Infrastructure

infra

infrastructure 层作为基础能力层,核心功能是为其他层提供通用技术支持,也就是 TEG 在干的活,其封装了与外部资源的交互实现细节。通过依赖倒置原则,保证上层模块不会感知到 infrastructure 层具体实现,因此即便涉及底层组件的替换变更,对上层模块也是没有侵扰的。

infra

其他概念

展开介绍 DDD 下各种模型前,需要首先探讨到有关“贫血模型”和“充血模型”的定义。

  • 贫血模型(Anemic Domain Model): 作为数据传输的载体,只包含简单的属性和基本的访问器方法(getter、setter方法)而不包含业务逻辑,更偏向于面向过程的编程风格;
  • 充血模型(Rich Domain Model): 该类对象不仅包含数据属性,还封装了与自身相关的业务逻辑,更符合面向对象的设计原则,强调对象的行为和状态的统一。

DDD 模型

  • VO(view object)视图对象:定义于 presentation 层的贫血模型,封装了展示层(前端)需要显示的数据;
  • DTO(data transfer object)数据传输对象:定义于 application 层的贫血模型,包含多个属性和相应 getter/setter 方法,用于在展示层与应用层传输数据;
  • Entity:定义于 domain 层的充血模型,是具有唯一标识的业务对象,其身份在整个业务流转的生命周期内保持不变。
  • VO(value object):定义于 domain 层的充血模型,是没有唯一标识的对象,其身份由其属性值而非唯一标识决定——有关 entity 与 value object 的差异,在下文详细说明;
  • PO(persistent object):定义于 infrastructure 层的贫血模型,是反映底层存储结构的持久化对象。

VO、Entity 与 Aggregate

Entity 与 VO(value object)均定义于 domain 层,两者均是基于面向对象设计实现,根据领域职能归属,将业务逻辑挂载到成员方法中。两者的核心差异在于:

  • VO 对应一系列没有唯一标识的静态属性集合,通过属性值标识其身份,本身没有生命周期的概念,一旦创建出来,其属性和状态即不可改变,属性值一旦发生更改,就代表产生出新的对象实例;
  • Entity 对应一系列具有唯一标识的业务模型,具有状态或者生命周期的概念,其身份是持久的,即使对象的某些属性发生变化,其身份也不会改变。

在 domain 层,还有一个重要概念——聚合(Aggregate)。Aggregate 用于将多个具有关联性的 entity 和 value object 组织在一起,确保聚合内的对象在业务处理流程中保持数据的一致性。

对于 application 层而言,访问 domain 层时使用聚合后的 Aggregate,也能降低因为 entity 数量更多而导致的业务理解成本。

Bounded context

最后要介绍的是 DDD 中的“限界上下文”(bounded context)。限界上下文与业务子域是具有强关联性的两个概念:

  • 业务子域(Subdomain): 子域代表了一个特定的业务领域,具有自己专属独特的业务概念、规则和流程,其关注的是业务领域本身,与技术实现无关,更多地关注业务逻辑和规则;
  • 限界上下文(Bounded Context): 限界上下文定义了业务领域的边界,保证领域内的模型含义与职责边界清晰一致。限界上下文属于针对业务子域在实现层面给出的解决方案,与技术实现密切相关,需要考虑如何在技术层面实现和管理模型。

在理想情况下,一个业务子域和一个限界上下文是一一对应的,但将理论落到实地之后还是不可避免存在差异部分,可能存在业务子域与限界上下文之间存在一对多或者多对一的情况。

context

DDD 项目基本结构

TEXT
project/
├── cmd/
│   └── server/
│       └── main.go                 # 程序启动入口
│
├── internal/
│   ├── interfaces/                 # 用户接口层 Presentation
│   │   ├── http/
│   │   │   ├── handler/
│   │   │   │   ├── order_handler.go
│   │   │   │   └── user_handler.go
│   │   │   ├── dto/
│   │   │   │   ├── order_request.go
│   │   │   │   └── order_response.go
│   │   │   └── router.go
│   │   ├── grpc/
│   │   │   └── order_server.go
│   │   └── consumer/
│   │       └── order_consumer.go
│   │
│   ├── application/                # 应用层 Application
│   │   ├── order/
│   │   │   ├── service.go
│   │   │   ├── command.go
│   │   │   └── query.go
│   │   └── user/
│   │       └── service.go
│   │
│   ├── domain/                     # 领域层 Domain
│   │   ├── order/
│   │   │   ├── aggregate.go
│   │   │   ├── entity.go
│   │   │   ├── value_object.go
│   │   │   ├── service.go
│   │   │   ├── repository.go
│   │   │   └── event.go
│   │   └── user/
│   │       ├── entity.go
│   │       ├── repository.go
│   │       └── service.go
│   │
│   ├── infrastructure/             # 基础设施层 Infrastructure
│   │   ├── persistence/
│   │   │   ├── mysql/
│   │   │   │   ├── model/
│   │   │   │   │   └── order_po.go
│   │   │   │   ├── order_repository.go
│   │   │   │   └── user_repository.go
│   │   │   └── redis/
│   │   │       └── order_cache.go
│   │   ├── messaging/
│   │   │   └── kafka/
│   │   │       └── order_publisher.go
│   │   ├── client/
│   │   │   ├── payment_client.go
│   │   │   └── inventory_client.go
│   │   └── transaction/
│   │       └── gorm_transaction.go
│   │
│   ├── bootstrap/                  # 依赖组装
│   │   ├── app.go
│   │   ├── database.go
│   │   └── wire.go
│   │
│   └── shared/                     # 项目内部通用能力
│       ├── errs/
│       ├── idgen/
│       └── clock/
│
├── migrations/                     # 数据库迁移
├── configs/                        # 配置文件
├── api/                            # OpenAPI / Proto
├── go.mod
└── go.sum
flowchart TB
    Interfaces["接口层<br/>Handler / Controller / DTO"]
    Application["应用层<br/>Application Service / Use Case"]
    Domain["领域层<br/>Entity / Aggregate / Repository Interface"]
    Infrastructure["基础设施层<br/>Repository Impl / GORM / Redis / MQ"]

    Interfaces --> Application
    Application --> Domain
    Infrastructure -. "实现领域层接口" .-> Domain

对于更重的项目,可以按照限界上下文拆分,而不一定要把所有业务都放在一个 domain 和 application 文件夹中:

TEXT
internal/
├── order/
│   ├── interfaces/
│   │   └── http/
│   │       └── handler.go
│   ├── application/
│   │   ├── create_order.go
│   │   └── cancel_order.go
│   ├── domain/
│   │   ├── order.go
│   │   ├── order_item.go
│   │   ├── repository.go
│   │   └── service.go
│   └── infrastructure/
│       ├── mysql_repository.go
│       └── payment_client.go
│
├── user/
│   ├── interfaces/
│   ├── application/
│   ├── domain/
│   └── infrastructure/
│
└── bootstrap/
    └── app.go

内容参考自小徐先生,本篇仅作为理论指引,后面我应该会单出一篇介绍我们的具体项目案例,我们目前的微服务基本都是基于 DDD 架构设计开发。

评论区