基于DDD设计库存追踪模型聚合根及明细类归属疑问
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
StockMovemententities (not identical to your arrival/removal lines) that track each individual stock change. EachStockMovementincludes:- Movement type (IN/OUT)
- Quantity changed
- Timestamp
- Reference ID (e.g.,
stock_arrival_idorstock_removal_idfor 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_idfor compliance/audit. - All
StockArrivalLineitems 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
StockArrivalLineentities (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
StockRemovalLinecannot 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
StockRemovalLineentities (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_idin 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:
StockArrivalLineandStockMovementserve 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
StockArrivalLinedirectly—you must use methods on theStockArrivalroot. - For persistence: Each aggregate can be stored in its own table (or set of tables). Stock’s
StockMovementrecords 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

