Loading content...
In object-oriented design, classes are the nouns—but relationships are the verbs that give your system meaning. A perfectly designed class in isolation is useless; it's the connections between classes that enable behavior, enforce constraints, and ultimately determine whether your system is robust or fragile.
Consider a banking system: a Customer class means nothing without its relationship to Account. An Account without connection to Transaction cannot track history. The relationships define not just structure, but the very semantics of what operations are possible and how data flows through your system.
By the end of this page, you will have a comprehensive understanding of all major relationship types in OOP—association, aggregation, composition, dependency, inheritance, and realization. You'll know when to use each, how they differ semantically, and how to represent them in code and UML.
Relationships in software design serve multiple critical purposes that directly impact system quality:
1. Semantic Precision Relationships encode meaning. The difference between "a Car has an Engine" (composition) versus "a Car uses a GasStation" (dependency) affects how we reason about the system, what invariants we can guarantee, and how the system evolves.
2. Lifecycle Management Relationships determine object lifecycles. In composition, destroying the whole destroys the parts. In aggregation, parts outlive the container. Getting this wrong leads to memory leaks, dangling references, or inconsistent state.
3. Coupling Control Different relationship types create different levels of coupling. Dependency is weaker than association, which is weaker than inheritance. Choosing the right relationship type directly affects maintainability and testability.
4. API Design
Relationships dictate your public interfaces. Should Order.getCustomer() return a full Customer object or just a customer ID? The relationship type guides this decision.
Misidentifying relationships is one of the most expensive design mistakes. Using inheritance when composition is appropriate leads to fragile base class problems. Using composition when aggregation is correct creates artificial lifecycle constraints. These mistakes compound as systems grow, becoming increasingly expensive to fix.
Association is the most general form of relationship between classes. It represents a structural connection where objects of one class are connected to objects of another class. Association implies that objects know about each other and can interact.
Key Characteristics:
Real-World Examples:
Student is associated with a Course (enrolled in)Doctor is associated with a Patient (treats)Employee is associated with a Project (works on)1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
// Association: Student and Course exist independently// A student can enroll in courses; courses can have students class Course { private courseName: string; private enrolledStudents: Student[] = []; constructor(courseName: string) { this.courseName = courseName; } enrollStudent(student: Student): void { this.enrolledStudents.push(student); // Note: We don't create the student - they exist independently } getEnrolledStudents(): Student[] { return [...this.enrolledStudents]; }} class Student { private studentId: string; private enrolledCourses: Course[] = []; constructor(studentId: string) { this.studentId = studentId; } enrollInCourse(course: Course): void { this.enrolledCourses.push(course); course.enrollStudent(this); } // Student continues to exist even if dropped from all courses dropAllCourses(): void { this.enrolledCourses = []; }} // Usage: Both objects have independent lifecyclesconst student = new Student("S001");const course = new Course("Data Structures");student.enrollInCourse(course); // Deleting the course doesn't delete the student// Deleting the student doesn't delete the courseUML Representation: Association is represented by a solid line connecting two classes. Role names and multiplicities can appear at each end.
Aggregation is a specialized form of association that represents a whole-part relationship where parts can exist independently of the whole. It's often called a "has-a" relationship with shared ownership.
Key Characteristics:
Real-World Examples:
Department has Employees (employees can move between departments)Playlist has Songs (songs exist in multiple playlists)Team has Players (players can be traded)1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
// Aggregation: Playlist aggregates Songs// Songs exist independently and can belong to multiple playlists class Song { constructor( public readonly id: string, public readonly title: string, public readonly artist: string, public readonly durationSeconds: number ) {}} class Playlist { private name: string; private songs: Song[] = []; constructor(name: string) { this.name = name; } // Aggregation: we receive an existing song, we don't create it addSong(song: Song): void { if (!this.songs.find(s => s.id === song.id)) { this.songs.push(song); } } removeSong(songId: string): void { this.songs = this.songs.filter(s => s.id !== songId); // Note: Song still exists after removal from playlist } getTotalDuration(): number { return this.songs.reduce((sum, song) => sum + song.durationSeconds, 0); }} // Usage: Songs have independent lifecyclesconst song1 = new Song("1", "Bohemian Rhapsody", "Queen", 354);const song2 = new Song("2", "Stairway to Heaven", "Led Zeppelin", 482); const rockPlaylist = new Playlist("Classic Rock");const favoritesPlaylist = new Playlist("My Favorites"); // Same song in multiple playlists (shared ownership)rockPlaylist.addSong(song1);favoritesPlaylist.addSong(song1); // Deleting playlist doesn't delete songs// Songs persist across playlist deletionsAggregation is represented by a line with an empty diamond on the whole side. The diamond points to the container/whole, indicating 'this class aggregates that class.'
Composition is a strong form of aggregation where parts cannot exist without the whole. It represents exclusive ownership with coupled lifecycles.
Key Characteristics:
Real-World Examples:
House is composed of Rooms (rooms don't exist outside a house)Order is composed of OrderItems (items belong to one order)Car is composed of an Engine (engine is part of exactly one car)1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
// Composition: Order owns OrderItems exclusively// OrderItems cannot exist without their parent Order class OrderItem { constructor( public readonly productId: string, public readonly productName: string, public readonly quantity: number, public readonly unitPrice: number ) {} getSubtotal(): number { return this.quantity * this.unitPrice; }} class Order { private readonly orderId: string; private readonly items: OrderItem[] = []; private readonly createdAt: Date; constructor(orderId: string) { this.orderId = orderId; this.createdAt = new Date(); } // Composition: We CREATE the OrderItem here // It's not passed in from outside addItem(productId: string, name: string, qty: number, price: number): void { const item = new OrderItem(productId, name, qty, price); this.items.push(item); } getTotal(): number { return this.items.reduce((sum, item) => sum + item.getSubtotal(), 0); } getItems(): ReadonlyArray<OrderItem> { return this.items; } // When order is cancelled/deleted, all items are gone // There's no separate "delete item and it still exists" operation} // Usage demonstrates exclusive ownershipconst order = new Order("ORD-001");order.addItem("PROD-1", "Keyboard", 2, 49.99);order.addItem("PROD-2", "Mouse", 1, 29.99); // Items are PART OF the order// If order is deleted, items are deleted// Items cannot be "moved" to another orderComposition is represented by a line with a filled diamond on the whole side. The filled diamond indicates strong ownership—the whole controls the lifecycle of the parts.
Dependency is the weakest form of relationship. It exists when one class uses another class without maintaining a persistent reference. The relationship is transient—it exists only during method execution.
Key Characteristics:
Real-World Examples:
ReportGenerator depends on DateFormatter (uses it to format dates)OrderService depends on EmailSender (uses it to send confirmations)PaymentProcessor depends on CurrencyConverter (uses it for conversion)1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
// Dependency: ReportGenerator uses DateFormatter// No persistent reference - just uses it when needed interface DateFormatter { format(date: Date): string;} class ISODateFormatter implements DateFormatter { format(date: Date): string { return date.toISOString(); }} class HumanReadableDateFormatter implements DateFormatter { format(date: Date): string { return date.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); }} class ReportGenerator { // No field storing DateFormatter - that would be association // Dependency: DateFormatter is received as parameter generateReport(data: ReportData, formatter: DateFormatter): string { const formattedDate = formatter.format(new Date()); return `Report generated on: ${formattedDate}\n${data.content}`; } // Another form of dependency: using a static method generateQuickReport(data: ReportData): string { // Depends on Date class const timestamp = new Date().getTime(); return `Quick Report [${timestamp}]: ${data.summary}`; }} // Dependency allows flexibility - different formatters can be usedconst generator = new ReportGenerator();const data = { content: "Sales figures...", summary: "Q4 Report" }; const isoReport = generator.generateReport(data, new ISODateFormatter());const readableReport = generator.generateReport(data, new HumanReadableDateFormatter());Dependency is represented by a dashed arrow pointing from the dependent class to the class it depends on. The arrow indicates 'this class depends on that class.'
Inheritance represents a generalization/specialization relationship. A subclass "is-a" type of its superclass, inheriting its structure and behavior while adding or overriding capabilities.
Key Characteristics:
When to Use Inheritance:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
// Inheritance: Different payment methods inherit from baseabstract class PaymentMethod { protected readonly id: string; protected readonly createdAt: Date; constructor(id: string) { this.id = id; this.createdAt = new Date(); } abstract processPayment(amount: number): Promise<PaymentResult>; abstract validate(): boolean; // Shared behavior getPaymentMethodId(): string { return this.id; }} class CreditCard extends PaymentMethod { constructor( id: string, private cardNumber: string, private expiryMonth: number, private expiryYear: number, private cvv: string ) { super(id); } validate(): boolean { const now = new Date(); const expiry = new Date(this.expiryYear, this.expiryMonth - 1); return expiry > now && this.cardNumber.length === 16; } async processPayment(amount: number): Promise<PaymentResult> { // Credit card specific processing return { success: true, transactionId: `CC-${Date.now()}` }; } // Credit card specific method getMaskedNumber(): string { return `****-****-****-${this.cardNumber.slice(-4)}`; }} class BankTransfer extends PaymentMethod { constructor( id: string, private routingNumber: string, private accountNumber: string ) { super(id); } validate(): boolean { return this.routingNumber.length === 9; } async processPayment(amount: number): Promise<PaymentResult> { // Bank transfer specific processing return { success: true, transactionId: `BT-${Date.now()}` }; }}Inheritance creates the tightest coupling. Favor composition over inheritance when possible. Use inheritance only for true is-a relationships where substitutability is guaranteed. Overusing inheritance leads to fragile base class problems and rigid hierarchies.
Realization (or Implementation) represents the relationship between an interface and a class that implements it. The class agrees to fulfill the contract defined by the interface.
Key Characteristics:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
// Realization: Classes implement the Repository interface interface Repository<T, ID> { findById(id: ID): Promise<T | null>; findAll(): Promise<T[]>; save(entity: T): Promise<T>; delete(id: ID): Promise<boolean>;} // PostgreSQL implementationclass PostgresUserRepository implements Repository<User, string> { constructor(private pool: Pool) {} async findById(id: string): Promise<User | null> { const result = await this.pool.query( 'SELECT * FROM users WHERE id = $1', [id] ); return result.rows[0] ?? null; } async findAll(): Promise<User[]> { const result = await this.pool.query('SELECT * FROM users'); return result.rows; } async save(user: User): Promise<User> { // PostgreSQL-specific save logic return user; } async delete(id: string): Promise<boolean> { const result = await this.pool.query( 'DELETE FROM users WHERE id = $1', [id] ); return result.rowCount > 0; }} // In-memory implementation for testingclass InMemoryUserRepository implements Repository<User, string> { private users: Map<string, User> = new Map(); async findById(id: string): Promise<User | null> { return this.users.get(id) ?? null; } async findAll(): Promise<User[]> { return Array.from(this.users.values()); } async save(user: User): Promise<User> { this.users.set(user.id, user); return user; } async delete(id: string): Promise<boolean> { return this.users.delete(id); }}| Relationship | Coupling | Lifecycle | UML Symbol | Example |
|---|---|---|---|---|
| Association | Medium | Independent | Solid line | Student ↔ Course |
| Aggregation | Medium | Independent | Empty diamond | Playlist ◇→ Song |
| Composition | Strong | Coupled | Filled diamond | Order ◆→ OrderItem |
| Dependency | Weak | Transient | Dashed arrow | Service ⇢ Helper |
| Inheritance | Strongest | Coupled | Hollow arrow | CreditCard ▷ PaymentMethod |
| Realization | Medium | Independent | Dashed hollow arrow | Class ⇢▷ Interface |
You now have a comprehensive understanding of all major relationship types in OOP. In the next page, we'll dive deep into cardinality and multiplicity—how to specify how many objects participate in each relationship.