2.4 KiB
Entities, Value Objects, and Aggregates
Start from identity and business invariants rather than database tables or a target class count.
Identity and value
An entity retains identity through changes; a value object is interchangeable with another value having the same attributes. Choose equality and mutability accordingly. Use existing identifier conventions rather than adding a new ID scheme by default.
Value objects should enforce their actual invariants and support immutable transformations. Do not infer business rules from examples: negative money may be valid for one concept and invalid for another.
Aggregate ownership
An aggregate owns a cohesive set of state and invariants, with a root controlling its changes. Keep it small enough to load and update for its real use cases; use references by ID for independent aggregates where the project's model calls for them.
Ask:
- Which rules must hold after every successful operation?
- Who owns each lifecycle and may change its state?
- Which state must be read and written together?
- What prevents two concurrent commands from both accepting incompatible outcomes?
- Can a partial result or delayed update be valid?
Do not embed a growing independent lifecycle merely to make all relationships reachable through one object.
Atomicity and concurrency
Aggregate boundaries and transaction boundaries are related, not identical. When an operation must commit or roll back across multiple aggregate documents, use the supported service-layer transaction required by the domain.
In this repository, ADR-0011 explicitly requires Mongo transactions for relevant rental operations. Preserve optimistic version checks, occupancy contention, and uniqueness constraints; replacing them with asynchronous events or a query-only overlap check would weaken the invariant.
Use eventual consistency only when delayed or partial progress is acceptable, with explicit failure and recovery handling. Keep remote calls and long-running work outside database transactions.
Verify the boundary
Exercise the actual operation, including invalid transitions, stale versions, duplicate commands, and competing callers where relevant. Prove the invariant at the storage/integration boundary when an in-memory test cannot establish it. Preserve distinct existing test coverage when moving behavior behind a new interface.