domain-entity-reference-direction
Domain Entity Reference Direction
Goal
When two domain entities are related, determine whether each entity needs a reference to the other based on its own domain responsibilities — invariants and behavior — not on read or query convenience.
A domain entity should hold a reference to another entity only when that reference is required for the entity to enforce its own invariants or perform its own domain behavior. If the relationship is only needed to answer queries or display data, it does not belong on the entity — it belongs in the repository as a query operation.
Most relationships are unidirectional. Bidirectional references are rare and require both entities to independently need the reference for their own domain logic.
What Counts as In Scope
Apply this skill to code that does one or more of these things:
- defines a domain entity that holds or could hold a reference to another domain entity
- introduces a new relationship between two domain entities
- adds a reference to an entity for the purpose of querying or displaying related data
- reviews whether an existing reference on a domain entity is justified by domain behavior
The Rule
-
A reference must be justified by the entity's own invariants or behavior.
- The entity needs the reference to enforce a business rule about itself.
- The entity needs the reference to perform a domain operation that belongs to it.
- If the entity does not use the reference in any invariant check or domain behavior, the reference does not belong on the entity.
-
Do not add references for read or query convenience.
- If the only reason to add a reference is to retrieve related data for display, listing, or reporting, do not add it to the entity.
- Resolve read-direction queries through repository operations instead — for example,
orderRepository.findByCustomerId(customerId).
-
Evaluate each direction independently.
- For entity A and entity B, ask separately: does A need to know about B for its own invariants or behavior? Does B need to know about A for its own invariants or behavior?
- Only add the reference in the direction where the answer is yes.
-
Bidirectional references require justification from both sides.
- A bidirectional relationship is only correct when both entities independently need the reference for their own domain logic.
- Example:
TeamholdsmemberIdsto enforce a maximum team size.PlayerholdsteamIdto enforce that a player cannot belong to two teams simultaneously. Both references are justified by independent invariants.
-
When the need is ambiguous, ask the human.
- If it is unclear whether an entity needs a reference for domain behavior or merely for query convenience, ask before adding the reference.
- State the two entities, the proposed direction, and why the need is unclear.
Detection Workflow
-
Identify the relationship and its current direction.
- Find the two domain entities involved.
- Determine which entity currently holds a reference to the other, or which entity a new reference is being proposed for.
-
For each direction, check for domain justification.
- Does the entity use this reference in any invariant check or business rule enforcement?
- Does the entity use this reference in any domain behavior method?
- If neither, the reference is not justified on this entity.
-
Check for query-only references.
- Is the reference used only to retrieve or display related data?
- If so, the reference should be removed from the entity and replaced with a repository query.
-
If ambiguous, ask the human.
- State which entity holds or would hold the reference.
- State what domain behavior or invariant would use it.
- Ask whether the reference is needed for domain logic or only for read convenience.
Writing or Changing Domain Entity References
-
Before adding a reference, state the domain justification.
- Identify which invariant or domain behavior on the entity requires the reference.
- If no invariant or behavior requires it, do not add the reference.
-
For unidirectional relationships (the common case):
- Add the reference only on the entity that needs it for its own domain logic.
- Resolve the inverse direction through a repository query when needed by business-logic entry points.
-
For bidirectional relationships (rare):
- Verify that both entities independently need the reference for their own invariants or behavior.
- Document why bidirectionality is required — both justifications must be explicit.
-
When removing an unjustified reference:
- Verify that no domain behavior or invariant on the entity depends on the reference.
- Move the query responsibility to the repository if callers need to retrieve the related data.
Examples
Unidirectional — Order references Customer, not the reverse:
class Order {
readonly id: OrderId
readonly customerId: CustomerId // Order needs to know its owner
readonly items: ReadonlyArray<OrderItem>
}
class Customer {
readonly id: CustomerId
readonly name: string
readonly email: Email
// No orderIds here — Customer does not need orders for its own invariants
}
// When you need a customer's orders, use the repository:
// orderRepository.findByCustomerId(customerId)
@dataclass(frozen=True)
class Order:
id: OrderId
customer_id: CustomerId # Order needs to know its owner
items: tuple[OrderItem, ...]
@dataclass(frozen=True)
class Customer:
id: CustomerId
name: str
email: Email
# No order_ids here — Customer does not need orders for its own invariants
# When you need a customer's orders, use the repository:
# order_repository.find_by_customer_id(customer_id)
data class Order(
val id: OrderId,
val customerId: CustomerId, // Order needs to know its owner
val items: List<OrderItem>,
)
data class Customer(
val id: CustomerId,
val name: String,
val email: Email,
// No orderIds here — Customer does not need orders for its own invariants
)
// When you need a customer's orders, use the repository:
// orderRepository.findByCustomerId(customerId)
Bidirectional — both sides have independent domain justification:
class Team {
readonly id: TeamId
readonly memberIds: ReadonlyArray<PlayerId> // needed to enforce max team size
addMember(playerId: PlayerId): Team {
if (this.memberIds.length >= MAX_TEAM_SIZE) {
throw new TeamFullError()
}
return new Team({ ...this, memberIds: [...this.memberIds, playerId] })
}
}
class Player {
readonly id: PlayerId
readonly teamId: TeamId | null // needed to enforce single-team constraint
joinTeam(teamId: TeamId): Player {
if (this.teamId !== null) {
throw new AlreadyInTeamError()
}
return new Player({ ...this, teamId })
}
}
Review Questions
When reading or reviewing code, ask:
- Does this entity use the reference in any invariant check or domain behavior?
- If the reference were removed, would the entity lose the ability to enforce a business rule about itself?
- Is this reference only used to retrieve or display related data?
- If bidirectional, do both entities independently need the reference for their own domain logic?
If a reference exists only for read or query convenience, it should be moved to a repository query.
Report the Outcome
When finishing the task:
- state which domain entities were involved and what relationship was evaluated
- state the direction of the reference and which entity holds it
- state the domain justification — which invariant or behavior requires the reference
- state whether the relationship is unidirectional or bidirectional, and why
- state whether any query-only references were removed or avoided
More from code-sherpas/agent-skills
neverthrow-return-types
Require `neverthrow`-based return types in TypeScript and JavaScript code whenever the surrounding technology allows it. Use when creating, refactoring, reviewing, or extending standalone functions, exported module functions, class methods, object methods, service methods, repository methods, and similar APIs that should expose explicit success and failure result types in their signatures. Prefer `Result<T, E>` for synchronous code and `ResultAsync<T, E>` for asynchronous code. Only skip a `neverthrow` return type when a framework, library, runtime interface, or externally imposed contract is incompatible and requires a different return shape.
16neverthrow-wrap-exceptions
Capture exceptions and promise failures with `neverthrow` instead of hand-written `try/catch` in TypeScript and JavaScript code. Use when wrapping synchronous functions that may throw, promise-returning functions that may throw before returning, existing `PromiseLike` values that may reject, or third-party APIs such as parsers, database clients, HTTP clients, file-system helpers, serializers, and SDK calls. Prefer `Result.fromThrowable` for synchronous throwers, `ResultAsync.fromThrowable` for promise-returning functions that may throw or reject, and `ResultAsync.fromPromise` when you already have a `PromiseLike` value in hand. Only keep `try/catch` when the language construct, cleanup requirement, or framework boundary truly requires it.
11atomic-design
Create or update web UI components with a strict reuse-first workflow. Use when building, refactoring, restyling, or extending frontend or template components while minimizing raw DOM or HTML by reusing or generalizing existing components first.
10write-persistence-representations
Create or update persistence-layer data representations in any stack, including ORM entities, schema definitions, table mappings, document models, collection definitions, and similar database-facing code. Use when agents needs to add or change persisted fields, identifiers, relationships, indexes, timestamps, auditing fields, or storage mappings in frameworks, libraries, or ORMs such as Prisma, TypeORM, Sequelize, Drizzle, Mongoose, Hibernate/JPA, Doctrine, Ecto, Active Record, or equivalent persistence technologies.
7business-logic
Identify, interpret, review, or write business logic in code. Use when an agent needs to decide whether code expresses business rules, business algorithms, or business workflows, or when it must implement, preserve, or refactor code that creates, stores, or transforms data according to real business policies.
7immutable-domain-entities
Require the immutable design pattern for domain entities. Use when an agent needs to create, modify, review, or interpret domain entities and should preserve identity while expressing state changes through new immutable instances. Domain entities must be modeled as immutable classes, not as plain type aliases or interfaces paired with standalone functions.
7