你口口声声说在做 DDD——那你的限界上下文到底“边界”在哪儿?
🏆本文收录于《滚雪球学SpringBoot 3》:
https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】https://blog.csdn.net/weixin_43970743/article/details/151115907,你想学习的都被收集在内,快速投入学习!!两不误。
若还想学习更多,可直接前往《滚雪球学SpringBoot(全版本合集)》:https://blog.csdn.net/weixin_43970743/category_11599389.html,涵盖SpringBoot所有版本教学文章。
演示环境说明:
- 开发工具:IDEA 2021.3
- JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
- Spring Boot版本:3.5.4(于25年7月24日发布)
- Maven版本:3.8.2 (或更高)
- Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
- 操作系统:Windows 11
1) 战略设计:用 Spring Boot 多模块把 Bounded Context “钉死”在结构里
先说个很直白的真相:限界上下文如果只写在文档里,那它迟早会变成“情绪价值”。
你需要一种机制,让边界“有形”——能被构建系统看到、能被依赖关系表达、最好还能被 CI 保护。多模块就是最朴素但很有效的办法。
Spring 官方就有一篇 Guide 专门讲 Spring Boot 的 multi-module 项目:一个主应用 + 一个/多个库模块(Jar)来组织结构。
这份 Guide 的落点不是 DDD,但它给了一个关键能力:把“边界”从包级别提升到模块级别。
1.1 推荐结构:一个 BC = 一个子模块(别在一个模块里硬塞多个 BC)
举个电商场景(你可以换成你自己的领域):
acme-shop/
pom.xml (parent)
bootstrap/ # 只做启动、装配、配置(薄)
order-bc/ # Bounded Context: Order
inventory-bc/ # Bounded Context: Inventory
payment-bc/ # Bounded Context: Payment
shared-kernel/ # (可选)共享内核:非常克制地放通用值对象/接口
为什么我建议 BC 用子模块而不是“一个工程里分几个 package”?
因为模块依赖是“硬”的:
inventory-bc想直接引用order-bc的内部实现?先过 Maven/Gradle 的依赖声明这一关。- 你可以在代码评审里一句话结束争论:“不允许加依赖,这个要用事件/ACL 解决。”
那种感觉,怎么说呢……有点爽😎。
Spring Boot 的多模块示例 Guide 里就是这种“主应用依赖库模块”的结构化方式。
1.2 模块之间怎么通信?先定“规则”,再选“技术”
DDD 的战略设计层面,模块间通信你大致就三种路线:
- 共享内核(Shared Kernel):少量、稳定的共同语言(谨慎使用)
- Open Host Service / Published Language:对外发布稳定 API(比如 HTTP/消息)
- 防腐层(ACL):把外部模型翻译成内部模型(后面第 4 节细讲)
这里先立个规矩:
- BC 内部:允许富领域模型、允许强一致事务
- BC 之间:优先事件驱动或 ACL,避免直接实体耦合
否则你会得到一个“看似模块化、实则大一统”的怪物:模块在,但语言没边界;上下文在,但模型被污染。
2) 战术实现:Entity、Value Object(Records)、Aggregate Root ——别写成“只有 getter 的展览品”
战术建模最容易踩坑的点是:
你以为你在写 Entity,其实你在写 DTO;你以为你在写聚合,其实你在写数据库表的镜像。
DDD 的对象要承载业务规则,不是只承载字段。
下面我用一个“下单”聚合给你一套可落地代码(Java 17+,Spring Boot 3 默认就是 Java 17 起步)。
2.1 Value Object:用 Java Records 写“值对象”,又短又硬
OpenJDK 的 Records(JEP 395)定位非常明确:它是不可变数据的透明载体,会自动生成构造器、访问器、equals/hashCode/toString 等。
这恰好非常适合 Value Object:不可变 + 值相等。
示例:Money、OrderId(值对象)
package com.acme.order.domain;
import java.math.BigDecimal;
import java.util.Currency;
import java.util.Objects;
public record Money(BigDecimal amount, Currency currency) {
public Money {
Objects.requireNonNull(amount, "amount");
Objects.requireNonNull(currency, "currency");
if (amount.scale() > 2) {
throw new IllegalArgumentException("Money scale must be <= 2");
}
if (amount.signum() < 0) {
throw new IllegalArgumentException("Money cannot be negative");
}
}
public Money add(Money other) {
requireSameCurrency(other);
return new Money(this.amount.add(other.amount), this.currency);
}
private void requireSameCurrency(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
}
}
package com.acme.order.domain;
import java.util.Objects;
import java.util.UUID;
public record OrderId(UUID value) {
public OrderId {
Objects.requireNonNull(value, "orderId");
}
public static OrderId newId() {
return new OrderId(UUID.randomUUID());
}
}
注意我在 record 里写了“紧凑构造器”做校验,这很关键:
值对象的规则要在创建时就成立,否则你会在系统里到处写 if/validate,最后谁都不信任数据。
2.2 Entity:有身份(Identity),状态会变,但规则要收敛在行为里
订单行(OrderLine)通常是实体或聚合内部实体:有 lineId,数量可变,价格不可变(看业务)。
package com.acme.order.domain;
import java.util.Objects;
import java.util.UUID;
public class OrderLine {
private final UUID lineId;
private final String sku;
private int quantity;
private final Money unitPrice;
private OrderLine(UUID lineId, String sku, int quantity, Money unitPrice) {
this.lineId = Objects.requireNonNull(lineId);
this.sku = Objects.requireNonNull(sku);
if (quantity <= 0) throw new IllegalArgumentException("quantity must be > 0");
this.quantity = quantity;
this.unitPrice = Objects.requireNonNull(unitPrice);
}
public static OrderLine create(String sku, int quantity, Money unitPrice) {
return new OrderLine(UUID.randomUUID(), sku, quantity, unitPrice);
}
public void increase(int delta) {
if (delta <= 0) throw new IllegalArgumentException("delta must be > 0");
this.quantity += delta;
}
public Money lineTotal() {
return new Money(unitPrice.amount().multiply(new java.math.BigDecimal(quantity)), unitPrice.currency());
}
// getters(尽量少暴露可变状态的直接 setter)
public UUID lineId() { return lineId; }
public String sku() { return sku; }
public int quantity() { return quantity; }
public Money unitPrice() { return unitPrice; }
}
2.3 Aggregate Root:业务一致性边界,别让外界随便拿内部实体“做手工活”
订单聚合作为 Aggregate Root:
- 保证不变量(例如:状态流转合法、不能对已支付订单再改价)
- 控制内部集合的修改入口
- 负责发布领域事件(下一节会用
AbstractAggregateRoot)
3) Domain Events:用 AbstractAggregateRoot 把“领域事件发布”变成标准动作
领域事件这东西,写对了像开挂:解耦、可扩展、可审计;写歪了像灾难:到处发 event、到处监听、业务链路变成“玄学漂流瓶”。
Spring Data 在这块给了你很官方、很顺滑的一套机制。
Spring Data 文档专门有一节讲“从聚合根发布领域事件”:你可以用 @DomainEvents 暴露事件集合,并用 @AfterDomainEventPublication 在发布后做清理。
同时,AbstractAggregateRoot 作为聚合根基类,提供 registerEvent(...) 来收集领域事件,并通过 domainEvents() 暴露。
换句话说:你不需要自己造“事件列表 + 清理逻辑”的轮子,Spring Data 已经把常见模式封装好了。
3.1 聚合根示例:Order 继承 AbstractAggregateRoot
package com.acme.order.domain;
import org.springframework.data.domain.AbstractAggregateRoot;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class Order extends AbstractAggregateRoot<Order> {
private OrderId id;
private OrderStatus status;
private final List<OrderLine> lines = new ArrayList<>();
protected Order() {} // for JPA if needed
private Order(OrderId id) {
this.id = Objects.requireNonNull(id);
this.status = OrderStatus.DRAFT;
}
public static Order draft() {
return new Order(OrderId.newId());
}
public void addLine(String sku, int qty, Money unitPrice) {
ensureStatus(OrderStatus.DRAFT);
lines.add(OrderLine.create(sku, qty, unitPrice));
}
public Money total() {
Money sum = new Money(java.math.BigDecimal.ZERO, java.util.Currency.getInstance("USD"));
for (var line : lines) {
sum = sum.add(line.lineTotal());
}
return sum;
}
public void place() {
ensureStatus(OrderStatus.DRAFT);
if (lines.isEmpty()) {
throw new IllegalStateException("Cannot place order with no lines");
}
this.status = OrderStatus.PLACED;
// 关键:注册领域事件(不会立刻发,交给 Spring Data 在 repository 保存后发布)
registerEvent(new OrderPlaced(this.id.value()));
}
private void ensureStatus(OrderStatus expected) {
if (this.status != expected) {
throw new IllegalStateException("Invalid state transition: " + status + " -> expected " + expected);
}
}
public OrderId id() { return id; }
public OrderStatus status() { return status; }
public List<OrderLine> lines() { return List.copyOf(lines); }
}
package com.acme.order.domain;
import java.util.UUID;
public record OrderPlaced(UUID orderId) {}
package com.acme.order.domain;
public enum OrderStatus {
DRAFT, PLACED, PAID, CANCELLED
}
3.2 为什么这个发布方式“靠谱”?
因为 Spring Data 的域事件机制是围绕聚合根与仓库生命周期设计的:
- 聚合根收集事件(
registerEvent/@DomainEvents) - 仓库保存聚合时发布事件(作为 Spring 应用事件)
- 发布后可触发回调做清理(
@AfterDomainEventPublication)
这套设计最大的好处是:事件跟聚合的状态变更绑定得很紧,不会出现“状态没落库但事件先飞出去”的尴尬(当然,你仍然要考虑事务边界与最终一致,这属于架构层面的选择)。
4) Repository 模式 + Spring Data 的防腐层(ACL):别让 Spring Data 的接口“污染”你的领域语言
这一节我想讲得更“狠”一点:
很多项目所谓的 Repository,其实就是把 JpaRepository 直接暴露给领域/应用层,然后大家开始在服务里写:
orderRepo.findByStatusAndCreatedAtBetweenAnd...
写着写着你会发现,你的领域语言变成了“SQL 语言的口音”,而且还夹着 Spring Data 的各种细节。
这就是模型被“基础设施”反向绑架的经典现场。
Spring Data 的 Repository 抽象本意是减少数据访问样板代码,提供一致的仓库接口概念。
但 DDD 的 Repository 模式强调:仓库是领域概念的一部分(集合的抽象),而不是数据库查询 DSL。
4.1 目标:领域层定义 Repository 接口,基础设施层用 Spring Data 去实现(适配器)
推荐分层(按 DDD 常见四层):
order-bc
├─ domain
│ ├─ Order (Aggregate Root)
│ ├─ OrderRepository (领域接口)
│ └─ ...
├─ application
│ ├─ PlaceOrderService
│ └─ ...
└─ infrastructure
├─ JpaOrderEntity / mapping
├─ SpringDataOrderJpaRepository
└─ OrderRepositoryAdapter (实现 domain.OrderRepository)
4.2 领域层:定义“说人话”的仓库接口
package com.acme.order.domain;
import java.util.Optional;
import java.util.UUID;
public interface OrderRepository {
Optional<Order> findById(UUID id);
void save(Order order);
}
你看,这接口很朴素,但它的好处是:
- 它不暴露 Spring Data
- 它不强迫领域知道 JPA Entity / 注解 / 懒加载
- 它只表达“领域需要什么集合能力”
4.3 基础设施层:Spring Data 只在这里出现(防腐层的第一步)
Spring Data 的文档明确说明了仓库抽象的核心概念与接口体系(比如 Repository、CrudRepository 等)。
但我们把它关在 infrastructure 里:
package com.acme.order.infrastructure;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
interface SpringDataOrderJpaRepository extends JpaRepository<OrderJpaEntity, UUID> {}
接着写适配器,把 Spring Data 的 Entity 映射为领域模型:
package com.acme.order.infrastructure;
import com.acme.order.domain.Order;
import com.acme.order.domain.OrderRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
class OrderRepositoryAdapter implements OrderRepository {
private final SpringDataOrderJpaRepository jpa;
OrderRepositoryAdapter(SpringDataOrderJpaRepository jpa) {
this.jpa = jpa;
}
@Override
public Optional<Order> findById(UUID id) {
return jpa.findById(id).map(OrderMapper::toDomain);
}
@Override
public void save(Order order) {
jpa.save(OrderMapper.toJpa(order));
}
}
OrderMapper 负责隔离 JPA 注解与领域对象结构(这就是防腐层的“翻译官”角色)。
4.4 为什么这算“防腐层”?
因为你的领域模型不会被这些东西污染:
- JPA 的
@Entity/@Id/@ManyToOne - Spring Data 的查询方法命名规则
- 延迟加载、脏检查、Entity 生命周期等持久化细节
更现实一点说:
你以后要把 JPA 换成别的存储,或者要引入 CQRS 读模型,领域层基本不用动——痛苦主要集中在 infrastructure,这就叫“把复杂留给边界”。
结尾:落地 DDD 的关键不是“写了多少战术名词”,而是“你敢不敢守住边界”
如果你读到这里有一点点共鸣,那我想再送你一句非常不性感但很实用的话:
DDD 不是一次性重构,是持续的边界维护。
你可以从三件小事开始(真的不夸张):
- 先把 BC 切到多模块(让边界有形)
- 把 Value Object 用 records 写起来(让不变量早失败)
- 聚合根统一用
AbstractAggregateRoot管领域事件(让事件发布有标准姿势)
🧧福利赠与你🧧
无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。
最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。
同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。
ps:本文涉及所有源代码,均已上传至Gitee:
https://gitee.com/bugjun01/SpringBoot-demo开源,供同学们一对一参考 Gitee传送门https://gitee.com/bugjun01/SpringBoot-demo,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗
🫵 Who am I?
我是 bug菌:
- 热活跃于 CSDN:
https://blog.csdn.net/weixin_43970743| 掘金:https://juejin.cn/user/695333581765240| InfoQ:https://www.infoq.cn/profile/4F581734D60B28/publish| 51CTO:https://blog.51cto.com/u_15700751| 华为云:https://bbs.huaweicloud.com/community/usersnew/id_1582617489455371| 阿里云:https://developer.aliyun.com/profile/uolxikq5k3gke| 腾讯云:https://cloud.tencent.com/developer/user/10216480/articles等技术社区; - CSDN 博客之星 Top30、华为云多年度十佳博主&卓越贡献奖、掘金多年度人气作者 Top40;
- 掘金、InfoQ、51CTO 等平台签约及优质作者;
- 全网粉丝累计 30w+。
更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看:https://bbs.csdn.net/topics/612438251 👈️
硬核技术公众号 「猿圈奇妙屋」https://bbs.csdn.net/topics/612438251 期待你的加入,一起进阶、一起打怪升级。
- End -
- 点赞
- 收藏
- 关注作者
评论(0)