某金融科技公司正在构建新一代交易系统。新系统使用领域驱动设计,模型清晰、代码整洁。然而它必须对接一套运行了 15 年的核心银行系统(Core Banking System)——这套系统的接口返回 COBOL 风格的定长字段,状态码用两位数字表示(“01”=正常、“02”=冻结、“99”=未知),金额用”分”而非”元”为单位。如果新系统直接使用这些数据结构,精心设计的领域模型将被老系统的”语言”侵蚀。防腐层(Anti-Corruption Layer)正是为解决这类问题而存在的。
本文深入讨论两种互补的上下文映射模式——防腐层(Anti-Corruption Layer,ACL)和开放主机服务(Open Host Service,OHS),分析其实现架构、适用场景和工程实践。
上一篇:领域事件与事件风暴 | 下一篇:CQRS + Event Sourcing 完整实战
一、为什么需要防腐层
1.1 直接集成的代价
当系统 A 直接调用系统 B 的接口并使用系统 B 的数据结构时,会发生以下问题:
| 问题 | 说明 |
|---|---|
| 模型污染 | 系统 B 的概念侵入系统 A 的领域模型 |
| 语义扭曲 | 系统 B 的字段命名和类型定义影响系统 A 的通用语言 |
| 变更耦合 | 系统 B 的接口变更直接导致系统 A 的代码修改 |
| 测试困难 | 测试系统 A 必须依赖系统 B 的运行环境 |
| 认知负载 | 开发者需要同时理解两套模型 |
1.2 一个具体的例子
假设新系统的领域模型中,账户(Account)是这样定义的:
// 新系统的领域模型——清晰、语义明确
public class Account {
private AccountId id;
private AccountHolder holder;
private Money balance;
private AccountStatus status; // ACTIVE, FROZEN, CLOSED
private Currency currency;
}而遗留核心银行系统的接口返回的是这样的数据:
{
"ACCT_NO": "00123456789012",
"CUST_CD": "C0001",
"BAL_AMT": 1234500,
"BAL_CCY": "156",
"STAT_CD": "01",
"ACCT_TYP": "SA",
"LST_TXN_DT": "20250101",
"BRN_CD": "001"
}如果直接在领域模型中使用
STAT_CD、BAL_AMT(以分为单位)、BAL_CCY(ISO
数字码而非字母码)这些概念,领域模型将变得面目全非。
二、防腐层的架构
2.1 三层结构
Eric Evans 在原著中定义的防腐层由三个组件构成:
graph LR
subgraph "我的限界上下文"
DS["领域服务<br/>Domain Service"]
end
subgraph "防腐层 ACL"
F["外观<br/>Facade"]
A["适配器<br/>Adapter"]
T["转换器<br/>Translator"]
end
subgraph "外部系统"
ES["遗留系统<br/>Legacy API"]
end
DS -->|"使用领域语言"| F
F -->|"简化接口"| A
A -->|"协议适配"| ES
T -.->|"模型转换"| F
T -.->|"模型转换"| A
- 外观(Facade):为本域提供符合领域语言的接口,屏蔽外部系统的复杂性;
- 适配器(Adapter):处理与外部系统的技术协议(HTTP、SOAP、MQ 等);
- 转换器(Translator):负责外部模型与领域模型之间的双向转换。
2.2 每个组件的职责
| 组件 | 输入 | 输出 | 职责 |
|---|---|---|---|
| Facade | 领域对象 | 领域对象 | 提供符合本域语言的接口 |
| Translator | 外部 DTO ↔︎ 领域对象 | 领域对象 ↔︎ 外部 DTO | 模型映射和数据转换 |
| Adapter | 外部 DTO | 外部 DTO | 网络通信、序列化、错误处理 |
2.3 数据流示意
本域代码 外部系统
│ │
│ 调用 Facade │
│ (领域语言) │
▼ │
Facade │
│ 调用 Translator │
│ (领域对象 → 外部 DTO) │
▼ │
Translator │
│ 调用 Adapter │
│ (外部 DTO) │
▼ │
Adapter ─────── HTTP / MQ / SOAP ─────────▶│
│ │
│◀──────── 响应数据 ───────────────────│
▼ │
Translator │
│ (外部 DTO → 领域对象) │
▼ │
Facade │
│ (返回领域对象) │
▼ │
本域代码 │
三、防腐层的实现
3.1 Java 实现
// ============ Adapter 层:负责网络通信 ============
public class CoreBankingAdapter {
private final HttpClient httpClient;
private final String baseUrl;
public CoreBankingAdapter(HttpClient httpClient, String baseUrl) {
this.httpClient = httpClient;
this.baseUrl = baseUrl;
}
public CoreBankingAccountDto fetchAccount(String accountNumber) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/accounts/" + accountNumber))
.header("Content-Type", "application/json")
.GET()
.build();
try {
HttpResponse<String> response = httpClient.send(
request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new ExternalSystemException(
"核心银行系统返回错误: " + response.statusCode());
}
return objectMapper.readValue(
response.body(), CoreBankingAccountDto.class);
} catch (IOException | InterruptedException e) {
throw new ExternalSystemException(
"调用核心银行系统失败", e);
}
}
}
// ============ 外部系统的 DTO ============
public class CoreBankingAccountDto {
@JsonProperty("ACCT_NO")
private String accountNumber;
@JsonProperty("CUST_CD")
private String customerCode;
@JsonProperty("BAL_AMT")
private long balanceAmount; // 以分为单位
@JsonProperty("BAL_CCY")
private String balanceCurrency; // ISO 数字码
@JsonProperty("STAT_CD")
private String statusCode;
@JsonProperty("LST_TXN_DT")
private String lastTransactionDate; // yyyyMMdd
// getter/setter 省略
}
// ============ Translator 层:负责模型转换 ============
public class AccountTranslator {
private static final Map<String, AccountStatus> STATUS_MAP = Map.of(
"01", AccountStatus.ACTIVE,
"02", AccountStatus.FROZEN,
"03", AccountStatus.CLOSED
);
private static final Map<String, Currency> CURRENCY_MAP = Map.of(
"156", Currency.getInstance("CNY"),
"840", Currency.getInstance("USD"),
"978", Currency.getInstance("EUR")
);
public Account toDomain(CoreBankingAccountDto dto) {
AccountStatus status = STATUS_MAP.get(dto.getStatusCode());
if (status == null) {
throw new TranslationException(
"未知的账户状态码: " + dto.getStatusCode());
}
Currency currency = CURRENCY_MAP.get(dto.getBalanceCurrency());
if (currency == null) {
throw new TranslationException(
"未知的币种代码: " + dto.getBalanceCurrency());
}
// 将"分"转换为"元"
BigDecimal amount = BigDecimal.valueOf(dto.getBalanceAmount())
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
return Account.reconstitute(
new AccountId(dto.getAccountNumber()),
new AccountHolder(dto.getCustomerCode()),
new Money(amount, currency),
status
);
}
}
// ============ Facade 层:提供领域语言接口 ============
public class ExternalAccountService {
private final CoreBankingAdapter adapter;
private final AccountTranslator translator;
public ExternalAccountService(CoreBankingAdapter adapter,
AccountTranslator translator) {
this.adapter = adapter;
this.translator = translator;
}
/**
* 查询外部账户信息,返回本域的 Account 领域对象。
* 本域代码只与 ExternalAccountService 交互,
* 不需要知道核心银行系统的存在。
*/
public Account findAccount(AccountId accountId) {
CoreBankingAccountDto dto = adapter.fetchAccount(
accountId.value());
return translator.toDomain(dto);
}
public boolean isAccountActive(AccountId accountId) {
Account account = findAccount(accountId);
return account.isActive();
}
}3.2 Go 实现
// adapter.go —— 网络通信层
type CoreBankingClient struct {
baseURL string
httpClient *http.Client
}
type coreBankingAccountDTO struct {
AccountNo string `json:"ACCT_NO"`
CustomerCode string `json:"CUST_CD"`
BalanceAmt int64 `json:"BAL_AMT"`
BalanceCcy string `json:"BAL_CCY"`
StatusCode string `json:"STAT_CD"`
}
func (c *CoreBankingClient) FetchAccount(ctx context.Context,
accountNo string) (*coreBankingAccountDTO, error) {
url := fmt.Sprintf("%s/accounts/%s", c.baseURL, accountNo)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("构建请求失败: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("调用核心银行系统失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("核心银行系统返回错误: %d", resp.StatusCode)
}
var dto coreBankingAccountDTO
if err := json.NewDecoder(resp.Body).Decode(&dto); err != nil {
return nil, fmt.Errorf("解析响应失败: %w", err)
}
return &dto, nil
}
// translator.go —— 模型转换层
var statusMap = map[string]AccountStatus{
"01": AccountStatusActive,
"02": AccountStatusFrozen,
"03": AccountStatusClosed,
}
var currencyMap = map[string]string{
"156": "CNY",
"840": "USD",
"978": "EUR",
}
func translateAccount(dto *coreBankingAccountDTO) (*Account, error) {
status, ok := statusMap[dto.StatusCode]
if !ok {
return nil, fmt.Errorf("未知的账户状态码: %s", dto.StatusCode)
}
currencyCode, ok := currencyMap[dto.BalanceCcy]
if !ok {
return nil, fmt.Errorf("未知的币种代码: %s", dto.BalanceCcy)
}
// 分 → 元
amount := decimal.NewFromInt(dto.BalanceAmt).
Div(decimal.NewFromInt(100))
return &Account{
ID: AccountID(dto.AccountNo),
Holder: AccountHolder(dto.CustomerCode),
Balance: NewMoney(amount, currencyCode),
Status: status,
}, nil
}
// facade.go —— 领域语言接口
type ExternalAccountService struct {
client *CoreBankingClient
}
func (s *ExternalAccountService) FindAccount(ctx context.Context,
id AccountID) (*Account, error) {
dto, err := s.client.FetchAccount(ctx, string(id))
if err != nil {
return nil, fmt.Errorf("查询外部账户失败: %w", err)
}
return translateAccount(dto)
}3.3 防腐层的测试策略
防腐层的每一层都需要独立测试:
| 层 | 测试方法 | 验证内容 |
|---|---|---|
| Adapter | WireMock / httptest 模拟外部 API | 网络通信、错误处理、超时 |
| Translator | 单元测试 | 所有已知的映射正确、未知值的错误处理 |
| Facade | 集成测试(Mock Adapter) | 端到端流程、领域对象构建正确 |
// translator 的单元测试
func TestTranslateAccount_Success(t *testing.T) {
dto := &coreBankingAccountDTO{
AccountNo: "00123456789012",
CustomerCode: "C0001",
BalanceAmt: 1234500,
BalanceCcy: "156",
StatusCode: "01",
}
account, err := translateAccount(dto)
assert.NoError(t, err)
assert.Equal(t, AccountID("00123456789012"), account.ID)
assert.Equal(t, AccountStatusActive, account.Status)
assert.Equal(t, "12345.00", account.Balance.Amount().String())
assert.Equal(t, "CNY", account.Balance.Currency())
}
func TestTranslateAccount_UnknownStatus(t *testing.T) {
dto := &coreBankingAccountDTO{
StatusCode: "99",
BalanceCcy: "156",
}
_, err := translateAccount(dto)
assert.Error(t, err)
assert.Contains(t, err.Error(), "未知的账户状态码")
}四、开放主机服务
4.1 定义
开放主机服务(Open Host Service,OHS)是上游上下文提供的一套标准化、版本化的公开协议,供多个下游上下文使用。
如果说防腐层是下游的自我保护机制,那么开放主机服务就是上游的主动服务机制。两者经常配合使用。
4.2 OHS 的设计原则
| 原则 | 说明 |
|---|---|
| 接口稳定 | 公开接口一旦发布,尽量不做破坏性变更 |
| 版本管理 | 支持多版本并存,给下游迁移时间 |
| 文档完善 | 提供完整的 API 文档和使用指南 |
| 独立于内部模型 | 接口数据结构与内部领域模型解耦 |
| 向后兼容 | 新版本应该兼容旧版本的客户端 |
4.3 OHS 的实现形式
┌────────────────────────────────┐
│ 开放主机服务 (OHS) │
│ │
│ ┌──────────────────────────┐ │
│ │ API 层(接口契约) │ │
│ │ - REST / gRPC / GraphQL │ │
│ │ - 版本管理: /v1, /v2 │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ┌──────────▼───────────────┐ │
│ │ 转换层 │ │
│ │ 领域模型 → API 响应模型 │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ┌──────────▼───────────────┐ │
│ │ 领域层 │ │
│ │ 内部领域模型 │ │
│ └──────────────────────────┘ │
└────────────────────────────────┘
4.4 代码示例:OHS 的 REST API
// OHS 的 API 数据传输对象——独立于内部领域模型
public record AccountResponse(
String accountId,
String holderName,
String balance,
String currency,
String status,
String lastUpdated
) {}
// OHS 控制器
@RestController
@RequestMapping("/api/v1/accounts")
public class AccountApiV1Controller {
private final AccountQueryService queryService;
private final AccountApiTranslator translator;
@GetMapping("/{accountId}")
public ResponseEntity<AccountResponse> getAccount(
@PathVariable String accountId) {
Account account = queryService.findById(new AccountId(accountId));
AccountResponse response = translator.toApiResponse(account);
return ResponseEntity.ok(response);
}
}
// OHS 的转换器:领域模型 → API 响应
public class AccountApiTranslator {
public AccountResponse toApiResponse(Account account) {
return new AccountResponse(
account.id().value(),
account.holder().name(),
account.balance().amount().toPlainString(),
account.balance().currency().getCurrencyCode(),
account.status().name().toLowerCase(),
account.lastUpdated().toString()
);
}
}五、发布语言
5.1 定义
发布语言(Published Language,PL)是一种文档化的、版本化的数据交换格式,供多方使用。它通常与开放主机服务配合——OHS 定义”如何通信”,PL 定义”通信的内容格式”。
5.2 常见形式
| 形式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| JSON Schema | 可读性好、生态丰富 | 类型系统较弱 | REST API |
| Protocol Buffers | 强类型、高性能、向后兼容 | 可读性差 | gRPC、消息队列 |
| Avro Schema | 支持 schema 演进 | 需要 schema 注册中心 | Kafka 生态 |
| OpenAPI (Swagger) | 文档和代码生成一体 | 规范复杂 | REST API 文档 |
| GraphQL Schema | 灵活查询 | 服务端实现复杂 | 前后端交互 |
5.3 Protocol Buffers 示例
syntax = "proto3";
package account.api.v1;
option go_package = "github.com/example/account/api/v1";
option java_package = "com.example.account.api.v1";
// 发布语言:账户服务的公开数据格式
service AccountService {
rpc GetAccount(GetAccountRequest) returns (AccountResponse);
rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse);
}
message GetAccountRequest {
string account_id = 1;
}
message AccountResponse {
string account_id = 1;
string holder_name = 2;
MoneyValue balance = 3;
AccountStatus status = 4;
google.protobuf.Timestamp last_updated = 5;
}
message MoneyValue {
string amount = 1; // 使用字符串避免浮点精度问题
string currency = 2; // ISO 4217 字母码
}
enum AccountStatus {
ACCOUNT_STATUS_UNSPECIFIED = 0;
ACCOUNT_STATUS_ACTIVE = 1;
ACCOUNT_STATUS_FROZEN = 2;
ACCOUNT_STATUS_CLOSED = 3;
}六、ACL vs Conformist vs Separate Ways 的选择
6.1 决策框架
flowchart TD
START["需要对接外部系统"] --> Q1{"外部系统的模型<br/>是否可以接受?"}
Q1 -->|"完全可以接受"| CF["遵从者<br/>Conformist"]
Q1 -->|"部分可接受"| Q2{"集成价值是否<br/>大于 ACL 成本?"}
Q1 -->|"完全不可接受"| Q2
Q2 -->|是| Q3{"外部系统是否<br/>提供 OHS?"}
Q2 -->|否| SW["各行其道<br/>Separate Ways"]
Q3 -->|是| ACL_OHS["ACL + 对接 OHS"]
Q3 -->|否| ACL_RAW["ACL + 直接适配"]
CF --> CF_NOTE["风险:模型污染<br/>适合:标准化系统"]
SW --> SW_NOTE["风险:数据孤岛<br/>适合:集成价值低"]
ACL_OHS --> ACL_NOTE["风险:维护成本<br/>适合:遗留系统对接"]
ACL_RAW --> ACL_NOTE
6.2 对比总结
| 维度 | ACL | Conformist | Separate Ways |
|---|---|---|---|
| 实现成本 | 高(需要建设翻译层) | 低(直接使用) | 无 |
| 维护成本 | 中(翻译层需跟随外部变更) | 低 | 无 |
| 模型纯净度 | 高(完全隔离) | 低(被外部模型污染) | 高(无集成) |
| 适用场景 | 遗留系统、外部 API | 强势第三方标准 | 集成价值低 |
| 风险 | 翻译层可能不完整 | 领域模型退化 | 数据不一致 |
七、工程案例:对接遗留 ERP 系统
7.1 背景
某制造企业的新一代供应链管理系统需要对接运行了 12 年的 SAP ERP 系统。ERP 系统通过 RFC(Remote Function Call)提供接口,数据格式为定长的 BAPI 结构。
7.2 挑战
| 挑战 | 具体表现 |
|---|---|
| 数据格式 | BAPI 结构使用定长字段,字段名缩写晦涩 |
| 编码体系 | 物料编码、工厂编码、供应商编码使用内部编号 |
| 业务语义 | “移动类型”(Movement Type)用三位数字编码 |
| 性能 | RFC 调用延迟高(200-500ms) |
| 可用性 | ERP 系统有维护窗口,每月停机一次 |
7.3 ACL 架构设计
┌─────────────────────────────────────────────────────┐
│ 新供应链系统 │
│ │
│ ┌─────────────┐ │
│ │ 采购域 │ │
│ │ 领域服务 │──── 使用领域语言 ────┐ │
│ └─────────────┘ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ 防腐层 (ACL) │ │
│ │ │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ Facade │ │ Translator│ │ Adapter │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ 提供领域 │ │ BAPI ↔ │ │ JCo/RFC │ │ │
│ │ │ 语言接口 │ │ 领域模型 │ │ 通信 │ │ │
│ │ └───────────┘ └───────────┘ └───────────┘ │ │
│ │ │ │
│ │ ┌───────────┐ │ │
│ │ │ Cache │ ← 缓解 RFC 延迟 │ │
│ │ └───────────┘ │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
│
RFC / BAPI
│
▼
┌──────────────────┐
│ SAP ERP │
│ (遗留系统) │
└──────────────────┘
7.4 关键实现
// ERP Adapter:封装 JCo/RFC 调用
public class SapMaterialAdapter {
private final JCoDestination destination;
public BapiMaterialDto getMaterial(String materialNumber) {
JCoFunction function = destination
.getRepository()
.getFunction("BAPI_MATERIAL_GET_DETAIL");
function.getImportParameterList()
.setValue("MATERIAL", materialNumber);
function.execute(destination);
JCoStructure header = function.getExportParameterList()
.getStructure("MATERIAL_GENERAL_DATA");
return new BapiMaterialDto(
header.getString("MATERIAL"),
header.getString("MATL_DESC"),
header.getString("BASE_UOM"),
header.getString("MATL_GROUP"),
header.getString("MATL_TYPE")
);
}
}
// Translator:BAPI 结构 → 领域对象
public class MaterialTranslator {
public Material toDomain(BapiMaterialDto bapi) {
return Material.reconstitute(
new MaterialId(bapi.materialNumber().trim()),
bapi.description().trim(),
mapUnitOfMeasure(bapi.baseUom()),
mapMaterialGroup(bapi.materialGroup()),
mapMaterialType(bapi.materialType())
);
}
private UnitOfMeasure mapUnitOfMeasure(String sapUom) {
return switch (sapUom.trim()) {
case "EA" -> UnitOfMeasure.EACH;
case "KG" -> UnitOfMeasure.KILOGRAM;
case "M" -> UnitOfMeasure.METER;
case "L" -> UnitOfMeasure.LITER;
default -> throw new TranslationException(
"未知的 SAP 计量单位: " + sapUom);
};
}
}
// Facade:领域语言接口 + 缓存
public class ExternalMaterialService {
private final SapMaterialAdapter adapter;
private final MaterialTranslator translator;
private final Cache<String, Material> cache;
public Material findMaterial(MaterialId id) {
return cache.get(id.value(), key -> {
BapiMaterialDto dto = adapter.getMaterial(key);
return translator.toDomain(dto);
});
}
}7.5 效果
| 指标 | 无 ACL | 有 ACL |
|---|---|---|
| 领域模型可读性 | 差(充斥 SAP 概念) | 好(纯领域语言) |
| 外部变更影响范围 | 全系统 | 仅 ACL |
| 单元测试覆盖率 | 30%(依赖 SAP 环境) | 85%(可 Mock) |
| 新人上手时间 | 3 周(需学习 SAP) | 1 周(只需学领域) |
| RFC 调用延迟 | 直接承受 300ms | 缓存后 < 5ms |
八、防腐层的演进模式
8.1 渐进式引入
不需要一次性为所有外部集成建立防腐层。推荐的演进路径:
阶段 1:识别最痛的集成点
├── 哪些外部模型对领域污染最严重?
└── 哪些集成点变更最频繁?
阶段 2:为最痛点建立 ACL
├── 先建 Translator,确保模型转换正确
├── 再建 Adapter,封装通信细节
└── 最后建 Facade,提供领域语言接口
阶段 3:逐步扩展到其他集成点
阶段 4:定期清理和优化
├── 外部系统升级时更新 Translator
└── 下线不再使用的映射逻辑
8.2 与 Strangler Fig 模式结合
当目标是逐步替换遗留系统时,防腐层可以与绞杀者模式(Strangler Fig Pattern)结合使用:
graph TB
subgraph "阶段 1:ACL 隔离"
NEW1["新系统"] -->|"ACL"| OLD1["遗留系统<br/>(全部功能)"]
end
subgraph "阶段 2:部分迁移"
NEW2["新系统<br/>(部分功能)"] -->|"ACL"| OLD2["遗留系统<br/>(部分功能)"]
end
subgraph "阶段 3:完全替换"
NEW3["新系统<br/>(全部功能)"]
OLD3["遗留系统<br/>(已下线)"]
end
OLD1 -.->|"演进"| OLD2
OLD2 -.->|"演进"| OLD3
九、综合权衡
| 维度 | ACL(重型) | ACL(轻型) | 直接集成 |
|---|---|---|---|
| 实现成本 | 高(三层完整实现) | 中(简化的转换层) | 低 |
| 维护成本 | 中 | 低 | 高(变更扩散) |
| 隔离效果 | 完全隔离 | 部分隔离 | 无隔离 |
| 适用场景 | 核心域对接遗留系统 | 支撑域对接标准 API | 通用域、低风险 |
| 测试友好 | 好 | 中 | 差 |
| 性能影响 | 有转换开销 | 较小 | 无 |
| 团队理解成本 | 中(需理解 ACL 模式) | 低 | 低(但后期高) |
| 维度 | OHS + PL | 定制 API | 无 API |
|---|---|---|---|
| 实现成本 | 高(需要文档和版本管理) | 中 | 低 |
| 下游适配成本 | 低(标准化) | 高(每个下游定制) | 最高 |
| 可扩展性 | 高(新下游直接接入) | 低 | 无 |
| 适用场景 | 一对多服务 | 一对一定制 | 内部模块 |
十、总结与下一步
防腐层和开放主机服务是 DDD 上下文映射中最具工程实践价值的两种模式。ACL 保护下游不被上游的”烂模型”污染,OHS 让上游以标准化的方式服务多个下游。两者经常组合使用:上游提供 OHS,下游通过 ACL 消费。
在下一篇文章中,我们将深入 CQRS + Event Sourcing 的完整实现,探讨事件存储设计、投影重建和事件版本化等工程细节。
参考资料
- Evans, Eric. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003.
- Vernon, Vaughn. Implementing Domain-Driven Design. Addison-Wesley, 2013.
- Fowler, Martin. “Anti-Corruption Layer.” martinfowler.com.
- Hohpe, Gregor; Woolf, Bobby. Enterprise Integration Patterns. Addison-Wesley, 2003.
- Newman, Sam. Building Microservices. 2nd ed., O’Reilly, 2021.
- 张逸.《解构领域驱动设计》. 人民邮电出版社, 2021.
- Richardson, Chris. Microservices Patterns. Manning, 2018.
- Gamma, Erich 等. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994.
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【系统架构设计百科】复杂性管理:架构的核心战场
系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略
【系统架构设计百科】DDD 战略设计:限界上下文与上下文映射
一个中型电商系统里,"订单"在交易团队意味着"待支付的购物车快照",在物流团队意味着"等待拣货的配送单",在财务团队意味着"一条应收账款记录"。三个团队共用同一张 torder 表、同一个 OrderService 类,每次迭代都互相踩脚。这种混乱的根源不是代码质量,而是缺少一项最基本的架构决策——限界上下文(Boun…
【系统架构设计百科】DDD 战术模式:聚合、实体与值对象
某团队在实施领域驱动设计时,把整个"订单"建模为一个聚合根(Aggregate Root),其中包含订单基本信息、所有订单行、配送信息、支付记录、物流轨迹、评价数据。结果这个聚合加载一次需要从 7 张表联查,保存一次需要锁定整个订单树。并发下单高峰期,数据库锁等待飙升至秒级。这就是典型的"大聚合"反模式——聚合的边界画…
【系统架构设计百科】DDD 与微服务:用领域模型划分服务边界
某电商团队按数据库表拆分微服务——用户服务管 tuser,商品服务管 tproduct,订单服务管 torder。看起来边界清晰,实际运行中却发现:下单需要同步调用商品服务查价格、调用库存服务检查库存、调用优惠服务算折扣、调用用户服务查地址,一个下单请求扇出 4 次 RPC,任意一个服务超时整条链路就失败。这种"一实体…