Association
Association is the plainest way one class can know about another: an object holds a reference to another object, usually as a field, for as long as both happen to be around. Every other relationship on this site's diagrams - aggregation, composition, even dependency - is a variation on this same idea with an extra rule bolted on, which is why it pays to get comfortable with the base case first.
Definition: "uses-a", not "is-a"
Where inheritance says "a Car is-a Vehicle," association says "a Driver
uses-a Car." Nothing about Driver gets copied into Car or vice versa - the two
classes stay completely independent, and all that connects them is that one holds a
reference to the other, typically as a field:
That's it. No shared lifecycle, no ownership implication - just "I know how to reach you." Aggregation and composition (covered next) both start from this exact shape and add a rule about who controls whose lifetime; association itself makes no such claim.
UML notation
Association is a solid line with an open arrowhead at the end being pointed to. No diamond, no dashes - those are reserved for composition/aggregation and dependency respectively. An arrowhead at one end only means the relationship is navigable in one direction; arrowheads at both ends mean either side can reach the other.
Directionality: unidirectional vs. bidirectional
Unidirectional is the diagram above: Driver holds a Car field, Car has nothing
pointing back. It's the cheaper option and the default you should reach for - one field
to maintain, one direction to reason about.
Bidirectional means both classes hold a reference to each other, and UML draws that as arrowheads at both ends of the same line:
The notation is one line either way; the cost shows up in code, not the diagram. Bidirectional means two references to keep in sync, and nothing enforces that automatically:
Skip the second assignment and you have a Husband who thinks he's married to someone
who, as far as her own field is concerned, is single. That's the whole tradeoff:
bidirectional associations are convenient to query from either side, and every mutation
now has two places to update instead of one. Default to unidirectional; add the second
reference only when you actually need to navigate that direction.
Multiplicity
Multiplicity says how many instances of one class can be associated with one instance
of the other, written at each end of the line: 1 (exactly one), 0..1 (zero or one),
* (zero or more, aka many), 1..* (one or more). Read it at the end closest to the
class it constrains: "Customer 1 -- * Order" reads as "one customer relates to zero
or more orders."
One-to-one
Each Person has exactly one Passport, and each Passport belongs to exactly one
Person (1 - 1):
One-to-many
One Customer places many Orders; each Order belongs to exactly one Customer
(1 - *). Note where the collection lives - on the "one" side, pointing at the "many":
Many-to-many
Many Students take many Courses, and many Courses have many Students (* - *):
A plain List<Course> on Student and List<Student> on Course compiles, but it has
nowhere to put data that belongs to the pairing rather than to either side - a grade,
an enrollment date, an attendance record. That's the signal to resolve the many-to-many
into its own class in the middle:
Student and Course now each hold a List<Enrollment> instead of referencing each
other directly - two one-to-many associations instead of one many-to-many. This is the
same move a relational database forces on you with a join table, for exactly the same
reason: a many-to-many relationship with its own attributes needs its own identity.
Telling association apart from its neighbours
- Association vs. dependency: the test is whether the reference is held as a
field.
Driver.caris an association because it's a field that outlives any single method call. ADriverthat receives aMechaniconly as a parameter togetServiced(Mechanic m)and never stores it is a dependency, not an association - see the Dependency page for the full test. - Association vs. aggregation/composition: plain association makes no claim about
ownership at all -
Driverdoesn't own theCar's lifecycle in either direction, it just knows where to find it. The moment you can say "the whole creates/destroys the part" or "the part is meaningless without the whole," you've moved into aggregation or composition territory - both are association plus a lifecycle rule.
Common mistakes
- Calling every field reference "association" without checking multiplicity.
Getting
1 - *backwards (putting the collection on the wrong class) is a design bug that compiles fine and produces wrong queries later. - Modeling a many-to-many as two plain collections when the pairing itself needs
data. If you catch yourself wanting to store a grade "on the student, for this
course," that data belongs on an
Enrollment, not shoved into a parallel map. - Making an association bidirectional "just in case." Every added direction is a second reference that can silently drift out of sync - add it when a real caller needs to navigate that way, not preemptively.
Try it yourself: model the relationship between a Passenger and a Flight they've
booked. Is it one-to-one, one-to-many, or many-to-many? Does it need an intermediary
class - and if so, what data lives there that doesn't belong on either Passenger or
Flight alone?
Check yourself
A Driver class has a `Car car;` field, but Car has no field pointing back to Driver. How does UML draw this, and what does it mean?