HasOne vs HasMany vs BelongTo in DDDplus: Association Types Explained
DDDplus models domain relationships as first-class association interfaces where HasOne represents single ownership, HasMany represents collection aggregation, and BelongTo represents external references without lifecycle control.
The funkygao/cp-ddd-framework treats object associations as explicit contracts rather than implicit fields, allowing compile-time reasoning about ownership and cardinality. These three association types—defined in the dddplus-spec module—enable developers to express whether a domain object contains, aggregates, or merely references another entity while keeping infrastructure concerns out of the domain layer.
HasOne: Single Ownership Relationships
The HasOne association expresses a strict one-to-one containment where the host object owns exactly one instance of the associated entity. According to the source code in [dddplus-spec/src/main/java/io/github/dddplus/model/association/HasOne.java](https://github.com/funkygao/cp-ddd-framework/blob/master/dddplus-spec/src/main/java/io/github/dddplus/model/association/HasOne.java#L13-L19), this interface signifies that the associated object shares the same aggregate lifecycle as its host.
When a Task has one Operator, the Task acts as the aggregate root responsible for the Operator's consistency. The domain model declares this through an interface extending HasOne<T>:
@KeyRelation(whom = Operator.class, type = KeyRelation.Type.HasOne)
public interface Task extends IAggregateRoot {
interface OperatorRef extends HasOne<Operator> { }
@Delegate private OperatorRef operator;
}
The concrete implementation resides in the infrastructure layer, typically delegating to a DAO:
public class TaskOperator implements Task.OperatorRef {
private final OperatorDao dao;
private final String taskId;
@Override
public Operator get() {
return dao.findOperatorByTaskId(taskId);
}
}
HasMany: Collection Aggregation
The HasMany association handles one-to-many relationships where the host aggregates zero or more instances of another entity. As defined in [dddplus-spec/src/main/java/io/github/dddplus/model/association/HasMany.java](https://github.com/funkygao/cp-ddd-framework/blob/master/dddplus-spec/src/main/java/io/github/dddplus/model/association/HasMany.java#L68-L71), this interface extends IBag to provide collection-like semantics while maintaining domain encapsulation.
This pattern applies when a Task manages multiple Orders. The domain interface can expose domain-specific query methods beyond basic collection operations:
@KeyRelation(whom = Order.class, type = KeyRelation.Type.HasMany)
public interface Task extends IAggregateRoot {
interface Orders extends HasMany<Order> {
List<Order> pendingOrders();
}
@Delegate private Orders orders;
}
Infrastructure implementations handle the persistence details:
public class TaskOrders implements Task.Orders {
private final OrderDao dao;
private final String taskId;
@Override
public List<Order> pendingOrders() {
return dao.findPendingByTaskId(taskId);
}
}
BelongTo: External References Without Ownership
The BelongTo association represents the inverse relationship—when an object references an external entity it does not own. As implemented in [dddplus-spec/src/main/java/io/github/dddplus/model/association/BelongTo.java](https://github.com/funkygao/cp-ddd-framework/blob/master/dddplus-spec/src/main/java/io/github/dddplus/model/association/BelongTo.java#L33-L39), this indicates that the host merely holds a pointer to an object managed by another aggregate.
A Carton that belongs to a CheckTask uses this pattern to reference its parent without controlling the CheckTask's lifecycle:
@KeyRelation(whom = CheckTask.class, type = KeyRelation.Type.BelongTo)
public class Carton {
private BelongToCheckTask ownerTask;
public interface BelongToCheckTask extends BelongTo<CheckTask> { }
}
Accessing the reference follows a simple getter pattern:
Carton carton = repo.get(cartonNo);
CheckTask task = carton.ownerTask().get();
KeyRelation Annotations and Visualization
All three association types integrate with the KeyRelation annotation system, which enables static analysis and diagram generation. The annotation's Type enum distinguishes between ownership models:
- HasOne: Renders as
*--*in PlantUML (composition) - HasMany: Renders as
*--Nin PlantUML (aggregation) - BelongTo: Renders as
--|>in PlantUML (dependency/association)
The PlantUmlRenderer processes these annotations to generate architectural diagrams that reflect the actual domain model structure.
Implementation Patterns and Lifecycle Boundaries
Choosing between these associations depends on aggregate boundaries and lifecycle ownership:
Use HasOne when:
- The associated object cannot exist independently of the host
- You need strict one-to-one cardinality with shared transaction boundaries
Use HasMany when:
- The host manages a collection of related entities
- Domain logic requires filtering or querying subsets of the collection (e.g.,
pendingOrders())
Use BelongTo when:
- The referenced object belongs to a different aggregate root
- The host needs read-only access without persistence responsibility
- Avoiding circular dependencies between aggregates
Summary
- HasOne (
dddplus-spec/.../HasOne.java): One-to-one ownership where the host contains exactly one instance of the associated entity and manages its lifecycle. - HasMany (
dddplus-spec/.../HasMany.java): One-to-many aggregation exposing collection semantics through domain-specific interfaces that extendIBag. - BelongTo (
dddplus-spec/.../BelongTo.java): Reference association indicating the host belongs to an external entity without ownership or lifecycle control. - KeyRelation annotation: Marks associations for compile-time analysis and PlantUML visualization (
*--*,*--N,--|>respectively). - Separation of concerns: Domain interfaces declare relationships via
@Delegatefields while infrastructure layers provide concrete implementations (DAO, cache, remote services).
Frequently Asked Questions
What is the difference between HasOne and BelongTo in DDDplus?
HasOne indicates the host owns the associated entity and controls its lifecycle (composition), while BelongTo indicates the host merely references an external entity it does not own (association). In the cp-ddd-framework, HasOne is used when a Task contains an Operator, whereas BelongTo is used when a Carton references its parent CheckTask without managing it.
Can HasMany collections contain domain-specific query methods?
Yes. Unlike generic collections, HasMany implementations in DDDplus often expose domain-specific methods such as pendingOrders() or activeUsers() that encapsulate business rules. The interface extends HasMany<T> (which itself extends IBag) while adding custom methods that delegate to appropriate infrastructure implementations.
How does DDDplus visualize these associations in UML diagrams?
The framework uses the @KeyRelation annotation with a Type enum to drive the PlantUmlRenderer. HasOne renders as a filled diamond composition (*--*), HasMany as a collection aggregation (*--N), and BelongTo as a simple association or dependency (--|>), accurately reflecting ownership semantics in generated documentation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →