- 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>
11 KiB
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
- LnbitsAPI - Foundation API layer, needed by other services
- InvoiceService - High impact business logic
- PaymentMonitorService - Complete partial migration
- NostrmarketService - Module-specific service
Validation Steps
For each migrated service:
- ✅ Service registered in appropriate module
- ✅ SERVICE_TOKEN added to di-container.ts
- ✅ All consumers updated to use injectService()
- ✅ Exported singleton removed from service file
- ✅ Service extends BaseService if applicable
- ✅ Dependencies declared in metadata
- ✅ No build errors or TypeScript warnings
- ✅ 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:
- Unit Tests: Test service logic with mocked dependencies
- Integration Tests: Test service with real dependencies via DI
- 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
- Service Injection:
injectService(SERVICE_TOKENS.INVOICE_SERVICE)provides the service - Type Safety: TypeScript knows the exact service type with
as InvoiceService - Testability: Easy to mock
INVOICE_SERVICEfor testing - Modularity: Market module depends on base module's InvoiceService
- 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