You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于DDD设计库存追踪模型聚合根及明细类归属疑问

How to Model Inventory Tracking Aggregates with DDD (Resolving the Many-to-Many Confusion)

Great question—this is a super common hurdle when shifting from anemic, data-first models to DDD, especially for inventory systems where event consistency and business rules are critical. Let’s break this down using DDD’s core principles, focusing on invariants and aggregate responsibilities.

First, Reframe the Problem: Stop Thinking in Tables, Start Thinking in Business Responsibilities

Your current anemic model is structured around database relationships, but DDD is about grouping behavior and data around business rules that must be enforced consistently (invariants). The confusion with many-to-many comes from trying to map database joins directly to aggregates—instead, we need to split these into aggregates that each own their own invariants.

Define the Aggregates and Their Invariants

Let’s split your model into three distinct aggregates, each with clear, bounded responsibilities:

1. Stock Aggregate Root

Core Responsibility: Maintain the correctness of inventory levels and serve as the source of truth for current stock quantity.
Key Invariants:

  • Total stock quantity (total_amount) can never be negative.
  • Every change to stock quantity must be traceable to a valid business event (inbound or outbound).

What Goes Inside:

  • The Stock entity itself (id, code, description, total_amount).
  • A collection of internal StockMovement entities (not identical to your arrival/removal lines) that track each individual stock change. Each StockMovement includes:
    • Movement type (IN/OUT)
    • Quantity changed
    • Timestamp
    • Reference ID (e.g., stock_arrival_id or stock_removal_id for audit traceability)
    • Optional: Unit price (if inventory valuation is part of your business rules)

Why No Direct Arrival/Removal Links?
The Stock aggregate doesn’t care about full arrival/removal details (like document compliance status or approver info). It only needs enough data to validate quantity changes and maintain traceability. Directly linking to other aggregates would create tight coupling and violate aggregate boundaries.

2. StockArrival Aggregate Root

Core Responsibility: Manage business rules for inbound inventory events (e.g., validating compliant document_ids, ensuring line items reference existing stock codes, enforcing pricing rules).
Key Invariants:

  • Every StockArrival must have a valid document_id for compliance/audit.
  • All StockArrivalLine items must reference a valid Stock code (checked at creation via a domain service).
  • Total arrival quantity must match the sum of line items.

What Goes Inside:

  • The StockArrival entity (id, date, document_id, status e.g., PENDING/APPROVED).
  • A collection of StockArrivalLine entities (stock_id, quantity, unit_price) that belong exclusively to this arrival.

Critical Behavior:
When a StockArrival is marked as approved (a business action), it publishes a StockArrivedDomainEvent containing key details (stock_id, quantity, arrival_id, unit_price). The Stock aggregate subscribes to this event, updates its total_amount, and adds a StockMovement record.

3. StockRemoval Aggregate Root

Core Responsibility: Manage outbound inventory rules (e.g., ensuring sufficient stock exists for the removal, validating outbound document compliance).
Key Invariants:

  • Requested quantity for each StockRemovalLine cannot exceed current available stock (checked at creation via a domain service).
  • Every StockRemoval must have a valid business reason/document.

What Goes Inside:

  • The StockRemoval entity (id, date, document_id, status).
  • A collection of StockRemovalLine entities (stock_id, quantity).

Critical Behavior:
When a StockRemoval is confirmed, it publishes a StockRemovedDomainEvent. The Stock aggregate subscribes to this event, decrements total_amount (enforcing the non-negative invariant), and adds a StockMovement record.

Resolving "Many-to-Many" and "Duplicate Storage" Concerns

  • No more cross-aggregate many-to-many: Aggregates don’t directly reference each other. Instead, they communicate via domain events for eventual consistency. The stock_id in arrival/removal lines is just an identity value (a reference to the Stock’s ID), not a database foreign key that couples aggregates.
  • Not duplicate storage: StockArrivalLine and StockMovement serve distinct purposes. The arrival line is part of the arrival’s compliance/audit record, while the StockMovement is part of the Stock’s quantity-tracking record. They’re not duplicates—they’re separate records serving different business needs.

Practical Implementation Tips

  • Use a domain service to coordinate cross-aggregate checks (e.g., when creating a StockRemoval, the service queries the Stock aggregate to confirm sufficient stock exists before allowing the removal to be created).
  • Ensure only the aggregate root can modify its internal entities. For example, you can’t update a StockArrivalLine directly—you must use methods on the StockArrival root.
  • For persistence: Each aggregate can be stored in its own table (or set of tables). Stock’s StockMovement records link only to the Stock’s ID, while arrival/removal lines link only to their respective aggregate roots.

Final Takeaway

Don’t let your existing database schema dictate your DDD model. Focus on business responsibilities and invariants first:

  • Stock owns inventory quantity correctness.
  • StockArrival owns inbound event compliance and validation.
  • StockRemoval owns outbound event compliance and validation.
  • Use domain events to sync state between aggregates, avoiding tight coupling and direct many-to-many relationships.

内容的提问来源于stack exchange,提问作者kravemir

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:08:01