别急着拆微服务:单体架构下基于 Domain 驱动的模块化防腐层设计

封面信息图

在很多创业团队或中早期项目中,我经常见到这样一种惨案:产品刚过 MVP 阶段,DAU 甚至还没破万,技术团队就已经按“微服务最佳实践”拆出了 12 个微服务。紧接着,各种分布式事务、网络调用超时、链路追踪(Tracing)配置、K8s 部署成本、跨服务 JOIN 查询等难题扑面而来。

团队原本只有 4 个后端开发,结果每天一半的时间都在处理基础设施和环境联调,产品迭代速度直接慢了三倍。

在操作系统底层,Unix 哲学强调“高内聚、低耦合”,但在初期也绝不会把每个系统调用拆成跨进程 RPC。对于创业公司而言,模块化单体(Modular Monolith) + 基于领域驱动(DDD)的防腐层设计,才是兼顾迭代效率与长期扩展性的最优解。

本文将演示如何在单体架构中建立严格的领域边界与防腐层(ACL),既能享受单体快速开发、进程内直接调用、原生事务支持的红利,又能在未来业务爆发时,低成本将模块剥离成独立微服务。


一、 为什么盲目微服务是初创公司的毒药?

我们可以算一笔极其现实的研发工程账:

[ 模块化单体架构 (Modular Monolith) ]
┌─────────────────────────────────────────────────────────────┐
│  同一进程空间 / 统一数据库 / 单次事务 (Begin -> Commit)      │
│  [ 用户领域 User ] ──► (防腐层 ACL / Go Interface) ──► [ 支付领域 Payment ]
└─────────────────────────────────────────────────────────────┘
  * 部署: 1 个二进制文件 / 秒级 CI/CD
  * 故障排查: 单机日志 / gdb 或 pprof 直接调
  * 跨领域调用: 内存指针传递 (0 序列化开销, 0 网络超时)

===============================================================

[ 盲目拆分的微服务架构 (Distributed Microservices) ]
┌──────────────┐     gRPC / HTTP (网络延迟 + 重试风暴)    ┌──────────────┐
│  User 独立服务│ ──────────────────────────────────────► │Payment 独立服务│
└──────────────┘                                          └──────────────┘
  * 基础设施: Consul/Nacos + OpenTelemetry + Istio + Seata 分布式事务
  * 运维负担: 10+ 容器镜像编排、跨服务日志聚合分析
  * 团队消耗: 每天排查“为什么服务 A 调服务 B 偶尔超时 504”

二、 领域防腐层(ACL)设计模式

所谓防腐层(Anticorruption Layer, ACL),核心思想是:领域 A 绝不直接依赖领域 B 的底层数据模型(ORM Entity),也不直接调用领域 B 的内部私有逻辑,而是通过一套定义良好的适配器(Adapter)与防腐转换器(Translator)进行通信

架构目录结构规范(以 Go 项目为例)
├── cmd/
│   └── server/main.go
├── internal/
│   ├── order/                  # 订单领域
│   │   ├── domain/             # 订单核心实体与业务规则
│   │   ├── repository/         # 订单数据库持久化
│   │   └── acl/                # 订单对外部领域的防腐适配层
│   │       └── payment_acl.go  # 依赖 payment 的防腐接口实现
│   └── payment/                # 支付领域
│       ├── api/                # 对外暴露的公共 Interface 与 DTO
│       │   └── payment.go
│       └── internal/           # 支付领域内部实现(禁止外部直接 import)

三、 Go 语言实战:基于接口隔离与 DTO 转换的防腐层

1. 支付领域对外暴露的公共契约(Public Contract)
package paymentapi

import "context"

// PaymentDTO 是纯净的对外传输对象,绝不暴露底层的数据库 Model
type PaymentOrderDTO struct {
    PaymentID   string
    OrderID     string
    AmountCents int64
    Status      string
}

// PaymentService 是支付领域对外承诺的接口契约
type PaymentService interface {
    CreateCharge(ctx context.Context, orderID string, amountCents int64) (*PaymentOrderDTO, error)
    QueryStatus(ctx context.Context, paymentID string) (string, error)
}
2. 订单领域内部的防腐层适配器(Order ACL)

订单领域内部定义自己关心的业务概念,通过防腐层将通用支付契约翻译为订单领域的内部语言:

package acl

import (
    "context"
    "errors"
    paymentapi "myproject/internal/payment/api"
)

// OrderPaymentBridge 是订单领域视角下的支付桥接接口
type OrderPaymentBridge interface {
    PayForOrder(ctx context.Context, orderID string, totalAmount int64) (isSuccess bool, payTradeNo string, err error)
}

type paymentACLAdapter struct {
    // 依赖注入支付领域的公用接口
    paymentClient paymentapi.PaymentService
}

func NewPaymentACLAdapter(client paymentapi.PaymentService) OrderPaymentBridge {
    return &paymentACLAdapter{paymentClient: client}
}

// PayForOrder 实现了防腐转换:隔离了外部状态机字段的脏逻辑
func (a *paymentACLAdapter) PayForOrder(ctx context.Context, orderID string, totalAmount int64) (bool, string, error) {
    if a.paymentClient == nil {
        return false, "", errors.New("payment service unavailable")
    }

    // 1. 调用支付模块对外接口
    dto, err := a.paymentClient.CreateCharge(ctx, orderID, totalAmount)
    if err != nil {
        return false, "", err
    }

    // 2. 防腐转换:将 PaymentDTO 的 Status 转换为订单领域的布尔值
    // 如果以后支付服务由进程内调用改为 RPC/HTTP 远程调用,只改这个适配器即可,上层订单业务无感知!
    isSuccess := (dto.Status == "PAID" || dto.Status == "SUCCESS")
    return isSuccess, dto.PaymentID, nil
}

四、 如何在 CI 阶段强制保障模块隔离?

如果只靠口头约定,程序员很容易在急着赶进度时,直接跨包引入私有 struct。我们可以在 GitHub Actions 中引入静态检查工具(如 go-cleanarch 或 ArchUnit),通过规则强制禁止非法导入:

# 安装架构约束检查工具
go install github.com/roblaszczak/go-cleanarch@latest

# 在 CI pipeline 中执行架构合规检查
go-cleanarch ./internal/...

规则示例(arch-rules.yaml):

rules:
  - package: "internal/order/domain"
    forbidden_imports:
      - "internal/payment/internal" # 严禁直接依赖支付模块内部私有实现
      - "internal/order/repository" # 领域实体不得反向依赖持久化层

五、 演进路线:何时真正执行物理拆分?

当业务增长到以下情况时,由于我们已经通过 ACL 将接口、DTO 和领域逻辑严格解耦,拆微服务只需做两件事:

  1. payment 包单独移到一个独立仓库,启动一个 gRPC/HTTP Server。
  2. 编写一个实现 paymentapi.PaymentService 的 gRPC Client 实现类,在 main.go 启动时直接替换注入。

此时,上层几十万行核心业务代码完全不需要动一行,重构周期从几个月缩短为两天,且几乎零线上故障风险

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐