面试追问地图

主问题必讲关键点下一层追问
分层依赖方向、职责、事务Manager 是否必要、跨层调用
限界上下文模型和语言边界如何从业务识别边界
实体/值对象身份、不可变、合法性值对象如何持久化
聚合根一致性边界、外部访问入口聚合大小、跨聚合一致性
Repository聚合持久化抽象与 DAO 区别、复杂查询
贫血/充血模型规则位置、可维护性巨型 Service 如何重构
领域事件已发生事实、解耦Outbox、幂等、乱序
防腐层模型转换、隔离外部概念与普通 Adapter 的区别
状态机合法状态和转换并发更新、补偿流程
DDD 取舍业务复杂度和团队能力何时只采用部分实践

DDD 题不要只背名词,应选择一个真实业务对象说明边界、不变量、事务和事件。


一、分层与职责

常见后端分层如何划分?

难度 🟡

  • Controller/API:协议转换、参数校验、鉴权入口。
  • Application/Service:用例编排、事务边界。
  • Domain:业务规则和领域模型。
  • Infrastructure:数据库、消息、外部服务实现。

分层的目标是控制依赖和变化,不是单纯创建更多包。

Controller、Service、Manager、DAO 如何分工?

  • Controller 不承载核心业务。
  • Service 表达业务用例和事务。
  • Manager 可封装跨 DAO、第三方或通用能力,但不能成为无边界杂物层。
  • DAO/Repository 负责持久化访问。

如果所有逻辑都堆在 Service,应继续按业务能力拆分,而不是只把代码搬到 Manager。

DTO、DO、VO 是否必须全部存在?

不必机械转换。需要隔离以下变化时再分离:

  • 外部接口契约。
  • 数据库存储结构。
  • 领域模型。
  • 页面展示结构。

简单 CRUD 中过多模型转换会增加维护成本。


二、DDD 基础

什么是限界上下文?

难度 🔴

限界上下文定义一个领域模型和术语有效的边界。

同一个词在不同上下文可以有不同含义,例如:

  • 订单上下文关注交易状态。
  • 物流上下文关注运单和配送状态。

微服务边界应优先参考业务上下文,而不是按数据库表拆分。

实体和值对象的区别?

  • 实体由身份标识区分,属性变化后仍是同一个对象。
  • 值对象由全部属性定义,没有独立身份,通常设计为不可变。

金额、坐标、时间区间常适合值对象,可在构造时维护合法性。

什么是聚合和聚合根?

聚合是一组需要保持一致性的领域对象,外部只能通过聚合根修改内部状态。

聚合边界决定:

  • 单个事务修改范围。
  • 一致性规则。
  • Repository 的保存单位。

聚合过大会造成锁竞争和加载成本,过小则把强一致规则推到分布式协调。

Repository 的职责是什么?

Repository 为领域层提供类似集合的持久化抽象:

  • 按业务标识获取聚合。
  • 保存聚合。
  • 隐藏数据库访问细节。

复杂报表查询不必强行通过聚合 Repository,可使用独立查询模型。


三、贫血模型与充血模型

贫血模型和充血模型的区别?

  • 贫血模型:对象主要保存数据,业务逻辑集中在 Service。
  • 充血模型:实体和值对象同时封装状态和业务规则。

充血模型有利于维护不变量,但并不意味着把所有流程和外部调用塞进实体。

如何避免“巨型 Service”?

  • 将业务规则移入实体和值对象。
  • 将跨实体但属于领域的规则放入 Domain Service。
  • Application Service 只编排用例。
  • 将不同业务用例拆为独立服务或命令处理器。

Domain Service 什么时候使用?

当一条领域规则:

  • 不自然属于某个实体或值对象。
  • 涉及多个领域对象。
  • 仍然是纯业务规则,而不是技术调用。

数据库、MQ 和 HTTP 调用属于基础设施,不应直接放进 Domain Service。


四、事务与领域事件

事务边界应该放在哪里?

通常放在 Application Service 的一个业务用例上。

事务应:

  • 覆盖必须原子完成的本地修改。
  • 尽量短。
  • 避免在事务中进行慢远程调用。

什么是领域事件?

领域事件表达领域中已经发生且其他部分关心的事实,例如:

OrderPaid
FenceEntered
UserRegistered

事件使用过去式,包含必要业务信息,不直接暴露内部实体。

领域事件如何可靠发布?

本地事务中同时保存业务数据和 Outbox 事件,提交后由后台任务或 CDC 发布。

消费者必须幂等,并处理重复、乱序和失败补偿。

命令和事件有什么区别?

  • 命令:希望某件事发生,可以失败,通常有明确接收者。
  • 事件:某件事已经发生,不应被撤销,可能有多个订阅者。

五、上下文集成

什么是防腐层 ACL?

防腐层位于自身领域与外部系统之间,负责:

  • 模型转换。
  • 协议适配。
  • 错误语义转换。
  • 隔离外部系统概念。

它避免外部系统的数据结构和术语污染自身领域模型。

微服务之间如何共享模型?

不要直接共享数据库实体类。应共享稳定契约:

  • API Schema。
  • 事件 Schema。
  • 独立客户端 SDK。

共享代码库过多会让服务重新产生编译期耦合。

如何处理跨上下文业务流程?

  • 简单同步流程可由 Application Service 编排。
  • 长流程可使用 Saga 或流程管理器。
  • 每个上下文维护自己的数据和事务。
  • 失败通过补偿而不是跨库大事务回滚。

六、设计原则与可维护性

SOLID 如何落地而不是停留在定义?

  • SRP:按变化原因拆分。
  • OCP:通过策略和扩展点新增能力。
  • LSP:子类型不破坏父类型契约。
  • ISP:提供小而聚焦的接口。
  • DIP:业务依赖抽象,基础设施实现抽象。

不要为了原则创建大量只有一个实现的空洞接口,抽象应服务真实变化。

如何判断是否需要设计模式?

先识别重复变化:

  • 多种可替换算法:策略模式。
  • 多步骤可插拔处理:责任链。
  • 创建逻辑复杂:工厂。
  • 跨对象状态通知:观察者。

模式是对变化的组织方式,不是代码越复杂越高级。

如何设计状态机?

  • 明确状态集合。
  • 明确允许的转换。
  • 转换时校验前置状态。
  • 状态变化和关键副作用保持一致。
  • 对非法转换返回明确错误。

数据库更新可使用 WHERE id=? AND status=? 保证并发下的状态前置条件。


七、场景题

如何建模订单取消?

需要区分:

  • 用户主动取消。
  • 超时自动取消。
  • 支付后退款。
  • 履约后不可取消。

领域模型应定义允许的状态转换;定时任务或延迟消息只负责触发命令,不能绕过订单自身规则。

如何建模地理围栏?

可拆分:

  • 围栏实体:标识、状态、业务归属。
  • 几何值对象:圆、多边形、坐标系。
  • 生效时间值对象。
  • 命中策略和通知规则。

坐标转换、空间索引属于基础设施或领域服务,围栏状态变化由聚合维护。

什么时候不适合使用复杂 DDD?

  • 业务只是简单 CRUD。
  • 领域规则少且变化慢。
  • 团队缺乏统一建模能力。
  • 项目生命周期短。

可以只采用限界上下文、值对象、明确事务边界等部分实践,不必一次引入全部战术模式。