某团队在实施领域驱动设计时,把整个”订单”建模为一个聚合根(Aggregate Root),其中包含订单基本信息、所有订单行、配送信息、支付记录、物流轨迹、评价数据。结果这个聚合加载一次需要从 7 张表联查,保存一次需要锁定整个订单树。并发下单高峰期,数据库锁等待飙升至秒级。这就是典型的”大聚合”反模式——聚合的边界画错了,比不画更糟糕。
战术设计(Tactical Design)是 DDD 中关于领域模型内部组织的一套模式语言。它回答的核心问题是:在一个限界上下文内部,如何用代码精确表达业务规则?本文聚焦于三个最基础的战术构件——聚合(Aggregate)、实体(Entity)和值对象(Value Object),并深入讨论聚合边界的设计原则。
一、实体与值对象
1.1 实体(Entity)
实体的核心特征是身份标识(Identity)。两个实体即使所有属性完全相同,只要身份标识不同,它们就是不同的对象。
实体的判断标准: - 需要在生命周期内被跟踪和区分; - 可以改变其属性值,但身份标识不变; - 相等性基于身份标识,而非属性值。
public class Customer {
private final CustomerId id; // 身份标识,不可变
private String name; // 可变属性
private Email email; // 可变属性
public Customer(CustomerId id, String name, Email email) {
this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name);
this.email = Objects.requireNonNull(email);
}
// 基于身份标识的相等性
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Customer)) return false;
Customer other = (Customer) o;
return this.id.equals(other.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
public void changeName(String newName) {
// 属性变了,但实体的身份不变
this.name = Objects.requireNonNull(newName);
}
}1.2 值对象(Value Object)
值对象的核心特征是值相等性(Value Equality)。两个值对象如果所有属性相同,它们就是相同的。值对象没有独立的身份标识。
值对象的判断标准: - 度量或描述某个事物; - 不可变(Immutable); - 可以被自由替换; - 相等性基于所有属性值。
public final class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
this.amount = Objects.requireNonNull(amount);
this.currency = Objects.requireNonNull(currency);
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new CurrencyMismatchException(this.currency, other.currency);
}
// 返回新对象,不修改原对象
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int quantity) {
return new Money(
this.amount.multiply(BigDecimal.valueOf(quantity)),
this.currency
);
}
// 基于值的相等性
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Money)) return false;
Money money = (Money) o;
return amount.compareTo(money.amount) == 0
&& currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
}1.3 实体还是值对象?判断流程
flowchart TD
START["一个领域概念"] --> Q1{"需要在生命周期内<br/>被跟踪和区分吗?"}
Q1 -->|是| Q2{"会发生状态变化吗?"}
Q1 -->|否| VO["值对象<br/>Value Object"]
Q2 -->|是| ENTITY["实体<br/>Entity"]
Q2 -->|否| Q3{"未来可能需要<br/>独立身份吗?"}
Q3 -->|是| ENTITY
Q3 -->|否| VO
VO --> V_RULES["规则:不可变<br/>值相等<br/>可自由替换"]
ENTITY --> E_RULES["规则:有 ID<br/>身份相等<br/>可变状态"]
1.4 常见的值对象示例
很多开发者习惯把以下概念建模为原始类型或实体,但它们更适合建模为值对象:
| 概念 | 错误建模 | 正确建模 |
|---|---|---|
| 金额 | double price |
Money(amount, currency) |
| 地址 | Address 实体(带 ID) |
Address 值对象 |
| 日期范围 | 两个 Date 字段 |
DateRange(start, end) |
| 邮箱 | String email |
Email(value) 带验证 |
| 坐标 | double lat, double lng |
GeoPoint(lat, lng) |
| 颜色 | String color |
Color(r, g, b) |
| 电话号码 | String phone |
PhoneNumber(countryCode, number) |
1.5 值对象的持久化
值对象没有独立的身份标识,因此在持久化时通常不单独建表,而是嵌入到实体的表中。
-- 值对象嵌入实体表
CREATE TABLE orders (
id UUID PRIMARY KEY,
-- Money 值对象,展开为两列
total_amount DECIMAL(18, 2) NOT NULL,
total_currency VARCHAR(3) NOT NULL,
-- Address 值对象,展开为多列
shipping_street VARCHAR(200),
shipping_city VARCHAR(50),
shipping_state VARCHAR(50),
shipping_zip VARCHAR(20),
shipping_country VARCHAR(2)
);在 JPA 中可以使用 @Embeddable 注解:
@Embeddable
public class Address {
@Column(name = "shipping_street")
private String street;
@Column(name = "shipping_city")
private String city;
@Column(name = "shipping_state")
private String state;
@Column(name = "shipping_zip")
private String zipCode;
@Column(name = "shipping_country")
private String country;
}二、聚合设计
2.1 聚合的定义
聚合(Aggregate)是一组相关对象的集合,作为数据修改的一个单元。聚合有一个根实体——聚合根(Aggregate Root),外部只能通过聚合根访问聚合内部的对象。
聚合的三个核心职责: 1. 一致性边界:聚合内的所有不变条件在每次事务结束时都必须满足; 2. 并发控制边界:聚合是乐观锁或悲观锁的最小粒度; 3. 事务边界:一个事务只修改一个聚合。
2.2 Vaughn Vernon 的四条聚合设计规则
Vaughn Vernon 在《实现领域驱动设计》中提出了四条聚合设计规则,这是目前最被广泛接受的聚合设计指导原则:
规则一:在聚合边界内保护业务不变条件
聚合的边界应该恰好包含那些必须在一个事务中保持一致的业务规则。
public class Order {
private OrderId id;
private List<OrderLine> lines;
private Money totalAmount;
private OrderStatus status;
// 不变条件:总金额必须等于所有行项金额之和
public void addLine(Product product, int quantity, Money unitPrice) {
if (this.status != OrderStatus.DRAFT) {
throw new OrderNotEditableException(this.id);
}
OrderLine line = new OrderLine(product.id(), quantity, unitPrice);
this.lines.add(line);
recalculateTotal(); // 维护不变条件
}
private void recalculateTotal() {
this.totalAmount = lines.stream()
.map(OrderLine::lineTotal)
.reduce(Money.ZERO, Money::add);
}
}规则二:设计小聚合
聚合应该尽可能小。只包含维护不变条件所必需的属性和关联。
错误:大聚合
┌─────────────────────────────────────┐
│ Order (聚合根) │
│ ├── OrderLine (1..N) │
│ ├── Payment (1..N) │
│ ├── Shipment (1..1) │
│ │ └── TrackingEvent (1..N) │
│ ├── Review (0..1) │
│ └── RefundRecord (0..N) │
└─────────────────────────────────────┘
正确:小聚合
┌───────────────┐ ┌───────────────┐
│ Order │ │ Payment │
│ └── OrderLine │ │ │
└───────────────┘ └───────────────┘
┌───────────────┐ ┌───────────────┐
│ Shipment │ │ Review │
│ └── Tracking │ │ │
└───────────────┘ └───────────────┘
规则三:通过 ID 引用其他聚合,而非对象引用
聚合之间不应该持有彼此的对象引用,而应该通过 ID 关联。
// 错误:对象引用
public class Order {
private Customer customer; // 持有了另一个聚合的引用
}
// 正确:ID 引用
public class Order {
private CustomerId customerId; // 只持有 ID
}通过 ID 引用的好处: - 清晰的聚合边界; - 避免级联加载性能问题; - 允许聚合独立存储(不同的数据库、不同的微服务)。
规则四:使用最终一致性更新其他聚合
当一个聚合的状态变化需要引起其他聚合的更新时,使用领域事件(Domain Event)实现最终一致性(Eventual Consistency),而非在同一个事务中修改多个聚合。
// Order 聚合发布领域事件
public class Order {
private List<DomainEvent> domainEvents = new ArrayList<>();
public void confirm() {
this.status = OrderStatus.CONFIRMED;
// 不直接修改库存聚合,而是发布事件
domainEvents.add(new OrderConfirmedEvent(
this.id,
this.lines.stream()
.map(l -> new OrderLineSnapshot(l.productId(), l.quantity()))
.collect(toList())
));
}
public List<DomainEvent> domainEvents() {
return Collections.unmodifiableList(domainEvents);
}
public void clearEvents() {
domainEvents.clear();
}
}
// 事件处理器在另一个事务中更新库存聚合
public class InventoryEventHandler {
public void handle(OrderConfirmedEvent event) {
for (OrderLineSnapshot line : event.lines()) {
Inventory inv = inventoryRepo.findByProductId(line.productId());
inv.reserve(line.quantity());
inventoryRepo.save(inv);
}
}
}2.3 聚合大小的设计启发
如何判断聚合是否太大?以下信号可供参考:
| 信号 | 说明 |
|---|---|
| 加载时间长 | 需要联查多张表才能加载完整聚合 |
| 并发冲突频繁 | 不同业务操作因为锁同一个聚合而互相阻塞 |
| 部分更新 | 经常只修改聚合的一小部分属性 |
| 不同的生命周期 | 聚合内的不同部分有不同的创建和修改时机 |
| 不同的访问频率 | 聚合内的不同部分被访问的频率差异很大 |
2.4 Go 语言中的聚合实现
// 订单聚合
type Order struct {
id OrderID
customerID CustomerID // 通过 ID 引用其他聚合
lines []OrderLine // 聚合内部的实体
totalAmount Money // 值对象
status OrderStatus
version int // 乐观锁版本号
events []DomainEvent
}
// OrderLine 是聚合内部的实体,有本地标识
type OrderLine struct {
lineID LineID
productID ProductID
quantity int
unitPrice Money
}
func (l OrderLine) LineTotal() Money {
return l.unitPrice.Multiply(l.quantity)
}
// 聚合根的方法封装业务规则
func (o *Order) AddLine(productID ProductID, qty int, price Money) error {
if o.status != OrderStatusDraft {
return ErrOrderNotEditable
}
if qty <= 0 {
return ErrInvalidQuantity
}
line := OrderLine{
lineID: NewLineID(),
productID: productID,
quantity: qty,
unitPrice: price,
}
o.lines = append(o.lines, line)
o.recalculateTotal()
return nil
}
func (o *Order) Confirm() error {
if o.status != OrderStatusDraft {
return ErrOrderNotEditable
}
if len(o.lines) == 0 {
return ErrEmptyOrder
}
o.status = OrderStatusConfirmed
o.events = append(o.events, OrderConfirmedEvent{
OrderID: o.id,
CustomerID: o.customerID,
Lines: o.lineSnapshots(),
Total: o.totalAmount,
OccurredAt: time.Now(),
})
return nil
}
func (o *Order) recalculateTotal() {
total := MoneyZero(CNY)
for _, line := range o.lines {
total = total.Add(line.LineTotal())
}
o.totalAmount = total
}
// 外部通过聚合根访问内部对象
func (o *Order) Lines() []OrderLine {
result := make([]OrderLine, len(o.lines))
copy(result, o.lines) // 返回副本,保护内部状态
return result
}三、跨聚合引用的模式
3.1 问题场景
业务中经常需要在操作一个聚合时,引用其他聚合的信息。例如:创建订单时需要验证商品价格和库存。
3.2 模式一:通过应用服务协调
应用服务(Application Service)负责加载多个聚合,并协调它们之间的交互。
public class PlaceOrderService {
private final OrderRepository orderRepo;
private final ProductRepository productRepo;
private final InventoryRepository inventoryRepo;
@Transactional
public OrderId placeOrder(PlaceOrderCommand cmd) {
// 加载所需的聚合
Product product = productRepo.findById(cmd.productId());
Inventory inventory = inventoryRepo.findByProductId(cmd.productId());
// 业务验证
if (!inventory.hasStock(cmd.quantity())) {
throw new InsufficientStockException(cmd.productId());
}
// 创建订单聚合
Order order = Order.create(
cmd.customerId(),
product.id(),
cmd.quantity(),
product.currentPrice()
);
// 注意:这里只保存订单聚合
// 库存的扣减通过领域事件异步完成
orderRepo.save(order);
eventPublisher.publish(order.domainEvents());
return order.id();
}
}3.3 模式二:领域事件驱动
当操作需要跨越多个聚合且不需要强一致性时,使用领域事件。
sequenceDiagram
participant App as 应用服务
participant Order as 订单聚合
participant Bus as 事件总线
participant Inv as 库存聚合
participant Pay as 支付聚合
App->>Order: confirm()
Order-->>App: OrderConfirmedEvent
App->>Bus: publish(event)
Bus->>Inv: handle(OrderConfirmedEvent)
Inv->>Inv: reserve(productId, qty)
Bus->>Pay: handle(OrderConfirmedEvent)
Pay->>Pay: createPayment(orderId, amount)
3.4 模式三:规约(Specification)传入
将跨聚合的验证逻辑封装为规约对象,注入到聚合方法中。
public interface PricingPolicy {
Money calculatePrice(ProductId productId, int quantity);
}
public class Order {
public void addLine(ProductId productId, int quantity, PricingPolicy pricing) {
Money price = pricing.calculatePrice(productId, quantity);
OrderLine line = new OrderLine(productId, quantity, price);
this.lines.add(line);
recalculateTotal();
}
}四、领域服务与应用服务
4.1 领域服务(Domain Service)
领域服务封装不属于任何单一实体或值对象的领域逻辑。
识别领域服务的信号: - 操作涉及多个聚合的领域逻辑; - 将逻辑放入任何一个实体都不自然; - 操作本身是领域概念的一部分。
// 领域服务:转账
public class TransferService {
public void transfer(Account from, Account to, Money amount) {
if (!from.hasSufficientBalance(amount)) {
throw new InsufficientBalanceException(from.id());
}
from.debit(amount);
to.credit(amount);
}
}4.2 应用服务(Application Service)
应用服务是领域模型的客户端,负责编排工作流程,但不包含业务逻辑。
// 应用服务:编排转账流程
public class TransferAppService {
private final AccountRepository accountRepo;
private final TransferService transferService;
private final EventPublisher eventPublisher;
@Transactional
public void executeTransfer(TransferCommand cmd) {
// 1. 加载聚合
Account from = accountRepo.findById(cmd.fromAccountId());
Account to = accountRepo.findById(cmd.toAccountId());
Money amount = new Money(cmd.amount(), cmd.currency());
// 2. 执行领域逻辑(委托给领域服务)
transferService.transfer(from, to, amount);
// 3. 持久化
accountRepo.save(from);
accountRepo.save(to);
// 4. 发布事件
eventPublisher.publish(new TransferCompletedEvent(
from.id(), to.id(), amount
));
}
}4.3 职责对比
| 职责 | 领域服务 | 应用服务 |
|---|---|---|
| 所在层 | 领域层 | 应用层 |
| 包含业务逻辑 | 是 | 否 |
| 依赖基础设施 | 否(通过接口) | 是(仓储、消息等) |
| 事务管理 | 否 | 是 |
| 典型操作 | 跨聚合的业务规则 | 加载聚合、调用领域逻辑、保存、发事件 |
| 是否有状态 | 无状态 | 无状态 |
| 命名风格 | 业务动词(Transfer) | 用例动词(ExecuteTransfer) |
五、仓储模式
5.1 仓储(Repository)的定义
仓储是聚合的集合接口,提供类似集合的 API 来存取聚合。仓储属于领域层的接口定义,但实现在基础设施层。
// 领域层:仓储接口
public interface OrderRepository {
Order findById(OrderId id);
void save(Order order);
void delete(Order order);
List<Order> findByCustomerId(CustomerId customerId);
}
// 基础设施层:仓储实现
public class JpaOrderRepository implements OrderRepository {
private final EntityManager em;
@Override
public Order findById(OrderId id) {
OrderEntity entity = em.find(OrderEntity.class, id.value());
if (entity == null) {
throw new OrderNotFoundException(id);
}
return OrderMapper.toDomain(entity);
}
@Override
public void save(Order order) {
OrderEntity entity = OrderMapper.toEntity(order);
em.merge(entity);
}
}5.2 Go 语言中的仓储
// 领域层:仓储接口
type OrderRepository interface {
FindByID(ctx context.Context, id OrderID) (*Order, error)
Save(ctx context.Context, order *Order) error
NextID() OrderID
}
// 基础设施层:PostgreSQL 实现
type pgOrderRepository struct {
db *sql.DB
}
func (r *pgOrderRepository) FindByID(ctx context.Context, id OrderID) (*Order, error) {
row := r.db.QueryRowContext(ctx,
`SELECT id, customer_id, total_amount, total_currency, status, version
FROM orders WHERE id = $1`, id.String())
var o orderRow
if err := row.Scan(&o.id, &o.customerID, &o.totalAmount,
&o.totalCurrency, &o.status, &o.version); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrOrderNotFound
}
return nil, fmt.Errorf("查询订单失败: %w", err)
}
lines, err := r.loadLines(ctx, id)
if err != nil {
return nil, err
}
return reconstitute(o, lines), nil
}
func (r *pgOrderRepository) Save(ctx context.Context, order *Order) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// 乐观锁:版本号检查
result, err := tx.ExecContext(ctx,
`UPDATE orders SET customer_id=$1, total_amount=$2,
total_currency=$3, status=$4, version=version+1
WHERE id=$5 AND version=$6`,
order.customerID.String(), order.totalAmount.Amount(),
order.totalAmount.Currency(), order.status,
order.id.String(), order.version)
if err != nil {
return err
}
rows, _ := result.RowsAffected()
if rows == 0 {
return ErrConcurrencyConflict
}
// 保存行项(先删后插,简化逻辑)
if err := r.deleteLines(ctx, tx, order.id); err != nil {
return err
}
if err := r.insertLines(ctx, tx, order.id, order.lines); err != nil {
return err
}
return tx.Commit()
}5.3 仓储的设计原则
| 原则 | 说明 |
|---|---|
| 每个聚合一个仓储 | 不为非聚合根的实体创建仓储 |
| 接口在领域层 | 领域层不依赖基础设施 |
| 整体存取 | 加载和保存整个聚合,不部分加载 |
| 隐藏持久化细节 | 调用方不关心底层是 SQL 还是 NoSQL |
| 类集合接口 | save、findById、delete
等 |
六、领域事件
6.1 领域事件的定义
领域事件(Domain Event)表示领域中发生的有意义的事情。它是过去时态的——描述已经发生的事实。
public class OrderConfirmedEvent implements DomainEvent {
private final OrderId orderId;
private final CustomerId customerId;
private final Money totalAmount;
private final Instant occurredAt;
public OrderConfirmedEvent(OrderId orderId, CustomerId customerId,
Money totalAmount) {
this.orderId = orderId;
this.customerId = customerId;
this.totalAmount = totalAmount;
this.occurredAt = Instant.now();
}
// getter 方法省略
}6.2 从聚合发布事件
聚合在执行业务操作时收集事件,由应用服务在事务提交后发布。
public abstract class AggregateRoot<ID> {
private final List<DomainEvent> domainEvents = new ArrayList<>();
protected void registerEvent(DomainEvent event) {
domainEvents.add(event);
}
public List<DomainEvent> domainEvents() {
return Collections.unmodifiableList(domainEvents);
}
public void clearEvents() {
domainEvents.clear();
}
}
public class Order extends AggregateRoot<OrderId> {
public void confirm() {
// 业务逻辑
this.status = OrderStatus.CONFIRMED;
// 注册领域事件
registerEvent(new OrderConfirmedEvent(this.id, this.customerId, this.totalAmount));
}
}6.3 领域事件 vs 集成事件
| 维度 | 领域事件 | 集成事件 |
|---|---|---|
| 范围 | 限界上下文内部 | 跨限界上下文 |
| 载体 | 内存中的对象 | 消息队列中的消息 |
| 格式 | 领域对象 | 序列化格式(JSON、Protobuf) |
| 消费者 | 同一上下文内的处理器 | 其他上下文或外部系统 |
| 一致性 | 通常同步 | 最终一致 |
详细讨论请参见 领域事件与事件风暴。
七、工程案例:订单聚合的重构
7.1 原始设计(大聚合)
某电商系统的订单聚合最初包含以下内容:
Order (聚合根)
├── OrderLine (1..50)
├── Payment (1..3)
├── Shipment (0..1)
│ └── TrackingEvent (0..100)
├── Coupon (0..5)
├── Invoice (0..1)
└── Review (0..1)
问题: - 加载一个订单需要联查 7 张表; - 物流跟踪事件不断增长,导致聚合越来越大; - 修改物流状态和修改订单状态会产生锁冲突; - 评价和发票与订单核心逻辑无关,但绑在一起。
7.2 重构后(小聚合)
graph TB
subgraph "重构后的聚合边界"
O["Order 聚合<br/>OrderLine"]
P["Payment 聚合"]
S["Shipment 聚合<br/>TrackingEvent"]
I["Invoice 聚合"]
R["Review 聚合"]
end
O -->|"OrderID"| P
O -->|"OrderID"| S
O -->|"OrderID"| I
O -->|"OrderID"| R
O -.->|"OrderConfirmedEvent"| P
O -.->|"OrderConfirmedEvent"| S
P -.->|"PaymentCompletedEvent"| O
S -.->|"ShipmentDeliveredEvent"| O
每个聚合只包含维护自身不变条件所需的最小数据集:
| 聚合 | 包含内容 | 不变条件 |
|---|---|---|
| Order | 订单基本信息、OrderLine | 总金额 = 行项金额之和 |
| Payment | 支付记录 | 支付金额 = 订单金额 |
| Shipment | 配送信息、物流跟踪 | 状态流转合法 |
| Invoice | 发票信息 | 发票金额 = 已支付金额 |
| Review | 评价内容 | 只能评价已签收的订单 |
7.3 重构效果
| 指标 | 重构前 | 重构后 |
|---|---|---|
| 订单加载时间 | 120ms | 15ms |
| 乐观锁冲突率 | 5% | 0.1% |
| 物流更新 P99 延迟 | 800ms | 50ms |
| 单次事务涉及的表数 | 7 | 1-2 |
八、战术模式的综合权衡
| 决策维度 | 选项 A | 选项 B | 权衡要点 |
|---|---|---|---|
| 聚合大小 | 大聚合 | 小聚合 | 强一致 vs 性能和并发 |
| 跨聚合引用 | 对象引用 | ID 引用 | 便利 vs 边界清晰 |
| 跨聚合一致性 | 事务一致 | 最终一致 | 简单性 vs 可扩展性 |
| 领域逻辑位置 | 全在实体中 | 实体 + 领域服务 | 内聚 vs 单一职责 |
| 值对象使用程度 | 少量(只用原始类型) | 大量(细粒度建模) | 开发速度 vs 类型安全 |
| 持久化策略 | ORM 映射 | 手动映射 | 开发效率 vs 控制力 |
| 事件发布时机 | 聚合内同步 | 事务提交后异步 | 实时性 vs 可靠性 |
关键原则
- 默认选小聚合:除非有明确的不变条件要求,否则从最小聚合开始;
- 默认用 ID 引用:对象引用只在有充分理由时使用;
- 默认用最终一致:强一致只在业务绝对要求时使用;
- 多用值对象:值对象是免费的类型安全保障。
九、从战术到实践
战术模式提供了组织领域模型的基本构件,但光有模式还不够——需要一种系统化的方法来发现这些构件。事件风暴(Event Storming)正是这样一种从业务到代码的桥梁方法。
参考资料
- Evans, Eric. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003.
- Vernon, Vaughn. Implementing Domain-Driven Design. Addison-Wesley, 2013.
- Vernon, Vaughn. “Effective Aggregate Design.” dddcommunity.org, 2011.
- Fowler, Martin. “DDD Aggregate.” martinfowler.com.
- Millett, Scott; Tune, Nick. Patterns, Principles, and Practices of Domain-Driven Design. Wrox, 2015.
- 张逸.《解构领域驱动设计》. 人民邮电出版社, 2021.
- Young, Greg. “CQRS Documents.” cqrs.files.wordpress.com, 2010.
- Ghosh, Debasish. Functional and Reactive Domain Modeling. Manning, 2016.
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【系统架构设计百科】复杂性管理:架构的核心战场
系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略
【系统架构设计百科】DDD 战略设计:限界上下文与上下文映射
一个中型电商系统里,"订单"在交易团队意味着"待支付的购物车快照",在物流团队意味着"等待拣货的配送单",在财务团队意味着"一条应收账款记录"。三个团队共用同一张 torder 表、同一个 OrderService 类,每次迭代都互相踩脚。这种混乱的根源不是代码质量,而是缺少一项最基本的架构决策——限界上下文(Boun…
【系统架构设计百科】防腐层与开放主机服务:系统集成的 DDD 方案
某金融科技公司正在构建新一代交易系统。新系统使用领域驱动设计,模型清晰、代码整洁。然而它必须对接一套运行了 15 年的核心银行系统(Core Banking System)——这套系统的接口返回 COBOL 风格的定长字段,状态码用两位数字表示("01"正常、"02"冻结、"99"未知),金额用"分"而非"元"为单位。…
【系统架构设计百科】DDD 与微服务:用领域模型划分服务边界
某电商团队按数据库表拆分微服务——用户服务管 tuser,商品服务管 tproduct,订单服务管 torder。看起来边界清晰,实际运行中却发现:下单需要同步调用商品服务查价格、调用库存服务检查库存、调用优惠服务算折扣、调用用户服务查地址,一个下单请求扇出 4 次 RPC,任意一个服务超时整条链路就失败。这种"一实体…