Modeling a Small Domain in UML 2.x: Choosing Composition, Aggregation, or a Plain Association
A task guide to modeling a small domain with UML 2.x class diagrams, centered on the composition vs. aggregation vs. association decision, with multiplicities, navigability, checks, and recovery options.
25 Feb 2026, 03:44 UTC

When you sketch a class diagram for a small domain — say, an order-processing module — the decision that causes the most debate is rarely what the classes are. It is what the lines between them mean. UML 2.x gives you three ways to connect a whole to its parts: composition, aggregation, and plain association. Two of them carry real engineering weight; one is mostly decoration. This guide walks through modeling a small domain with that decision front and center, including the multiplicities and navigability that make the diagram actually useful for code generation and review.
Desired outcome
A UML 2.x class diagram of a small domain (5–10 classes) in which every association end has an explicit multiplicity, ownership relationships use composition only where lifetime coupling is real, generalization is used only where substitutability holds, and the diagram can be checked against skeleton code without ambiguity.
Prerequisites
- A UML 2.x-compliant tool (PlantUML, Visual Paradigm, Enterprise Architect, or a drawing tool with UML shapes — the semantics matter more than the vendor).
- A short requirements text or user stories for the domain, so you can extract candidate classes.
- Agreement with your team on one convention: whether the hollow diamond (aggregation) is allowed at all. More on this below.
Step 1: Extract candidate classes and connect them with plain associations
Underline the nouns in your requirements. For an order module you might get Customer, Order, OrderLine, Product, Address, Payment. Draw them as boxes and connect related ones with plain lines first. Do not add diamonds yet — decide the relationship exists before deciding its strength.
Step 2: Add multiplicities before anything else
Multiplicity is the constraint that reviewers, code generators, and database mappings actually act on. Write it at both ends of every association:
Customer1 — 0..*Order(a customer may have many orders; an order belongs to exactly one customer)Order1 — 1..*OrderLine(an order has at least one line)OrderLine* — 1ProductOrder1 — 0..1Payment
If you cannot state the multiplicity at an end, you do not understand the relationship yet. Resolve that before moving on.
Step 3: Apply the ownership test for composition
Composition (filled diamond on the whole's end) means two things together: a part belongs to at most one whole at a time, and the part's lifetime is bound to the whole's — deleting the whole deletes the part. Ask for each candidate pair: if the whole is destroyed, should the part cease to exist?
Order◆—OrderLine: yes. An order line is meaningless outside its order. Composition is correct.Order—Customer: no. Deleting an order must not delete the customer. Plain association.Order—Address: depends on your design. If the address is a snapshot copied onto the order (shipping address at time of purchase), composition fits. If it is a shared reference to a customer-managed address book entry, it is a plain association.
That last case is the kind of decision the diagram exists to record. Two reasonable designs differ only in the diamond, and the code differs accordingly (cascade delete versus shared reference).
Step 4: Skip aggregation, or define it locally
Aggregation (hollow diamond) is the weakly defined middle option. The UML specification gives it little enforceable semantics beyond "shared" grouping, and no mainstream code generator treats it differently from a plain association. The practical consequence: a hollow diamond communicates intent to humans but nothing checkable to tools.
The safer engineering choice is a plain association with an explicit multiplicity and, if useful, a role name like contains or members. If your team wants to keep the hollow diamond as a readability hint, write down a one-line local convention ("hollow diamond = shared reference, no cascade delete") and apply it consistently. What you should not do is mix aggregation and composition on the assumption that tools will enforce the difference.
Step 5: State navigability explicitly
A navigability arrow on an association end means that side can be reached from the other — in code, a reference or a foreign key in that direction. Early analysis sketches can omit arrows, but a model that will be read by implementers should not. For the order module: Order → OrderLine (order knows its lines), OrderLine → Product (line knows its product), but no arrow from Customer to Order if you plan to query orders via a repository rather than hold a collection on the customer object. Unidirectional navigability where possible keeps coupling down and makes the mapping to code unambiguous.
Step 6: Add attributes, then generalization, then operations
Add attributes with types (totalAmount: Money, placedAt: Instant). Introduce generalization (the hollow-triangle inheritance arrow) only when a true is-a relationship with substitutability exists: anywhere a Payment is used, a CardPayment must work without surprising the caller. If you are inheriting to reuse fields rather than to be substitutable, prefer composition. Add operations last — they are the least stable part of an early model.
Expected checks
- Every association end shows a multiplicity; none are left blank.
- Every filled diamond passes the destruction test: deleting the whole should delete the part.
- Every generalization passes a substitution test: mentally substitute the subclass wherever the superclass appears and confirm the behavior still holds.
- No class is orphaned — every class participates in at least one relationship or is deliberately standalone.
- Navigability arrows match the intended reference direction in code.
Verifying against code
Hand-write or generate skeleton code from the diagram and confirm the composition parts are created and destroyed with their owner. For example, in Java the Order/OrderLine composition should look like the lines being created inside the order and never exposed as a mutable shared collection:
public class Order {
private final List<OrderLine> lines = new ArrayList<>();
public void addLine(Product product, int quantity) {
lines.add(new OrderLine(product, quantity)); // part created by the whole
}
public List<OrderLine> getLines() {
return Collections.unmodifiableList(lines); // no external mutation
}
}If the natural code keeps a shared reference instead — an OrderLine passed in from outside and reused — that is evidence the diamond was wrong, not that the code is wrong.
Recovery options
If the model drifts from the code over time, reverse-engineer the current code into a scratch diagram and diff it against the design diagram. Expect clutter: reverse-engineering tools typically expose every field as an association, so prune aggressively and only compare the relationships you care about. If you find composition was overused — parts that actually outlive or move between wholes — demote those links to plain associations with explicit multiplicities rather than to aggregation. That keeps the model honest and tool-actionable. Finally, be aware that diagram interchange between UML tools (via XMI) is unreliable across vendors; keep the model in a text-based format like PlantUML if migration or diffing matters to you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.