Loading content...
Knowing that a Customer is associated with Order is only half the story. The critical question is: How many? Can a customer have zero orders? Multiple orders? Can an order belong to exactly one customer, or could it be shared? These numerical constraints—cardinality and multiplicity—transform vague relationships into precise, enforceable contracts.
Getting multiplicity wrong leads to some of the most insidious bugs in software. A system designed for one-to-one that encounters one-to-many in production will corrupt data silently. A model that assumes 'at least one' but receives zero will crash unexpectedly.
By the end of this page, you will master cardinality notation (1, 0..1, 0.., 1.., n..m), understand how multiplicity constraints map to code, and know how to enforce these constraints at both compile-time and runtime.
These terms are often used interchangeably, but they have distinct origins:
Cardinality (from database theory):
Multiplicity (from UML):
In practice, we use these terms interchangeably in most design discussions. What matters is specifying the exact numeric constraints on your relationships.
| Notation | Meaning | Description |
|---|---|---|
| 1 | Exactly one | Mandatory, single instance required |
| 0..1 | Zero or one | Optional, at most one instance |
| 0..* | Zero or more | Optional collection, any size |
| 1..* | One or more | Mandatory collection, at least one |
| Many (shorthand) | Same as 0..* | |
| n | Specific number | Exactly n instances (e.g., 3) |
| n..m | Range | Between n and m instances (e.g., 2..5) |
In a one-to-one relationship, each instance of class A is associated with exactly one instance of class B, and vice versa.
Variations:
Real-World Examples:
User ↔ UserProfile (each user has exactly one profile)Person ↔ Passport (each person has at most one passport)Country ↔ CapitalCity (each country has one capital)1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
// One-to-One: User has exactly one Profile// Mandatory on both sides (1--1) class UserProfile { private readonly userId: string; private bio: string = ""; private avatarUrl: string | null = null; private preferences: UserPreferences; constructor(userId: string, preferences: UserPreferences) { this.userId = userId; this.preferences = preferences; } setBio(bio: string): void { if (bio.length > 500) { throw new Error("Bio cannot exceed 500 characters"); } this.bio = bio; } getBio(): string { return this.bio; } getUserId(): string { return this.userId; }} class User { private readonly id: string; private readonly email: string; private readonly profile: UserProfile; // Mandatory: 1--1 constructor(id: string, email: string) { this.id = id; this.email = email; // Profile is created WITH the user (enforces 1--1) this.profile = new UserProfile(id, new UserPreferences()); } // No setProfile() method - profile is immutably linked getProfile(): UserProfile { return this.profile; } getId(): string { return this.id; }} // One-to-One with optional side (1 -- 0..1)// Employee must exist, but ParkingSpot assignment is optional class ParkingSpot { private assignedEmployee: Employee | null = null; constructor(public readonly spotNumber: string) {} assignTo(employee: Employee): void { if (this.assignedEmployee !== null) { throw new Error("Spot already assigned"); } this.assignedEmployee = employee; } unassign(): void { this.assignedEmployee = null; } isAvailable(): boolean { return this.assignedEmployee === null; }} class Employee { private parkingSpot: ParkingSpot | null = null; // Optional: 0..1 constructor( public readonly id: string, public readonly name: string ) {} assignParkingSpot(spot: ParkingSpot): void { if (this.parkingSpot !== null) { throw new Error("Employee already has a parking spot"); } this.parkingSpot = spot; spot.assignTo(this); } getParkingSpot(): ParkingSpot | null { return this.parkingSpot; }}For mandatory 1:1 relationships, create the related object in the constructor. For optional 1:1, use nullable fields with methods that enforce the constraint (e.g., throwing if a second assignment is attempted).
One-to-many is perhaps the most common relationship type. One instance of class A is associated with multiple instances of class B, but each B belongs to exactly one A.
Variations:
Real-World Examples:
Author → Book (author writes many books; each book has one author)Department → Employee (department has many employees)Order → OrderItem (order contains many items)1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
// One-to-Many: Author (1) to Books (0..*)// An author can have zero or more books// Each book has exactly one author class Book { private readonly author: Author; // Mandatory: 1 (each book has ONE author) constructor( public readonly isbn: string, public readonly title: string, author: Author ) { this.author = author; } getAuthor(): Author { return this.author; }} class Author { private readonly books: Book[] = []; // 0..*: can have zero or more books constructor( public readonly id: string, public readonly name: string ) {} // Author creates their own books (maintains bidirectional consistency) writeBook(isbn: string, title: string): Book { const book = new Book(isbn, title, this); this.books.push(book); return book; } getBooks(): ReadonlyArray<Book> { return this.books; } getBookCount(): number { return this.books.length; }} // One-to-Many with MANDATORY children: 1 -- 1..*// A Course MUST have at least one Lesson class Lesson { constructor( public readonly id: string, public readonly title: string, public readonly durationMinutes: number ) {}} class Course { private readonly lessons: Lesson[]; // 1..*: at least one required // Constructor enforces 1..* constraint constructor( public readonly id: string, public readonly title: string, initialLesson: Lesson // Must provide at least one ) { this.lessons = [initialLesson]; } addLesson(lesson: Lesson): void { this.lessons.push(lesson); } removeLesson(lessonId: string): void { // Enforce 1..* - cannot remove if only one lesson if (this.lessons.length <= 1) { throw new Error("Course must have at least one lesson"); } const index = this.lessons.findIndex(l => l.id === lessonId); if (index !== -1) { this.lessons.splice(index, 1); } } getLessons(): ReadonlyArray<Lesson> { return this.lessons; }}In a many-to-many relationship, instances of both classes can be associated with multiple instances of the other.
Variations:
Real-World Examples:
Student ↔ Course (students enroll in many courses; courses have many students)Product ↔ Order (products appear in many orders; orders contain many products)Actor ↔ Movie (actors perform in many movies; movies have many actors)123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
// Many-to-Many: Students and Courses// A student can enroll in many courses// A course can have many students class Student { private readonly courses: Set<Course> = new Set(); constructor( public readonly id: string, public readonly name: string ) {} enroll(course: Course): void { if (!this.courses.has(course)) { this.courses.add(course); course.addStudent(this); } } drop(course: Course): void { if (this.courses.has(course)) { this.courses.delete(course); course.removeStudent(this); } } getCourses(): ReadonlySet<Course> { return this.courses; }} class Course { private readonly students: Set<Student> = new Set(); constructor( public readonly code: string, public readonly title: string, public readonly maxCapacity: number = 30 ) {} addStudent(student: Student): void { if (this.students.size >= this.maxCapacity) { throw new Error("Course is at capacity"); } if (!this.students.has(student)) { this.students.add(student); student.enroll(this); } } removeStudent(student: Student): void { if (this.students.has(student)) { this.students.delete(student); student.drop(this); } } getStudents(): ReadonlySet<Student> { return this.students; } getEnrollmentCount(): number { return this.students.size; }} // Many-to-Many with Association Class (carrying additional data)// When the relationship itself has attributes interface Enrollment { student: Student; course: Course; enrollmentDate: Date; grade?: string;} class EnrollmentManager { private enrollments: Map<string, Enrollment> = new Map(); private getKey(studentId: string, courseCode: string): string { return `${studentId}:${courseCode}`; } enroll(student: Student, course: Course): Enrollment { const key = this.getKey(student.id, course.code); if (this.enrollments.has(key)) { throw new Error("Already enrolled"); } const enrollment: Enrollment = { student, course, enrollmentDate: new Date(), }; this.enrollments.set(key, enrollment); return enrollment; } recordGrade(studentId: string, courseCode: string, grade: string): void { const key = this.getKey(studentId, courseCode); const enrollment = this.enrollments.get(key); if (!enrollment) { throw new Error("Enrollment not found"); } enrollment.grade = grade; }}When a many-to-many relationship has its own attributes (like enrollment date or grade), create an association class to represent the relationship itself. This is common in real systems and maps directly to junction tables in databases.
Multiplicity constraints should be enforced in code, not just documented. Here are strategies for each type:
| Multiplicity | Compile-Time | Runtime |
|---|---|---|
| 1 (mandatory) | Non-nullable field, required constructor param | Null checks, assertions |
| 0..1 (optional) | Nullable field or Optional<T> | Check before use |
| 0..* (collection) | Initialize empty, allow any additions | No special constraints |
| 1..* (at least one) | Require initial item in constructor | Check count before removal |
| n (exactly n) | Fixed-size array or tuple type | Validate size on mutations |
| n..m (range) | N/A for compile-time | Validate count on add/remove |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
// Enforcing 2..5 multiplicity (range constraint)// A Team must have between 2 and 5 members class TeamMember { constructor( public readonly id: string, public readonly name: string, public readonly role: string ) {}} class Team { private static readonly MIN_MEMBERS = 2; private static readonly MAX_MEMBERS = 5; private readonly members: TeamMember[]; // Constructor enforces minimum constructor( public readonly name: string, initialMembers: [TeamMember, TeamMember, ...TeamMember[]] // At least 2 ) { if (initialMembers.length < Team.MIN_MEMBERS) { throw new Error(`Team must have at least ${Team.MIN_MEMBERS} members`); } if (initialMembers.length > Team.MAX_MEMBERS) { throw new Error(`Team cannot have more than ${Team.MAX_MEMBERS} members`); } this.members = [...initialMembers]; } addMember(member: TeamMember): void { if (this.members.length >= Team.MAX_MEMBERS) { throw new Error(`Cannot add member: team is at maximum capacity (${Team.MAX_MEMBERS})`); } if (this.members.some(m => m.id === member.id)) { throw new Error("Member already in team"); } this.members.push(member); } removeMember(memberId: string): void { if (this.members.length <= Team.MIN_MEMBERS) { throw new Error(`Cannot remove: team must have at least ${Team.MIN_MEMBERS} members`); } const index = this.members.findIndex(m => m.id === memberId); if (index === -1) { throw new Error("Member not found in team"); } this.members.splice(index, 1); } getMembers(): ReadonlyArray<TeamMember> { return this.members; } getMemberCount(): number { return this.members.length; } canAddMember(): boolean { return this.members.length < Team.MAX_MEMBERS; } canRemoveMember(): boolean { return this.members.length > Team.MIN_MEMBERS; }}Multiplicity constraints must be enforced at the database level for data integrity:
| Relationship | Database Pattern | Constraints |
|---|---|---|
| 1:1 | FK in either table with UNIQUE | NOT NULL if mandatory; UNIQUE always |
| 1:* | FK in 'many' side pointing to 'one' | NOT NULL if mandatory on many side |
| : | Junction table with two FKs | Composite PK or UNIQUE constraint on pair |
| 0..1 | Nullable FK | UNIQUE if 1:1; nullable allows zero |
| 1..* | Application-level validation | CHECK or trigger to enforce minimum |
Always enforce multiplicity at BOTH the application layer and database layer. Application validation provides friendly error messages; database constraints prevent data corruption from bugs or direct SQL queries.
You now understand how to specify and enforce the numeric constraints on relationships. In the next page, we'll explore navigability—determining which direction(s) objects can traverse relationships.