密封接口定义限界上下文内领域事件,将合法事件类型(如ordercreated、orderpaid等)变为编译器可检查的契约;用sealed interface+record确保不可变性与穷尽处理;跨上下文通信须经防腐层转换,禁止直接复用或序列化原始类型。

用密封接口定义限界上下文内的领域事件,核心是把“这个上下文里只允许发生哪些事”变成编译器能检查的事实,而不是靠文档或约定来提醒。
明确列出所有合法事件类型
在某个限界上下文(比如订单上下文)中,先梳理出领域专家确认的、真实发生的业务事实。例如:订单已创建、订单已支付、订单已发货、订单已取消。这些不是开发脑补的,而是来自知识提炼环节。
- 用 sealed interface 定义事件顶层契约,如 public sealed interface OrderEvent permits OrderCreated, OrderPaid, OrderShipped, OrderCancelled {}
- 每个具体事件用 record 实现,天然不可变且语义清晰,如 public record OrderCreated(String orderId, Instant occurredAt) implements OrderEvent {}
- 所有 permits 类必须与接口同模块(Java 中推荐同包),否则编译直接报错
强制消费方穷尽处理每种事件
当另一个上下文(如库存上下文)需要响应订单事件时,不能靠 if-else 或 instanceof 碰运气——必须显式覆盖全部合法变体。
- 用 switch 表达式处理,编译器自动检查是否遗漏分支:switch (event) { case OrderCreated e -> reserveStock(e.orderId()); case OrderPaid e -> confirmPayment(e.orderId()); ... }
- 禁止写 default 分支,它会掩盖对新增事件类型的无知;如果真有兜底逻辑,应显式加一个如 UnknownOrderEvent 的占位类型,并纳入 permits 列表
- IDE 会实时高亮未覆盖分支,比代码评审或测试覆盖率更早发现问题
隔离跨上下文事件传递路径
密封接口本身不跨上下文复用。订单上下文的 OrderEvent 和物流上下文的 ShipmentEvent 是完全独立的类型体系,哪怕字段名一样也不能混用。
- 两个上下文之间通信必须经过防腐层(ACL):订单上下文对外只暴露自己的 OrderEvent 密封族;物流上下文需定义自己的接收协议,如 LogisticsCommand,并由 ACL 负责转换
- 避免在消息序列化层(如 Kafka)直接传输原始密封类型——它们是 Java 类型系统内部契约,应转为 JSON Schema 或 Avro 协议后再跨边界
- 若需扩展新事件(如增加 OrderRefunded),必须修改 OrderEvent 的 permits 列表并同步更新所有 switch,这自然触发上下游协作评审
配合通用事件结构保持可演进性
每个具体事件 record 可统一实现基础接口,承载通用元数据,同时保留业务语义。
- 定义 interface DomainEvent { String id(); Instant occurredOn(); String type(); },让所有 record 实现它
- 具体事件 record 只聚焦业务属性,如 public record OrderPaid(String orderId, BigDecimal amount, Instant occurredAt) implements OrderEvent, DomainEvent {}
- 这样既满足序列化框架要求(通过通用接口提取元数据),又不破坏密封性——外部无法伪造新类型,也无法绕过类型约束篡改事件内容











