Skip to main content

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:

class Driver {
private String name;
private Car car; // Driver "has-a" Car reference
}

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.

DriverCar
A Driver knows about a Car; the Car has no field pointing back. One arrowhead, one direction.

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:

HusbandWife
A Husband references a Wife and a Wife references a Husband - navigable both ways.

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:

class Husband {
private Wife wife;
 
void marry(Wife w) {
this.wife = w;
w.setHusbandInternal(this); // easy to forget this half
}
}
 
class Wife {
private Husband husband;
 
void setHusbandInternal(Husband h) {
this.husband = h;
}
}

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):

class Person {
private Passport passport;
}
 
class Passport {
private String number;
private Person owner;
}

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":

class Customer {
private List<Order> orders;
}
 
class Order {
private Customer customer;
private LocalDate placedOn;
}

Many-to-many​

Many Students take many Courses, and many Courses have many Students (* - *):

StudentCourse
Many students take many courses (* to *) - a plain collection field on both sides.

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:

StudentEnrollment-grade: string-enrolledOn: dateCourse
Enrollment turns one * to * association into two 1 to * associations, with a home for grade and enrolledOn.
class Enrollment {
private Student student;
private Course course;
private String grade;
private LocalDate enrolledOn;
}

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.car is an association because it's a field that outlives any single method call. A Driver that receives a Mechanic only as a parameter to getServiced(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 - Driver doesn't own the Car'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 composition territory - or, if the whole merely holds the part without creating or destroying it, aggregation. 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

Question 1 of 3

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?