Last Updated: February 12, 2026
In the real world, nothing exists in isolation.
These connections define how different entities interact and collaborate.
When we design software using Object-Oriented Programming (OOP), our goal is to model this real world where objects communicate and work together to achieve meaningful outcomes.
So, how do we represent these connections between our objects?
In this chapter, we will explore the most fundamental and common of these relationships: Association.
Association represents a relationship between two classes where one object uses, communicates with, or references another.
This relationship models the idea:
“One object need to know about the existence of another object to perform its responsibilities”
If Class A must interact with Class B to fulfill its purpose, then Class A is associated with Class B.
Think of a Student and a Teacher.
However:
This is a real-world association:
In UML class diagrams, association is represented by a solid line between two classes.
Multiplicity defines how many instances of one class can be associated with another. It is written near the class ends in UML diagrams.
The solid line is the key. Inheritance uses a solid line with a hollow triangle. Aggregation adds a hollow diamond. Composition adds a filled diamond. Plain association is just the line, optionally with an arrowhead for direction and multiplicity labels at each end.
Associations between classes can vary depending on how objects are connected and in which direction information flows.
In Object-Oriented Design, associations are primarily defined by two key properties:
Directionality determines which class holds a reference to the other and whether communication is one-way or two-way.
In a unidirectional association, only one class is aware of or holds a reference to the other class. The referenced class has no knowledge of who is referencing it.
Example: An Order object uses a PaymentGateway to process transactions, but the PaymentGateway doesn't keep track of any orders. The order knows about the gateway. The gateway doesn't know about the order.
Order holds a reference to PaymentGateway and calls its method. But PaymentGateway has no field or reference pointing back to Order. This is the simplest and most common form of association. When in doubt, start with unidirectional. You can always add the reverse direction later if needed.
In a bidirectional association, both classes are aware of each other. Each class holds a reference to the other, enabling two-way communication.
Example: A Team has a list of Developers, and each Developer knows which Team they belong to. Either side can navigate to the other.
Notice how addDeveloper() updates both sides of the relationship: it adds the developer to the team's list and sets the team reference on the developer. This is important. In a bidirectional association, both references must stay in sync. If you add a developer to the team but forget to set the developer's team reference, you'll get inconsistent state where the team thinks it has the developer, but the developer doesn't know which team it belongs to.
Bidirectional associations are more complex to maintain than unidirectional ones. You need to keep both sides synchronized, which means more code and more opportunities for bugs. Use them only when both sides genuinely need to navigate to the other.
Multiplicity defines how many instances of one class can be associated with instances of another class. It describes the quantity and nature of the connections.
Each object of one class is linked to exactly one object of the other class.
Example: Each User has exactly one Profile, and each Profile belongs to one User. This is a bidirectional one-to-one relationship.
One-to-one associations make sense when you want to separate concerns even though the objects are tightly paired. A User handles authentication (login, password, roles), while a Profile handles display information (avatar, bio, preferences). Merging them into one class would work, but separating them keeps each class focused on a single responsibility.
If you find that two one-to-one associated classes are always created, modified, and deleted together with no independent use case, that's a signal they might belong as a single class instead.
One object of a class is linked to multiple objects of another class. This is one of the most common patterns in software design.
Example: Each Project can have many Issues (bug reports, feature requests), but each Issue belongs to one Project. The project holds a list of issues, and each issue holds a back-reference to its project.
Multiple objects from one class are associated with multiple objects from another class. This is common in scenarios involving memberships, enrollments, or tagging systems.
Example: A User can be a member of multiple Groups (WhatsApp groups, Slack channels), and a Group can have multiple Users. Both sides hold a list of the other. The joinGroup() and addUser() methods keep both sides in sync.
Notice the guard clause in both joinGroup() and addUser(). Without it, calling alice.joinGroup(backend) would add backend to Alice's groups, then backend.addUser(alice) would add Alice to backend's users, then it would call alice.joinGroup(backend) again, and you'd be stuck in an infinite loop. The contains check breaks the recursion.
Many-to-many associations are inherently bidirectional and require careful synchronization. In database design, you'd model this with a join table. In code, both sides hold a list of the other, and you need helper methods that update both sides atomically.
Let's build a system that combines multiple association types in a realistic domain. A hospital manages doctors, patients, rooms, and appointments. The relationships between these entities demonstrate unidirectional, bidirectional, one-to-many, and many-to-many associations working together.
Here's how the classes connect:
Appointment holds a reference to a Room (unidirectional, the room doesn't know about its appointments).Doctor has a list of Appointment objects, and each Appointment points back to its Doctor (bidirectional one-to-many).Patient has a list of Appointment objects, and each Appointment points back to its Patient (bidirectional one-to-many).Doctor and Patient are connected many-to-many through Appointment as an intermediary. A doctor sees many patients, and a patient can visit many doctors, but they don't reference each other directly.Appointment class is the intermediary. Instead of Doctor and Patient holding direct references to each other (which would create a tangled many-to-many), they connect through Appointment. This is a common pattern for modeling many-to-many relationships in code, analogous to a join table in a relational database.Room stays simple. The room doesn't need to know about appointments. It's just a location. This keeps the relationship unidirectional and avoids unnecessary coupling.Appointment is a full object, you can add fields like time, status, notes, or diagnosis without modifying Doctor or Patient. Try doing that with a direct many-to-many reference.