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

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?