Loading content...
Dependency Injection is often justified with abstract promises: 'loose coupling,' 'better design,' 'cleaner architecture.' While true, these abstractions don't convey the concrete value DI delivers daily. This page grounds DI's benefits in tangible, practical terms.
We'll explore three interconnected pillars—flexibility, testability, and maintainability—showing how each manifests in real code, real tests, and real maintenance scenarios. By the end, you'll understand not just that DI helps, but exactly how and why it helps in specific situations.
By the end of this page, you will deeply understand how DI enables runtime behavior changes, dramatically simplifies testing, and reduces the cost of system evolution. You'll see concrete code examples demonstrating each benefit.
Flexibility is the capacity to change behavior without modifying existing code. DI enables flexibility by externalizing the choice of which implementations to use. The consuming code remains unchanged; only the injected dependencies change.
The Flexibility Spectrum:
Flexibility exists on a spectrum from compile-time to runtime:
DI supports all four levels, with greater power as you move toward runtime flexibility.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
// ═══════════════════════════════════════════// THE ABSTRACTION: What payment processing means// ═══════════════════════════════════════════interface IPaymentProcessor { charge(amount: Money, card: CardDetails): Promise<PaymentResult>; refund(transactionId: string, amount: Money): Promise<RefundResult>; getTransaction(transactionId: string): Promise<Transaction>;} // ═══════════════════════════════════════════// IMPLEMENTATION 1: Production Stripe Integration// ═══════════════════════════════════════════class StripePaymentProcessor implements IPaymentProcessor { constructor(private readonly apiKey: string) {} async charge(amount: Money, card: CardDetails): Promise<PaymentResult> { // Real Stripe API call const response = await fetch('https://api.stripe.com/v1/charges', { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ amount: amount.cents.toString(), currency: amount.currency, source: card.token }) }); return this.parseResponse(response); } // ... refund, getTransaction implementations} // ═══════════════════════════════════════════// IMPLEMENTATION 2: Development Sandbox// ═══════════════════════════════════════════class SandboxPaymentProcessor implements IPaymentProcessor { async charge(amount: Money, card: CardDetails): Promise<PaymentResult> { // No real charges, simulates success/failure based on amount console.log(`[SANDBOX] Simulating charge of ${amount.formatted}`); // Special test amounts trigger different behaviors if (amount.cents === 9999) { return { success: false, error: 'Card declined (sandbox test)' }; } return { success: true, transactionId: `sandbox_${Date.now()}`, amount }; } // ... simulated refund, getTransaction} // ═══════════════════════════════════════════// IMPLEMENTATION 3: Test Mock for Unit Tests// ═══════════════════════════════════════════class MockPaymentProcessor implements IPaymentProcessor { private readonly responses: Map<string, PaymentResult> = new Map(); public chargeCallCount = 0; public lastChargedAmount?: Money; // Test setup: predetermine responses givenChargeWillSucceed(transactionId: string): void { this.responses.set('charge', { success: true, transactionId, amount: Money.zero() }); } givenChargeWillFail(error: string): void { this.responses.set('charge', { success: false, error }); } async charge(amount: Money, card: CardDetails): Promise<PaymentResult> { this.chargeCallCount++; this.lastChargedAmount = amount; return this.responses.get('charge') || { success: false, error: 'No mock configured' }; } // ... mock refund, getTransaction} // ═══════════════════════════════════════════// THE CONSUMER: Unchanged across all environments// ═══════════════════════════════════════════class CheckoutService { constructor(private readonly paymentProcessor: IPaymentProcessor) {} async checkout(cart: Cart, paymentDetails: CardDetails): Promise<CheckoutResult> { // This code NEVER changes regardless of payment processor const total = cart.calculateTotal(); const result = await this.paymentProcessor.charge(total, paymentDetails); if (result.success) { return { success: true, orderId: generateOrderId() }; } return { success: false, error: result.error }; }} // ═══════════════════════════════════════════// COMPOSITION: Different configurations per context// ═══════════════════════════════════════════// Production compositionfunction createProductionCheckout(): CheckoutService { return new CheckoutService( new StripePaymentProcessor(process.env.STRIPE_LIVE_KEY!) );} // Development composition function createDevelopmentCheckout(): CheckoutService { return new CheckoutService(new SandboxPaymentProcessor());} // Test compositionfunction createTestCheckout(mock: MockPaymentProcessor): CheckoutService { return new CheckoutService(mock);}The Flexibility Payoff:
CheckoutService across all three contextsWhen business requirements change ('We need to support PayPal internationally'), flexibility through DI means the engineering response is 'Implement one adapter class and update configuration' rather than 'Rewrite and retest the entire checkout flow.'
Beyond simple implementation swapping, DI enables sophisticated flexibility patterns that would be impossible or extremely complex without dependency externalization.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
// ═══════════════════════════════════════════// DECORATOR: Add Logging to ANY Payment Processor// ═══════════════════════════════════════════class LoggingPaymentDecorator implements IPaymentProcessor { constructor( private readonly inner: IPaymentProcessor, private readonly logger: ILogger ) {} async charge(amount: Money, card: CardDetails): Promise<PaymentResult> { this.logger.info(`Charging ${amount.formatted}`, { cardLast4: card.last4 }); const start = performance.now(); try { const result = await this.inner.charge(amount, card); const duration = performance.now() - start; this.logger.info(`Charge completed in ${duration}ms`, { success: result.success, transactionId: result.transactionId }); return result; } catch (error) { this.logger.error('Charge failed with exception', { error }); throw error; } } // ... decorated refund, getTransaction} // ═══════════════════════════════════════════// DECORATOR: Add Retry Logic via DI// ═══════════════════════════════════════════class RetryingPaymentDecorator implements IPaymentProcessor { constructor( private readonly inner: IPaymentProcessor, private readonly maxRetries: number = 3, private readonly retryDelayMs: number = 1000 ) {} async charge(amount: Money, card: CardDetails): Promise<PaymentResult> { let lastError: Error | undefined; for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { return await this.inner.charge(amount, card); } catch (error) { lastError = error as Error; if (attempt < this.maxRetries) { await this.delay(this.retryDelayMs * attempt); } } } throw lastError; } private delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); }} // ═══════════════════════════════════════════// DECORATOR: Add Circuit Breaker via DI// ═══════════════════════════════════════════class CircuitBreakerPaymentDecorator implements IPaymentProcessor { private failures = 0; private circuitOpen = false; private circuitOpenTime?: Date; constructor( private readonly inner: IPaymentProcessor, private readonly failureThreshold: number = 5, private readonly resetTimeMs: number = 30000 ) {} async charge(amount: Money, card: CardDetails): Promise<PaymentResult> { if (this.isCircuitOpen()) { return { success: false, error: 'Payment service temporarily unavailable' }; } try { const result = await this.inner.charge(amount, card); this.recordSuccess(); return result; } catch (error) { this.recordFailure(); throw error; } } private isCircuitOpen(): boolean { if (!this.circuitOpen) return false; const elapsed = Date.now() - this.circuitOpenTime!.getTime(); if (elapsed > this.resetTimeMs) { this.circuitOpen = false; this.failures = 0; return false; } return true; } private recordSuccess(): void { this.failures = 0; } private recordFailure(): void { this.failures++; if (this.failures >= this.failureThreshold) { this.circuitOpen = true; this.circuitOpenTime = new Date(); } }} // ═══════════════════════════════════════════// COMPOSITION: Stack Decorators for Production-Ready Payment// ═══════════════════════════════════════════function createProductionPaymentProcessor(): IPaymentProcessor { // Start with the real implementation const stripe = new StripePaymentProcessor(process.env.STRIPE_KEY!); // Wrap with retry logic const withRetry = new RetryingPaymentDecorator(stripe, 3, 500); // Wrap with circuit breaker const withCircuitBreaker = new CircuitBreakerPaymentDecorator(withRetry, 5, 30000); // Wrap with logging (outermost to capture everything) const withLogging = new LoggingPaymentDecorator(withCircuitBreaker, logger); return withLogging;} // CheckoutService receives this fully-decorated processor// It has NO knowledge of retries, circuit breakers, or loggingWhat DI Enables Here:
StripePaymentProcessor focuses only on Stripe API integrationTestability—the ease of writing comprehensive, fast, reliable tests—is often the first tangible benefit teams experience when adopting DI. Without DI, unit tests become integration tests. With DI, true isolation is possible.
Why DI Transforms Testing:
Tests need to control the environment. When a class creates its own dependencies:
With DI, tests control the environment by providing controlled dependencies.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
// ═══════════════════════════════════════════// WITHOUT DI: Testing is Painful// ═══════════════════════════════════════════class OrderServiceWithoutDI { private paymentGateway = new StripeGateway(config.stripeKey); private emailSender = new SendGridEmailer(config.sendGridKey); private inventoryApi = new WarehouseApi(config.warehouseUrl); async processOrder(order: Order): Promise<OrderResult> { // Validates inventory (HTTP call) const available = await this.inventoryApi.checkStock(order.items); if (!available) return { success: false, reason: 'Out of stock' }; // Charges card (HTTP call to Stripe) const payment = await this.paymentGateway.charge(order.total, order.card); if (!payment.success) return { success: false, reason: 'Payment failed' }; // Sends confirmation (HTTP call to SendGrid) await this.emailSender.send(order.customer.email, 'Order confirmed!'); return { success: true, orderId: payment.transactionId }; }} // Testing this class requires:// 1. Running Stripe test server (or mocking at network level)// 2. Running SendGrid test server (or mocking at network level)// 3. Running Warehouse API (or mocking at network level)// 4. Each test takes 100ms-1000ms (network latency)// 5. Tests fail when any external service is down// 6. Testing error conditions requires external service manipulation describe('OrderServiceWithoutDI', () => { it('should process order successfully', async () => { // Setup: Configure test accounts in Stripe, SendGrid, Warehouse // This test takes ~500ms and requires network // If Stripe test server is down, test fails // If network is slow, test is slow }); it('should handle payment failure', async () => { // How do we make Stripe return a failure? // Use a magic test card number: 4000000000000002 // But this requires KNOWING Stripe's test cards // And it still requires a network call });}); // ═══════════════════════════════════════════// WITH DI: Testing is Straightforward// ═══════════════════════════════════════════interface IPaymentGateway { charge(amount: Money, card: CardDetails): Promise<PaymentResult>;} interface IEmailSender { send(to: string, message: string): Promise<void>;} interface IInventoryApi { checkStock(items: OrderItem[]): Promise<boolean>;} class OrderServiceWithDI { constructor( private readonly paymentGateway: IPaymentGateway, private readonly emailSender: IEmailSender, private readonly inventoryApi: IInventoryApi ) {} async processOrder(order: Order): Promise<OrderResult> { const available = await this.inventoryApi.checkStock(order.items); if (!available) return { success: false, reason: 'Out of stock' }; const payment = await this.paymentGateway.charge(order.total, order.card); if (!payment.success) return { success: false, reason: 'Payment failed' }; await this.emailSender.send(order.customer.email, 'Order confirmed!'); return { success: true, orderId: payment.transactionId }; }} // Testing with DI: fast, isolated, comprehensivedescribe('OrderServiceWithDI', () => { let orderService: OrderServiceWithDI; let mockPayment: jest.Mocked<IPaymentGateway>; let mockEmail: jest.Mocked<IEmailSender>; let mockInventory: jest.Mocked<IInventoryApi>; beforeEach(() => { // Create fresh mocks for each test mockPayment = { charge: jest.fn() }; mockEmail = { send: jest.fn() }; mockInventory = { checkStock: jest.fn() }; // Inject mocks orderService = new OrderServiceWithDI(mockPayment, mockEmail, mockInventory); }); it('should process order successfully when all steps succeed', async () => { // Arrange: Configure mock responses mockInventory.checkStock.mockResolvedValue(true); mockPayment.charge.mockResolvedValue({ success: true, transactionId: 'tx_123' }); mockEmail.send.mockResolvedValue(undefined); // Act const result = await orderService.processOrder(testOrder); // Assert expect(result.success).toBe(true); expect(result.orderId).toBe('tx_123'); expect(mockInventory.checkStock).toHaveBeenCalledWith(testOrder.items); expect(mockPayment.charge).toHaveBeenCalledWith(testOrder.total, testOrder.card); expect(mockEmail.send).toHaveBeenCalledWith(testOrder.customer.email, 'Order confirmed!'); }); it('should return failure when inventory is unavailable', async () => { // Arrange: Inventory returns false mockInventory.checkStock.mockResolvedValue(false); // Act const result = await orderService.processOrder(testOrder); // Assert expect(result.success).toBe(false); expect(result.reason).toBe('Out of stock'); expect(mockPayment.charge).not.toHaveBeenCalled(); // Not reached expect(mockEmail.send).not.toHaveBeenCalled(); // Not reached }); it('should return failure when payment is declined', async () => { // Arrange: Inventory OK, payment fails mockInventory.checkStock.mockResolvedValue(true); mockPayment.charge.mockResolvedValue({ success: false, error: 'Declined' }); // Act const result = await orderService.processOrder(testOrder); // Assert expect(result.success).toBe(false); expect(result.reason).toBe('Payment failed'); expect(mockEmail.send).not.toHaveBeenCalled(); // Not reached }); // Each test runs in ~1ms with zero network calls // Each test is independent and isolated // Edge cases are trivially testable});| Aspect | Without DI | With DI |
|---|---|---|
| Test Speed | 100-1000ms (network) | 1-10ms (in-memory) |
| Reliability | Flaky (external systems) | Deterministic |
| Setup Complexity | Configure external services | Create mock objects |
| Edge Case Coverage | Limited by external system behavior | Complete control |
| Parallelization | Difficult (shared external state) | Trivial (isolated) |
| CI/CD Requirements | External service access | None |
A test suite with 500 tests that each take 500ms (network) takes 250 seconds. With DI and mocks, the same tests take 5 seconds. That's not a marginal improvement—it's the difference between developers running tests before every commit and developers never running tests locally.
DI doesn't just make existing tests faster—it makes previously untestable scenarios testable. Edge cases, error conditions, and timing-dependent behavior become accessible.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
// ═══════════════════════════════════════════// SCENARIO 1: Testing Exception Handling// ═══════════════════════════════════════════it('should handle payment gateway timeout gracefully', async () => { // Without DI: How do you make Stripe timeout? Disconnect network mid-request? // With DI: Simply configure the mock mockPayment.charge.mockRejectedValue(new TimeoutError('Gateway timeout')); mockInventory.checkStock.mockResolvedValue(true); const result = await orderService.processOrder(testOrder); expect(result.success).toBe(false); expect(result.reason).toContain('timeout');}); it('should handle inventory service returning malformed data', async () => { // Mock returns unexpected structure mockInventory.checkStock.mockResolvedValue(undefined as any); // Verify the service handles this gracefully const result = await orderService.processOrder(testOrder); expect(result.success).toBe(false); // No crash, graceful degradation}); // ═══════════════════════════════════════════// SCENARIO 2: Testing Specific Business Logic Branches// ═══════════════════════════════════════════it('should apply premium discount for loyalty members', async () => { // Create a loyalty-aware mock const mockPricing: IpPricingService = { calculateDiscount: (customer) => customer.loyaltyTier === 'platinum' ? 0.15 : 0 }; const service = new CheckoutService(mockPayment, mockPricing); const premiumCustomer = { ...testCustomer, loyaltyTier: 'platinum' }; const result = await service.checkout(cart, premiumCustomer); expect(mockPayment.charge).toHaveBeenCalledWith( expect.objectContaining({ discount: 0.15 }) );}); // ═══════════════════════════════════════════// SCENARIO 3: Testing Compensating Transactions// ═══════════════════════════════════════════it('should refund payment if shipping fails', async () => { mockInventory.checkStock.mockResolvedValue(true); mockPayment.charge.mockResolvedValue({ success: true, transactionId: 'tx_999' }); mockShipping.scheduleDelivery.mockRejectedValue(new Error('Shipping unavailable')); mockPayment.refund.mockResolvedValue({ success: true }); const result = await orderService.processOrder(testOrder); // Verify compensating transaction occurred expect(mockPayment.refund).toHaveBeenCalledWith('tx_999', testOrder.total); expect(result.success).toBe(false); expect(result.reason).toContain('shipping');}); // ═══════════════════════════════════════════// SCENARIO 4: Testing Temporal Behavior// ═══════════════════════════════════════════it('should expire reservation after 15 minutes', async () => { // Inject a controllable clock const mockClock: IClock = { now: jest.fn() }; // Start: create reservation mockClock.now.mockReturnValue(new Date('2024-01-01T10:00:00Z')); const reservation = await reservationService.create(order); // 10 minutes later: still valid mockClock.now.mockReturnValue(new Date('2024-01-01T10:10:00Z')); expect(await reservationService.isValid(reservation.id)).toBe(true); // 16 minutes later: expired mockClock.now.mockReturnValue(new Date('2024-01-01T10:16:00Z')); expect(await reservationService.isValid(reservation.id)).toBe(false);}); // ═══════════════════════════════════════════// SCENARIO 5: Testing Rate Limiting// ═══════════════════════════════════════════it('should reject requests after rate limit exceeded', async () => { // Configure mock to track call counts let callCount = 0; mockPayment.charge.mockImplementation(async () => { callCount++; if (callCount > 10) { throw new RateLimitError('Too many requests'); } return { success: true, transactionId: `tx_${callCount}` }; }); // Make 15 requests const results = await Promise.all( Array(15).fill(null).map(() => orderService.processOrder(testOrder)) ); // First 10 succeed expect(results.slice(0, 10).every(r => r.success)).toBe(true); // Last 5 fail gracefully expect(results.slice(10).every(r => !r.success)).toBe(true);});Without DI, many of these scenarios are untestable in automated tests. Teams resort to manual testing ('try pulling the network cable') or hope for the best. With DI, comprehensive automated coverage of failure modes is not only possible but straightforward.
Maintainability is the cost of changing software over time. High maintainability means changes are localized, predictable, and low-risk. Low maintainability means changes ripple unpredictably, requiring extensive modifications and introducing regressions.
DI promotes maintainability through several mechanisms:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
// ═══════════════════════════════════════════// SCENARIO 1: Upgrading a Dependency// ═══════════════════════════════════════════ // Original: Using legacy email serviceclass LegacyEmailService implements IEmailSender { send(to: string, message: string): Promise<void> { // Uses old SMTP library return legacySmtp.sendMail({ to, body: message }); }} // Change: Migrate to modern service// WITHOUT DI: Hunt through entire codebase for "new LegacyEmailService()"// WITH DI: Create new implementation, change one registration class ModernEmailService implements IEmailSender { constructor(private readonly apiKey: string) {} send(to: string, message: string): Promise<void> { // Uses modern REST API return fetch('https://api.email-service.com/send', { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}` }, body: JSON.stringify({ to, message }) }); }} // Changes required:// 1. Create ModernEmailService (new class)// 2. Update composition root (one location)// // NOT required:// - Touch OrderService// - Touch NotificationService // - Touch ReportGenerator// - Touch any consumer of IEmailSender // ═══════════════════════════════════════════// SCENARIO 2: Adding New Functionality// ═══════════════════════════════════════════ // Requirement: Add email templating// WITHOUT DI: Modify LegacyEmailService, risking regressions in all consumers// WITH DI: Extend via decorator or new implementation // Option A: Decorator extending existing serviceclass TemplatedEmailService implements IEmailSender { constructor( private readonly inner: IEmailSender, private readonly templateEngine: ITemplateEngine ) {} async send(to: string, message: string): Promise<void> { // If message looks like a template reference, expand it const expandedMessage = message.startsWith('template:') ? await this.templateEngine.render(message.substring(9)) : message; return this.inner.send(to, expandedMessage); }} // Composition: Wrap existing serviceconst emailer = new TemplatedEmailService( new ModernEmailService(config.emailApiKey), new HandlebarsTemplateEngine()); // Consumers are UNAWARE of the enhancement// They call send() exactly as before// Backward compatibility is automatic // ═══════════════════════════════════════════// SCENARIO 3: Feature Flagging New Behavior// ═══════════════════════════════════════════ // Requirement: Test new payment processor with 10% of usersclass FeatureFlaggedPaymentProcessor implements IPaymentProcessor { constructor( private readonly percentNew: number, private readonly oldProcessor: IPaymentProcessor, private readonly newProcessor: IPaymentProcessor, private readonly analytics: IAnalytics ) {} async charge(amount: Money, card: CardDetails): Promise<PaymentResult> { const useNew = Math.random() * 100 < this.percentNew; const processor = useNew ? this.newProcessor : this.oldProcessor; this.analytics.track('payment_processor_selection', { selected: useNew ? 'new' : 'old', amount: amount.cents }); return processor.charge(amount, card); }} // Composition: Create the A/B test setupconst paymentProcessor = new FeatureFlaggedPaymentProcessor( 10, // 10% get new processor new StripeProcessor(config.stripeKey), new AdyenProcessor(config.adyenKey), analyticsService); // CheckoutService injects the feature-flagged processor// It has NO KNOWLEDGE of the A/B test// Behavior change requires zero production code changesMaintainability improvements can be measured. While precise metrics vary by codebase, several indicators consistently improve with DI adoption.
| Metric | Before DI | After DI | Impact |
|---|---|---|---|
| Files touched per feature | 8-15 files | 2-4 files | ~70% reduction |
| Regression risk per change | High (coupling) | Low (isolation) | Fewer incidents |
| Time to understand class | Read implementation | Read constructor | ~5x faster |
| Integration test requirement | Every change | Composition changes only | Faster CI/CD |
| Onboarding time for feature | Days (trace dependencies) | Hours (read interfaces) | ~3x faster |
| Confidence in refactoring | Low (hidden connections) | High (explicit deps) | More refactoring |
The Compounding Effect:
Maintainability benefits compound over the lifespan of a codebase. In year one, DI might seem like overhead. By year three, the codebase without DI has accumulated enough coupling that every change is risky and slow. The DI codebase continues evolving at roughly constant velocity.
Change Velocity Over Time:
Without DI: ████████████████████████░░░░░░░░░░░░░░
Year 1 Year 2 Year 3
With DI: ████████████████████████████████████████
Year 1 Year 2 Year 3
This velocity difference is why experienced engineers insist on DI for anything expected to live beyond a few months.
DI is an investment with negative short-term ROI (added complexity) and massive long-term ROI (sustained maintainability). Make this investment for codebases with expected lifespans beyond a few months. Skip it for truly throwaway prototypes.
Flexibility, testability, and maintainability are not independent benefits—they reinforce each other in a virtuous cycle.
The Reinforcement Mechanisms:
Flexibility enables Testability — Because implementations are substitutable, tests can inject mocks. If you can swap Stripe for a mock, tests become fast and reliable.
Testability enables Maintainability — Because tests are comprehensive and fast, engineers refactor confidently. Changes are verified immediately, reducing regression risk.
Maintainability enables Flexibility — Because the codebase is well-structured with clear interfaces, adding new implementations is straightforward. New payment processors, new notification channels, new storage backends—all plug in cleanly.
Breaking the Cycle:
Conversely, losing any benefit endangers the others:
Experienced practitioners don't think of flexibility, testability, and maintainability as separate goals. They're three facets of the same gem: a well-designed system. DI is the technique that polishes this gem.
We've moved beyond abstract promises to concrete value:
Flexibility:
Testability:
Maintainability:
These benefits don't require faith—they're measurable in test speed, change frequency, regression rate, and developer productivity.
You now understand the concrete, practical benefits DI provides. In the next page, we'll explore DI scope and boundaries—when and where to apply DI, and what lies beyond its appropriate use.