web-app/docs/04-migrations/dependency-injection-migration.md
padreug 7a32085ee1 Migrate InvoiceService to dependency injection pattern
- Add INVOICE_SERVICE token to DI container
- Register InvoiceService in base module with proper lifecycle
- Update market store to use dependency injection instead of singleton
- Remove exported singleton from InvoiceService class
- Add comprehensive migration documentation with examples
- Maintain type safety with proper TypeScript interfaces

This migration eliminates the legacy singleton pattern and improves:
- Testability through service injection
- Modular architecture with clear boundaries
- Single source of truth for service instances
- Consistent dependency injection patterns

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-07 01:10:55 +02:00

11 KiB

Dependency Injection Migration Plan

Overview

This document outlines the migration approach for converting legacy exported singleton services to the dependency injection pattern, following the successful authentication architecture refactoring.

Legacy Pattern vs New Pattern

Legacy Pattern (To Be Replaced)

// Service definition with exported singleton
export class SomeService {
  // service implementation
}
export const someService = new SomeService()

// Consumer code
import { someService } from '@/services/someService'
someService.doSomething()

New Dependency Injection Pattern

// Service definition (no exported singleton)
export class SomeService extends BaseService {
  // service implementation
}

// Service registration in module
container.provide(SERVICE_TOKENS.SOME_SERVICE, someService)

// Consumer code
const someService = injectService(SERVICE_TOKENS.SOME_SERVICE) as SomeService
someService.doSomething()

Services to Migrate

Priority 1: High Impact Services

1. InvoiceService

  • File: src/core/services/invoiceService.ts
  • Current: export const invoiceService = new InvoiceService()
  • Token: Need to add INVOICE_SERVICE: Symbol('invoiceService')
  • Usage: Market store and components for payment processing
  • Impact: Core business logic, high usage
  • Dependencies: Likely uses LnbitsAPI and other services

2. NostrmarketService

  • File: src/modules/market/services/nostrmarketService.ts
  • Current: export const nostrmarketService = new NostrmarketService()
  • Token: Need to add NOSTRMARKET_SERVICE: Symbol('nostrmarketService')
  • Usage: Market module composables and components
  • Impact: Key module service, should follow DI pattern
  • Dependencies: Likely uses RelayHub and other Nostr services

Priority 2: Medium Impact Services

3. PaymentMonitorService

  • File: src/modules/market/services/paymentMonitor.ts
  • Current: export const paymentMonitor = new PaymentMonitorService()
  • Token: SERVICE_TOKENS.PAYMENT_MONITOR (already exists)
  • Status: Partially migrated - token exists but service not properly registered
  • Usage: Market store
  • Impact: Background service, less direct component usage

4. LnbitsAPI

  • File: src/lib/api/lnbits.ts
  • Current: export const lnbitsAPI = new LnbitsAPI()
  • Token: Need to add LNBITS_API: Symbol('lnbitsAPI')
  • Usage: AuthService and other core services
  • Impact: API layer foundation, widely used but lower-level

Migration Steps

Step 1: Add SERVICE_TOKENS

Update src/core/di-container.ts to add missing tokens:

export const SERVICE_TOKENS = {
  // ... existing tokens ...
  
  // New service tokens
  INVOICE_SERVICE: Symbol('invoiceService'),
  NOSTRMARKET_SERVICE: Symbol('nostrmarketService'),
  LNBITS_API: Symbol('lnbitsAPI'),
  
  // PAYMENT_MONITOR already exists
} as const

Step 2: Register Services in Modules

Base Module Registration

For core services like InvoiceService and LnbitsAPI:

// src/modules/base/index.ts
import { InvoiceService } from '@/core/services/invoiceService'
import { LnbitsAPI } from '@/lib/api/lnbits'

const invoiceService = new InvoiceService()
const lnbitsAPI = new LnbitsAPI()

container.provide(SERVICE_TOKENS.INVOICE_SERVICE, invoiceService)
container.provide(SERVICE_TOKENS.LNBITS_API, lnbitsAPI)

Market Module Registration

For market-specific services:

// src/modules/market/index.ts
import { NostrmarketService } from './services/nostrmarketService'
import { PaymentMonitorService } from './services/paymentMonitor'

const nostrmarketService = new NostrmarketService()
const paymentMonitor = new PaymentMonitorService()

container.provide(SERVICE_TOKENS.NOSTRMARKET_SERVICE, nostrmarketService)
container.provide(SERVICE_TOKENS.PAYMENT_MONITOR, paymentMonitor)

Step 3: Update Service Classes

Convert services to extend BaseService for dependency injection:

// Before
export class SomeService {
  constructor() {
    // direct imports/dependencies
  }
}

// After
export class SomeService extends BaseService {
  protected readonly metadata = {
    name: 'SomeService',
    dependencies: ['AuthService', 'RelayHub'] // declare dependencies
  }
  
  // Access dependencies through DI
  private get authService(): AuthService {
    return this.dependencies.get('AuthService') as AuthService
  }
}

Step 4: Update Consumer Code

Replace direct imports with dependency injection:

// Before
import { invoiceService } from '@/core/services/invoiceService'

// After
import { injectService, SERVICE_TOKENS } from '@/core/di-container'

const invoiceService = injectService(SERVICE_TOKENS.INVOICE_SERVICE) as InvoiceService

Step 5: Remove Exported Singletons

After migration, remove the exported singleton from service files:

// Remove this line
export const someService = new SomeService()

Migration Order

  1. LnbitsAPI - Foundation API layer, needed by other services
  2. InvoiceService - High impact business logic
  3. PaymentMonitorService - Complete partial migration
  4. NostrmarketService - Module-specific service

Validation Steps

For each migrated service:

  1. Service registered in appropriate module
  2. SERVICE_TOKEN added to di-container.ts
  3. All consumers updated to use injectService()
  4. Exported singleton removed from service file
  5. Service extends BaseService if applicable
  6. Dependencies declared in metadata
  7. No build errors or TypeScript warnings
  8. Functionality testing - service works as expected

Benefits of Migration

Architecture Benefits

  • Single Source of Truth: Services managed by DI container
  • Loose Coupling: Components depend on interfaces, not concrete classes
  • Testability: Easy to inject mock services for testing
  • Module Boundaries: Clear service ownership and lifecycle

Maintainability Benefits

  • Centralized Registration: All services registered in module definitions
  • Consistent Patterns: Same DI pattern across all services
  • Service Lifecycle: Container manages service instantiation and disposal
  • Dependency Visibility: Service dependencies clearly declared

Performance Benefits

  • No Duplicate Instances: Container ensures singleton behavior
  • Lazy Loading: Services only created when needed
  • Efficient Memory Usage: Single instances shared across modules

Testing Strategy

Each migrated service should include:

  1. Unit Tests: Test service logic with mocked dependencies
  2. Integration Tests: Test service with real dependencies via DI
  3. Mock Service Creation: For testing consumers

Example mock setup:

// Test setup
const mockInvoiceService = {
  createInvoice: vi.fn(),
  checkPayment: vi.fn()
}

container.provide(SERVICE_TOKENS.INVOICE_SERVICE, mockInvoiceService)

Risk Mitigation

  • Incremental Migration: One service at a time
  • Backward Compatibility: Keep old imports temporarily during migration
  • Comprehensive Testing: Validate each service after migration
  • Rollback Plan: Git commits allow easy rollback if issues arise

Migration Status

Completed Migrations

1. InvoiceService (Completed)

  • Status: Successfully migrated
  • Token: SERVICE_TOKENS.INVOICE_SERVICE
  • Registration: Base module (src/modules/base/index.ts)
  • Usage: Market store (src/modules/market/stores/market.ts)

Before:

// ❌ Legacy singleton pattern
import { invoiceService } from '@/core/services/invoiceService'
const invoice = await invoiceService.createInvoice(order, adminKey)

After:

// ✅ Dependency injection pattern
const invoiceService = injectService(SERVICE_TOKENS.INVOICE_SERVICE) as InvoiceService
const invoice = await invoiceService.createInvoice(order, adminKey)

🔄 In Progress

2. NostrmarketService (Pending)

  • Status: 🔄 Ready for migration
  • File: src/modules/market/services/nostrmarketService.ts
  • Token: Need to add NOSTRMARKET_SERVICE
  • Registration: Market module

3. PaymentMonitorService (Partially Migrated)

  • Status: 🔄 Token exists, needs completion
  • Token: SERVICE_TOKENS.PAYMENT_MONITOR (already exists)
  • Registration: Market module

4. LnbitsAPI (Pending)

  • Status: 🔄 Ready for migration
  • File: src/lib/api/lnbits.ts
  • Token: Need to add LNBITS_API
  • Registration: Base module

Practical Example: Market Module Using InvoiceService

Here's a real-world example of how the Market module now uses the migrated InvoiceService through dependency injection:

Market Store Implementation

// src/modules/market/stores/market.ts
import { defineStore } from 'pinia'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import type { InvoiceService } from '@/core/services/invoiceService'

export const useMarketStore = defineStore('market', () => {
  // Inject InvoiceService via dependency injection
  const invoiceService = injectService(SERVICE_TOKENS.INVOICE_SERVICE) as InvoiceService
  
  const createOrderInvoice = async (order: Order, adminKey: string) => {
    try {
      // Use injected service to create Lightning invoice
      const invoice = await invoiceService.createInvoice(order, adminKey, {
        tag: "nostrmarket",
        order_id: order.id,
        merchant_pubkey: order.sellerPubkey,
        expiry: 3600
      })
      
      console.log('Invoice created:', {
        paymentHash: invoice.payment_hash,
        bolt11: invoice.bolt11,
        amount: invoice.amount
      })
      
      return invoice
    } catch (error) {
      console.error('Failed to create order invoice:', error)
      throw error
    }
  }
  
  return {
    createOrderInvoice,
    // ... other store methods
  }
})

Benefits Demonstrated

  1. Service Injection: injectService(SERVICE_TOKENS.INVOICE_SERVICE) provides the service
  2. Type Safety: TypeScript knows the exact service type with as InvoiceService
  3. Testability: Easy to mock INVOICE_SERVICE for testing
  4. Modularity: Market module depends on base module's InvoiceService
  5. Single Source: One InvoiceService instance shared across the app

Service Registration (Base Module)

// src/modules/base/index.ts
import { InvoiceService } from '@/core/services/invoiceService'

const invoiceService = new InvoiceService()

export const baseModule: ModulePlugin = {
  async install(_app: App) {
    // Register InvoiceService in DI container
    container.provide(SERVICE_TOKENS.INVOICE_SERVICE, invoiceService)
    
    // Other service registrations...
  }
}

Success Metrics

Completed Services

  • InvoiceService: Successfully migrated to DI pattern
  • AuthService: Previously migrated to DI pattern

Remaining Tasks

  • 🔄 NostrmarketService: Needs migration
  • 🔄 PaymentMonitorService: Complete partial migration
  • 🔄 LnbitsAPI: Needs migration

Overall Progress

  • 2/5 services migrated to dependency injection
  • Zero build errors
  • Consistent DI patterns established
  • Documentation updated with examples