Loading learning content...
You've identified entities, chosen relationship types, specified cardinality, and determined navigability. But how do you know your relationship model is correct? Validation is the process of systematically checking your relationship design for errors, inconsistencies, and omissions before you commit to implementation.
Relationship validation catches problems early—when they're cheap to fix. A missing relationship discovered during coding costs hours. The same oversight discovered in production costs days or weeks, plus potential data migration nightmares.
By the end of this page, you will have a systematic checklist for validating relationships, understand common relationship anti-patterns, know how to verify relationships against requirements, and master techniques for relationship testing.
Use this systematic checklist to validate every relationship in your model:
Every relationship should trace back to a requirement. Walk through each use case and verify that your model supports it:
Requirement Tracing Process:
List All Use Cases: Extract every functional requirement involving data relationships
Trace Each Operation: For each use case, identify which entities and relationships are needed
Verify Traversability: Confirm you can reach all required data through the model
Check Constraints: Ensure cardinality supports all valid scenarios
Identify Gaps: Mark any use case that cannot be satisfied by current relationships
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
// Example: Validating relationships for an e-commerce system// Requirement: "A customer can view their order history" // VALIDATION:// ✓ Customer → Orders: Need 1:* (Customer has many Orders)// ✓ Navigability: Customer needs to find their orders? YES// ✓ Order → Customer: Each order belongs to exactly one customer? YES // Requirement: "An order shows all items with product details" // VALIDATION:// ✓ Order → OrderItems: Need 1:* (Order has many OrderItems)// ✓ OrderItem → Product: Need *:1 (Item references Product)// ✓ Can we traverse Order → OrderItems → Products? YES // Let's verify with code that the model supports the requirements interface OrderHistory { customerId: string; orders: Array<{ orderId: string; date: Date; items: Array<{ productName: string; quantity: number; price: number; }>; }>;} class CustomerService { constructor(private customerRepo: CustomerRepository) {} // This method VALIDATES that our model supports the requirement async getOrderHistory(customerId: string): Promise<OrderHistory> { const customer = await this.customerRepo.findById(customerId); if (!customer) throw new Error("Customer not found"); // Can we traverse Customer → Orders? ✓ const orders = customer.getOrders(); return { customerId: customer.id, orders: orders.map(order => ({ orderId: order.id, date: order.createdAt, // Can we traverse Order → OrderItems → Product? ✓ items: order.getItems().map(item => ({ productName: item.getProduct().name, quantity: item.quantity, price: item.unitPrice })) })) }; }}Mentally or literally 'walk' through your model for each use case. Start at the entry point entity and trace every navigation needed. If you can't reach required data, you have a missing relationship.
Avoid these common relationship design mistakes:
| Anti-Pattern | Problem | Solution |
|---|---|---|
| God Object Relationships | One class connects to everything | Decompose into focused classes with specific relationships |
| Missing Intermediate Entity | Many-to-many with attributes stored awkwardly | Create association class to hold relationship data |
| Unnecessary Bidirectionality | Circular references everywhere | Default to unidirectional; add back-refs only when needed |
| Wrong Aggregation Type | Composition where parts should survive | Analyze lifecycle carefully; use aggregation for independent parts |
| Implicit Relationships | Related data connected by matching IDs only | Make relationships explicit with proper references |
| Redundant Relationships | Multiple paths to same data causing sync issues | Choose canonical path; derive others or eliminate |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
// ANTI-PATTERN: Missing Intermediate Entity// Many-to-many without association class // BAD: Where does enrollment date go?class Student { courses: Course[] = []; // enrollmentDates: Map<Course, Date> = ??? // Awkward!} class Course { students: Student[] = [];} // GOOD: Association class holds relationship dataclass Enrollment { constructor( public readonly student: Student, public readonly course: Course, public readonly enrolledAt: Date, public grade?: string ) {}} class Student { enrollments: Enrollment[] = []; getCourses(): Course[] { return this.enrollments.map(e => e.course); }} // ------------------------------------------- // ANTI-PATTERN: Implicit Relationships (ID-based only) // BAD: Related by ID but no real relationshipclass OrderBad { customerId: string; // Just an ID, no real connection // To get customer, must query separately // No compile-time guarantee that customer exists} // GOOD: Explicit relationshipclass OrderGood { customer: Customer; // Real reference constructor(customer: Customer) { this.customer = customer; // Relationship is explicit and type-safe }} // ------------------------------------------- // ANTI-PATTERN: Redundant Relationships // BAD: Multiple paths to same dataclass Project { team: Team; members: Employee[]; // Redundant! Team already has members lead: Employee; // Could be derived from team} // GOOD: Single source of truthclass Project { team: Team; getMembers(): Employee[] { return this.team.getMembers(); } getLead(): Employee { return this.team.getLead(); }}Beyond structural correctness, relationships often have business constraints that must be validated:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
// Enforcing relationship constraints in code class Team { private members: Set<Employee> = new Set(); private lead: Employee | null = null; addMember(employee: Employee): void { this.members.add(employee); } // SUBSET CONSTRAINT: Lead must be a member setLead(employee: Employee): void { if (!this.members.has(employee)) { throw new Error("Lead must be a team member. Add them first."); } this.lead = employee; } removeMember(employee: Employee): void { // CONSTRAINT: Cannot remove lead if (this.lead === employee) { throw new Error("Cannot remove team lead. Assign new lead first."); } this.members.delete(employee); }} // EXCLUSION CONSTRAINT: Self-referential validationclass Employee { private manager: Employee | null = null; setManager(manager: Employee): void { // Cannot manage yourself if (manager === this) { throw new Error("An employee cannot be their own manager"); } // Prevent circular management chains let current: Employee | null = manager; while (current !== null) { if (current === this) { throw new Error("Circular management chain detected"); } current = current.manager; } this.manager = manager; }} // UNIQUENESS CONSTRAINT: No duplicate enrollmentsclass Course { private enrollments: Map<string, Enrollment> = new Map(); enroll(student: Student): Enrollment { if (this.enrollments.has(student.id)) { throw new Error("Student is already enrolled in this course"); } const enrollment = new Enrollment(student, this, new Date()); this.enrollments.set(student.id, enrollment); return enrollment; }} // TEMPORAL CONSTRAINT: Date-based validationclass Project { constructor( public readonly startDate: Date, public readonly endDate: Date ) {} addMember(employee: Employee, joinDate: Date): void { if (joinDate > this.endDate) { throw new Error("Cannot join a project after its end date"); } if (joinDate < this.startDate) { throw new Error("Cannot join a project before its start date"); } // Add member... }}Relationships require specific testing strategies to ensure correctness:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
// Testing relationship invariants describe('Department-Employee Relationship', () => { // Test cardinality: 1:* (department has 0 or more employees) test('department can exist without employees', () => { const dept = new Department('D1', 'Engineering'); expect(dept.getEmployeeCount()).toBe(0); }); test('department can have multiple employees', () => { const dept = new Department('D1', 'Engineering'); dept.hire(new Employee('E1', 'Alice')); dept.hire(new Employee('E2', 'Bob')); expect(dept.getEmployeeCount()).toBe(2); }); // Test bidirectional consistency test('adding employee updates both sides', () => { const dept = new Department('D1', 'Engineering'); const emp = new Employee('E1', 'Alice'); dept.hire(emp); expect(dept.getEmployees()).toContain(emp); expect(emp.getDepartment()).toBe(dept); }); test('transferring employee updates all references', () => { const engineering = new Department('D1', 'Engineering'); const product = new Department('D2', 'Product'); const emp = new Employee('E1', 'Alice'); engineering.hire(emp); emp.transferTo(product); expect(engineering.getEmployees()).not.toContain(emp); expect(product.getEmployees()).toContain(emp); expect(emp.getDepartment()).toBe(product); }); // Test constraint enforcement test('throws when violating constraint', () => { const team = new Team('T1', 'Alpha Team'); const nonMember = new Employee('E1', 'Alice'); expect(() => team.setLead(nonMember)) .toThrow('Lead must be a team member'); }); // Test edge cases test('removing last item from 1..* relationship throws', () => { const order = new Order('O1', initialItem); expect(() => order.removeItem(initialItem.id)) .toThrow('Order must have at least one item'); });});When reviewing a design (yours or others'), ask these relationship-focused questions:
You've now completed Manual Module 4: Defining Relationships. Let's consolidate what you've learned:
Congratulations! You now have the knowledge to define precise, validated relationships in your LLD models. In the next module, we'll cover applying design patterns—knowing which patterns to use for which problems.