Loading learning content...
You have a UML diagram with 15 classes. Where do you start?
This seemingly simple question has profound implications for productivity. Start in the wrong place, and you'll write code that immediately needs refactoring when you discover incompatibilities. Start correctly, and each class you implement builds naturally on the previous ones, with clear test points along the way.
Implementation order is a strategic decision, not a random choice. Experienced engineers follow predictable patterns that:
This page provides a systematic framework for determining implementation order in any design, whether you're in an interview implementing on a whiteboard or building a production system.
By the end of this page, you will understand dependency analysis for implementation ordering, the foundational-first strategy for class implementation, how to identify and implement core abstractions early, techniques for parallel implementation paths, and the relationship between implementation order and testability.
Consider two developers implementing the same design—a simple e-commerce order system with the following classes:
Order → OrderItem → Product
→ Customer
→ PaymentMethod (interface)
├── CreditCardPayment
└── PayPalPayment
→ ShippingAddress
Developer A starts with Order, because "it's the main class." They quickly realize:
Developer B analyzes dependencies first:
Developer B writes cleaner code, can test each class as it's completed, and never has to leave "TODO" stubs.
In interviews, demonstrating awareness of implementation order signals maturity. When you explain 'I'll implement Product first because it has no dependencies, then OrderItem which needs Product, then finally Order'—you show systems thinking that distinguishes senior candidates from junior ones.
Before implementing, we need to understand the dependency structure. This requires examining the UML diagram and building a dependency graph.
What counts as a dependency?
A class A depends on class B if:
Building the Dependency Graph:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
// ================================================// Example: Parking Lot System Dependency Analysis// ================================================ // Classes in the design:// - ParkingLot// - ParkingFloor// - ParkingSpot (abstract)// - CompactSpot// - LargeSpot// - HandicappedSpot// - Vehicle (abstract)// - Car// - Truck// - Motorcycle// - Ticket// - ParkingRate// - PaymentProcessor (interface)// - CashPayment// - CardPayment // Dependency Analysis: // No dependencies (roots):// - ParkingRate → []// - PaymentProcessor → [] (interface) // Single dependency:// - CashPayment → [PaymentProcessor]// - CardPayment → [PaymentProcessor]// - Vehicle → [] (abstract, no dependencies from user classes)// - Car → [Vehicle]// - Truck → [Vehicle]// - Motorcycle → [Vehicle]// - ParkingSpot → [Vehicle] (references what's parked)// - CompactSpot → [ParkingSpot]// - LargeSpot → [ParkingSpot]// - HandicappedSpot → [ParkingSpot] // Multiple dependencies:// - Ticket → [Vehicle, ParkingSpot, ParkingRate]// - ParkingFloor → [ParkingSpot] (has many spots)// - ParkingLot → [ParkingFloor, Ticket, PaymentProcessor] // ================================================// Resulting Implementation Order// ================================================ const implementationOrder = [ // Tier 0: No dependencies (foundations) { tier: 0, classes: ['ParkingRate', 'PaymentProcessor', 'Vehicle'] }, // Tier 1: Depends only on Tier 0 { tier: 1, classes: ['CashPayment', 'CardPayment', 'Car', 'Truck', 'Motorcycle', 'ParkingSpot'] }, // Tier 2: Depends on Tier 0-1 { tier: 2, classes: ['CompactSpot', 'LargeSpot', 'HandicappedSpot'] }, // Tier 3: Depends on Tier 0-2 { tier: 3, classes: ['Ticket', 'ParkingFloor'] }, // Tier 4: Depends on all previous (culmination) { tier: 4, classes: ['ParkingLot'] },]; // Visual representation:// // Tier 0: ParkingRate PaymentProcessor Vehicle// | | |// Tier 1: v CashPayment Car/Truck/Motorcycle// | CardPayment |// | | v// Tier 1: | | ParkingSpot// | | / | \// Tier 2: | | Compact Large Handicapped// | | \ | /// Tier 3: +---------------+-----> Ticket// | |// Tier 3: | ParkingFloor// | / |// Tier 4: +----> ParkingLotThe process of ordering classes by dependencies is a form of topological sort. Classes are arranged so that every class appears after all its dependencies. This is the same algorithm used by build systems (make, gradle) and package managers (npm, pip) to determine installation order.
The Foundational-First Strategy is the primary approach for implementation ordering. It follows the natural dependency structure from leaves to roots.
The Core Principle:
Implement classes with zero dependencies first, then implement classes whose dependencies are already complete.
This creates a layered implementation where each layer can be tested and verified before the next layer begins.
The Algorithm:
| Tier | Criteria | Typical Classes | Testing Approach |
|---|---|---|---|
| Tier 0 | No dependencies | Value objects, interfaces, abstract base classes with no fields | Unit test in isolation |
| Tier 1 | Depends only on Tier 0 | Simple concrete classes, interface implementations | Unit test with real Tier 0 objects |
| Tier 2 | Depends on Tier 0-1 | More complex classes, subclasses of Tier 1 | Unit test with real dependencies |
| Tier N | Depends on Tier 0 to (N-1) | Coordinator classes, facades | Integration test with all dependencies |
| Final Tier | Most dependencies | Main entry points, controllers, orchestrators | End-to-end testing |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
// ================================================// Library Management System - Implementation Order// ================================================ // Step 1: Analyze the design// Classes: Book, Author, Member, Librarian, Loan, Fine, Library, Reservation // Step 2: Map dependencies// Author → []// Book → [Author]// Member → []// Librarian → []// Loan → [Book, Member]// Fine → [Loan]// Reservation → [Book, Member]// Library → [Book, Member, Librarian, Loan, Fine, Reservation] // Step 3: Assign tiers// Tier 0: Author, Member, Librarian (no dependencies)// Tier 1: Book (needs Author)// Tier 2: Loan, Reservation (need Book, Member)// Tier 3: Fine (needs Loan)// Tier 4: Library (needs everything) // ================================================// Tier 0 Implementation// ================================================ // 1. Author - No dependenciesclass Author { constructor( private readonly id: string, private name: string, private biography: string ) {} getId(): string { return this.id; } getName(): string { return this.name; } getBiography(): string { return this.biography; }} // 2. Member - No dependenciesclass Member { private maxBooks: number = 5; constructor( private readonly id: string, private name: string, private email: string, private memberSince: Date = new Date() ) {} getId(): string { return this.id; } getName(): string { return this.name; } getEmail(): string { return this.email; } getMaxBooks(): number { return this.maxBooks; }} // 3. Librarian - No dependenciesclass Librarian { constructor( private readonly id: string, private name: string, private employeeId: string ) {} getId(): string { return this.id; } getName(): string { return this.name; }} // ================================================// Tier 1 Implementation (depends on Tier 0)// ================================================ // 4. Book - Depends on Authorclass Book { private available: boolean = true; constructor( private readonly isbn: string, private title: string, private author: Author, // Tier 0 - already implemented! private publicationYear: number ) {} getIsbn(): string { return this.isbn; } getTitle(): string { return this.title; } getAuthor(): Author { return this.author; } isAvailable(): boolean { return this.available; } checkout(): void { if (!this.available) { throw new Error("Book is not available"); } this.available = false; } returnBook(): void { this.available = true; }} // ================================================// Tier 2 Implementation (depends on Tier 0-1)// ================================================ // 5. Loan - Depends on Book (Tier 1), Member (Tier 0)class Loan { private returnDate: Date | null = null; constructor( private readonly id: string, private book: Book, // Tier 1 - implemented! private member: Member, // Tier 0 - implemented! private dueDate: Date, private checkoutDate: Date = new Date() ) {} getId(): string { return this.id; } getBook(): Book { return this.book; } getMember(): Member { return this.member; } getDueDate(): Date { return this.dueDate; } returnBook(): void { this.returnDate = new Date(); this.book.returnBook(); } isOverdue(): boolean { return new Date() > this.dueDate && !this.returnDate; }} // 6. Reservation - Depends on Book (Tier 1), Member (Tier 0)class Reservation { private fulfilled: boolean = false; constructor( private readonly id: string, private book: Book, private member: Member, private reservationDate: Date = new Date() ) {} fulfill(): void { this.fulfilled = true; } isFulfilled(): boolean { return this.fulfilled; }} // ================================================// Tier 3 Implementation (depends on Tier 0-2)// ================================================ // 7. Fine - Depends on Loan (Tier 2)class Fine { private paid: boolean = false; constructor( private readonly id: string, private loan: Loan, // Tier 2 - implemented! private amount: number ) {} pay(): void { this.paid = true; } isPaid(): boolean { return this.paid; } getAmount(): number { return this.amount; }} // ================================================// Tier 4 Implementation (Final - depends on all)// ================================================ // 8. Library - Orchestrator class, depends on everythingclass Library { private books: Book[] = []; private members: Member[] = []; private librarians: Librarian[] = []; private loans: Map<string, Loan> = new Map(); private fines: Fine[] = []; private reservations: Reservation[] = []; addBook(book: Book): void { this.books.push(book); } addMember(member: Member): void { this.members.push(member); } addLibrarian(librarian: Librarian): void { this.librarians.push(librarian); } checkoutBook(bookIsbn: string, memberId: string): Loan { const book = this.books.find(b => b.getIsbn() === bookIsbn); const member = this.members.find(m => m.getId() === memberId); if (!book || !member) { throw new Error("Book or member not found"); } if (!book.isAvailable()) { throw new Error("Book is not available"); } book.checkout(); const dueDate = new Date(); dueDate.setDate(dueDate.getDate() + 14); // 2 weeks const loan = new Loan( `loan_${Date.now()}`, book, member, dueDate ); this.loans.set(loan.getId(), loan); return loan; } // ... other library operations}Notice how after implementing Tier 0, we can immediately write unit tests for Author, Member, and Librarian. After Tier 1, we can test Book with real Author instances. After Tier 2, we can test Loan with real Book and Member instances. There's never a need for mocks when testing within the same codebase.
Sometimes dependencies form cycles: A depends on B, B depends on C, C depends on A. These cycles prevent clean tier assignment.
Example Cycle:
Order → Customer (Order references Customer)
Customer → Order (Customer has order history)
Strategies for Breaking Cycles:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
// ================================================// PROBLEM: Circular Dependency// ================================================ // ❌ This creates a cycle - can't determine which to implement first/*class Order { private customer: Customer; // Order needs Customer // ...} class Customer { private orderHistory: Order[]; // Customer needs Order // ...}*/ // ================================================// SOLUTION 1: Interface Extraction// ================================================ // Extract what Order needs from Customer into an interfaceinterface OrderCustomer { getId(): string; getName(): string; getShippingAddress(): Address;} // Now Order depends on OrderCustomer interface, not Customer classclass Order { private customer: OrderCustomer; // Interface dependency constructor(customer: OrderCustomer) { this.customer = customer; } getCustomerName(): string { return this.customer.getName(); }} // Customer implements the interfaceclass Customer implements OrderCustomer { private id: string; private name: string; private address: Address; private orderHistory: Order[] = []; // Can reference Order now constructor(id: string, name: string, address: Address) { this.id = id; this.name = name; this.address = address; } // Interface implementation getId(): string { return this.id; } getName(): string { return this.name; } getShippingAddress(): Address { return this.address; } // Class-specific methods addOrder(order: Order): void { this.orderHistory.push(order); }} // Implementation order becomes:// 1. Address (no dependencies)// 2. OrderCustomer interface (no dependencies)// 3. Order (depends on OrderCustomer interface)// 4. Customer (implements OrderCustomer, references Order) // ================================================// SOLUTION 2: Dependency Direction Reversal// ================================================ // Instead of Customer holding orders, orders are queried through a repository class OrderV2 { private customerId: string; // ID reference, not object reference constructor(customerId: string) { this.customerId = customerId; } getCustomerId(): string { return this.customerId; }} class CustomerV2 { private id: string; private name: string; // No order reference - orders are queried when needed constructor(id: string, name: string) { this.id = id; this.name = name; } getId(): string { return this.id; } getName(): string { return this.name; }} class OrderRepository { private orders: OrderV2[] = []; findByCustomerId(customerId: string): OrderV2[] { return this.orders.filter(o => o.getCustomerId() === customerId); }} // Implementation order becomes:// 1. CustomerV2 (no dependencies)// 2. OrderV2 (just uses customerId, not Customer object)// 3. OrderRepository (depends on OrderV2) // ================================================// SOLUTION 3: Event-Based Decoupling// ================================================ interface OrderEventListener { onOrderCreated(orderId: string, customerId: string): void;} class OrderV3 { private static listeners: OrderEventListener[] = []; static addListener(listener: OrderEventListener): void { this.listeners.push(listener); } constructor( private id: string, private customerId: string ) { // Notify listeners instead of directly updating Customer OrderV3.listeners.forEach(l => l.onOrderCreated(id, customerId)); }} class CustomerV3 implements OrderEventListener { private orderIds: string[] = []; constructor(private id: string, private name: string) { // Register to receive order events OrderV3.addListener(this); } onOrderCreated(orderId: string, customerId: string): void { if (customerId === this.id) { this.orderIds.push(orderId); } } getOrderIds(): string[] { return [...this.orderIds]; }} // Implementation order becomes:// 1. OrderEventListener interface (no dependencies)// 2. OrderV3 (depends on OrderEventListener interface)// 3. CustomerV3 (implements OrderEventListener)If you find circular dependencies in your design, pause and reconsider. While they can sometimes be handled, they often indicate that responsibilities are poorly distributed. Consider whether the cycle can be eliminated by rethinking which class should own which responsibility.
Within the same tier, some classes are more important than others. Core abstractions—the interfaces and abstract classes that define the system's extension points—should be implemented before their concrete implementations.
Why Core Abstractions First?
Identifying Core Abstractions:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
// ================================================// Payment Processing System - Abstraction Priority// ================================================ // Tier 0 Foundation (all no dependencies):// Within Tier 0, implement in this order: // 1. FIRST: Core interface - defines the contractinterface PaymentProcessor { processPayment(amount: number, source: PaymentSource): Promise<PaymentResult>; refund(transactionId: string): Promise<RefundResult>; validateSource(source: PaymentSource): boolean;} // 2. SECOND: Value objects used by the interfaceinterface PaymentSource { type: 'card' | 'bank' | 'wallet'; token: string;} interface PaymentResult { success: boolean; transactionId: string; error?: string;} interface RefundResult { success: boolean; refundId: string; error?: string;} // 3. THIRD: Abstract base class (if present)abstract class BasePaymentProcessor implements PaymentProcessor { protected maxRetries: number = 3; protected retryDelayMs: number = 1000; abstract processPayment(amount: number, source: PaymentSource): Promise<PaymentResult>; abstract refund(transactionId: string): Promise<RefundResult>; // Default implementation (can be overridden) validateSource(source: PaymentSource): boolean { return source.token.length > 0; } // Template method for retry logic protected async withRetry<T>(operation: () => Promise<T>): Promise<T> { let lastError: Error | null = null; for (let attempt = 0; attempt < this.maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error as Error; await this.delay(this.retryDelayMs * (attempt + 1)); } } throw lastError || new Error('Operation failed after retries'); } private delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); }} // ================================================// Tier 1: Concrete implementations (depend on Tier 0)// ================================================ // Now we can implement concrete payment processors// Each knows exactly what interface to implement class StripeProcessor extends BasePaymentProcessor { constructor(private apiKey: string) { super(); } async processPayment(amount: number, source: PaymentSource): Promise<PaymentResult> { return this.withRetry(async () => { // Stripe-specific implementation const response = await this.callStripeAPI('/charges', { amount, source: source.token }); return { success: true, transactionId: response.id }; }); } async refund(transactionId: string): Promise<RefundResult> { return this.withRetry(async () => { const response = await this.callStripeAPI('/refunds', { charge: transactionId }); return { success: true, refundId: response.id }; }); } private async callStripeAPI(endpoint: string, data: object): Promise<any> { // HTTP call to Stripe return { id: `stripe_${Date.now()}` }; }} class PayPalProcessor extends BasePaymentProcessor { constructor( private clientId: string, private clientSecret: string ) { super(); this.maxRetries = 5; // PayPal needs more retries } async processPayment(amount: number, source: PaymentSource): Promise<PaymentResult> { return this.withRetry(async () => { // PayPal-specific implementation const token = await this.getAccessToken(); const response = await this.callPayPalAPI('/v2/payments', { amount: { total: amount, currency: 'USD' }, payer: { token: source.token } }, token); return { success: true, transactionId: response.id }; }); } async refund(transactionId: string): Promise<RefundResult> { return this.withRetry(async () => { const token = await this.getAccessToken(); const response = await this.callPayPalAPI( `/v2/payments/${transactionId}/refund`, {}, token ); return { success: true, refundId: response.id }; }); } private async getAccessToken(): Promise<string> { // OAuth token retrieval return 'access_token'; } private async callPayPalAPI( endpoint: string, data: object, token: string ): Promise<any> { return { id: `paypal_${Date.now()}` }; }} // ================================================// Tier 2: Classes that use the processors// ================================================ // PaymentService can work with any PaymentProcessorclass PaymentService { constructor(private processor: PaymentProcessor) {} async checkout(cart: ShoppingCart, source: PaymentSource): Promise<string> { if (!this.processor.validateSource(source)) { throw new Error('Invalid payment source'); } const result = await this.processor.processPayment( cart.getTotal(), source ); if (!result.success) { throw new Error(result.error || 'Payment failed'); } return result.transactionId; }}By implementing PaymentProcessor interface first, we immediately know what StripeProcessor and PayPalProcessor must provide. The interface becomes a specification that guides implementation. If we started with StripeProcessor, we might design an interface around Stripe's quirks rather than a clean abstraction.
In team settings—or when you want to defer certain complexity—classes within the same tier can be implemented in parallel or in any order.
Identifying Parallel Paths:
Classes are parallelizable when:
Example Parallel Paths:
Tier 1 Parallel Groups:
├── Group A: CompactSpot, LargeSpot, HandicappedSpot
│ (all extend ParkingSpot, don't depend on each other)
├── Group B: Car, Truck, Motorcycle
│ (all extend Vehicle, don't depend on each other)
└── Group C: CashPayment, CardPayment
(all implement PaymentProcessor, don't depend on each other)
Solo Developer Strategy:
When working alone, prioritize by:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
// ================================================// Vehicle Hierarchy - Parallel Implementation// ================================================ // Tier 0: Abstract baseabstract class Vehicle { protected licensePlate: string; constructor(licensePlate: string) { this.licensePlate = licensePlate; } abstract getSize(): VehicleSize; abstract getFuelType(): FuelType; getLicensePlate(): string { return this.licensePlate; }} type VehicleSize = 'compact' | 'regular' | 'large';type FuelType = 'gasoline' | 'diesel' | 'electric' | 'hybrid'; // ================================================// Tier 1: Concrete vehicles (PARALLEL)// ================================================ // These can be implemented in any order - no dependencies between them // Implementation A: Start with simplest (Motorcycle)class Motorcycle extends Vehicle { private engineSize: number; constructor(licensePlate: string, engineSize: number) { super(licensePlate); this.engineSize = engineSize; } getSize(): VehicleSize { return 'compact'; } getFuelType(): FuelType { return 'gasoline'; } getEngineSize(): number { return this.engineSize; }} // Implementation B: Regular complexity (Car)class Car extends Vehicle { private numDoors: number; private fuelType: FuelType; constructor(licensePlate: string, numDoors: number, fuelType: FuelType) { super(licensePlate); this.numDoors = numDoors; this.fuelType = fuelType; } getSize(): VehicleSize { return 'regular'; } getFuelType(): FuelType { return this.fuelType; } getNumDoors(): number { return this.numDoors; }} // Implementation C: Most complex (Truck)class Truck extends Vehicle { private payloadCapacity: number; private hasTrailer: boolean; constructor( licensePlate: string, payloadCapacity: number, hasTrailer: boolean = false ) { super(licensePlate); this.payloadCapacity = payloadCapacity; this.hasTrailer = hasTrailer; } getSize(): VehicleSize { return 'large'; } getFuelType(): FuelType { return 'diesel'; } getPayloadCapacity(): number { return this.payloadCapacity; } attachTrailer(): void { this.hasTrailer = true; } detachTrailer(): void { this.hasTrailer = false; } // Trucks have extra complexity requiresCommercialLicense(): boolean { return this.payloadCapacity > 10000 || this.hasTrailer; }} // ================================================// Templating Strategy// ================================================ // After implementing one fully (with tests), use as template: /*abstract class PaymentProcessor { async process(amount: number): Promise<Result>; async refund(txnId: string): Promise<Result>;} // Template: StripeProcessor (fully implemented + tested)class StripeProcessor extends PaymentProcessor { ... } // Clone template, change details:class PayPalProcessor extends PaymentProcessor { // Same structure as StripeProcessor // Different implementation details} class SquareProcessor extends PaymentProcessor { // Same structure as StripeProcessor // Different implementation details}*/In interviews with limited time, implement ONE variant of parallel classes fully (e.g., just Car, not Motorcycle and Truck). Then say: 'Motorcycle and Truck follow the same pattern, differing only in [specific details]. I'll implement them the same way if time permits.' This demonstrates you understand the pattern without wasting time on repetitive code.
A powerful benefit of dependency-ordered implementation is immediate testability. Each class you complete can be tested with real dependencies—no mocks required.
The Testing Progression:
| Tier | Test Type | Dependencies | Example |
|---|---|---|---|
| Tier 0 | Pure unit tests | None (isolated) | Test Author name validation |
| Tier 1 | Unit tests with real deps | Real Tier 0 objects | Test Book with real Author |
| Tier 2 | Unit tests with real deps | Real Tier 0-1 objects | Test Loan with real Book, Member |
| Tier N | Integration-ish tests | Real object graphs | Test Order with full object tree |
| Final | End-to-end tests | Complete system | Test full workflow |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
// ================================================// Testing at Each Tier// ================================================ // Tier 0 Test: Author (no dependencies)describe('Author', () => { it('should create author with valid data', () => { const author = new Author('1', 'Isaac Asimov', 'Prolific sci-fi writer'); expect(author.getId()).toBe('1'); expect(author.getName()).toBe('Isaac Asimov'); expect(author.getBiography()).toBe('Prolific sci-fi writer'); }); it('should reject empty name', () => { expect(() => new Author('1', '', 'Bio')).toThrow('Name is required'); }); // No mocks needed - Author has no dependencies}); // Tier 0 Test: Member (no dependencies)describe('Member', () => { it('should create member with default borrowing limit', () => { const member = new Member('m1', 'John Doe', 'john@example.com'); expect(member.getMaxBooks()).toBe(5); }); // No mocks needed}); // Tier 1 Test: Book (depends on Author)describe('Book', () => { let author: Author; beforeEach(() => { // Use REAL Author - it's already implemented and tested! author = new Author('a1', 'Isaac Asimov', 'Sci-fi master'); }); it('should create book with author', () => { const book = new Book('978-0', 'Foundation', author, 1951); expect(book.getTitle()).toBe('Foundation'); expect(book.getAuthor().getName()).toBe('Isaac Asimov'); expect(book.isAvailable()).toBe(true); }); it('should mark book as unavailable when checked out', () => { const book = new Book('978-0', 'Foundation', author, 1951); book.checkout(); expect(book.isAvailable()).toBe(false); }); it('should throw when checking out unavailable book', () => { const book = new Book('978-0', 'Foundation', author, 1951); book.checkout(); // First checkout expect(() => book.checkout()).toThrow('Book is not available'); }); // Still no mocks - using real Author}); // Tier 2 Test: Loan (depends on Book, Member)describe('Loan', () => { let author: Author; let book: Book; let member: Member; beforeEach(() => { // All real objects - they're implemented and tested! author = new Author('a1', 'Isaac Asimov', 'Sci-fi master'); book = new Book('978-0', 'Foundation', author, 1951); member = new Member('m1', 'John Doe', 'john@example.com'); }); it('should create loan with future due date', () => { const dueDate = new Date(); dueDate.setDate(dueDate.getDate() + 14); const loan = new Loan('loan1', book, member, dueDate); expect(loan.getBook()).toBe(book); expect(loan.getMember()).toBe(member); expect(loan.isOverdue()).toBe(false); }); it('should detect overdue loan', () => { const pastDueDate = new Date(); pastDueDate.setDate(pastDueDate.getDate() - 1); // Yesterday const loan = new Loan('loan1', book, member, pastDueDate); expect(loan.isOverdue()).toBe(true); }); it('should return book and mark as no longer overdue', () => { const pastDueDate = new Date(); pastDueDate.setDate(pastDueDate.getDate() - 1); const loan = new Loan('loan1', book, member, pastDueDate); expect(loan.isOverdue()).toBe(true); loan.returnBook(); expect(loan.isOverdue()).toBe(false); expect(book.isAvailable()).toBe(true); }); // Still no mocks - real object graph}); // Tier 4 Test: Library (orchestrator)describe('Library', () => { let library: Library; let author: Author; let book: Book; let member: Member; beforeEach(() => { // Build real object graph author = new Author('a1', 'Isaac Asimov', 'Sci-fi master'); book = new Book('978-0', 'Foundation', author, 1951); member = new Member('m1', 'John Doe', 'john@example.com'); library = new Library(); library.addBook(book); library.addMember(member); }); it('should checkout available book to member', () => { const loan = library.checkoutBook('978-0', 'm1'); expect(loan.getBook()).toBe(book); expect(loan.getMember()).toBe(member); expect(book.isAvailable()).toBe(false); }); it('should reject checkout of unavailable book', () => { library.checkoutBook('978-0', 'm1'); // First checkout expect(() => library.checkoutBook('978-0', 'm1')) .toThrow('Book is not available'); }); // Integration-level test with real object graph});Mocks become necessary at system boundaries—when a class depends on external services (databases, APIs, file systems). For internal class dependencies within your design, real objects are cleaner and more trustworthy. The foundational-first strategy ensures internal dependencies are always available for testing.
We've established a systematic framework for determining implementation order. Here's the consolidated methodology:
You now have a strategic framework for implementation ordering that minimizes rework, enables incremental testing, and provides clear progress visibility. This skill directly translates to interview success—explaining your implementation order demonstrates systems thinking and experience. Next, we'll explore the 'Interface First, Implementation Second' principle that complements this ordering strategy.
What's next:
With the implementation order established, the next page explores the Interface First, Implementation Second principle—why defining contracts before writing concrete code leads to better designs and faster development, especially in interview settings.