web-app/docs/01-architecture/modular-design.md
padreug a373fa714d Update modular design documentation and workspace configuration
- Change the active file in workspace.json to point to the new modular-design.md.
- Revise modular-design.md to enhance clarity and structure, including a new table of contents and updated sections on design philosophy, architecture components, and service abstractions.
- Remove outdated content and improve the overall presentation of modular design patterns and best practices.

These updates aim to streamline the documentation for better accessibility and understanding of the modular architecture.
2025-09-08 12:03:28 +02:00

12 KiB

🔧 Modular Design Patterns

Plugin-based architecture enabling scalable, maintainable, and extensible feature development with dependency injection, service abstractions, and clean modular boundaries.

Table of Contents

Design Philosophy

Separation of Concerns

Each module owns its complete domain logic, from data models to UI components, ensuring clear boundaries and reducing coupling between features.

Dependency Injection

Services communicate through well-defined interfaces using a centralized DI container, enabling loose coupling and testability.

Plugin-Based Development

Features are implemented as self-contained modules that can be independently developed, tested, and deployed.

Reactive Architecture

Services integrate Vue's reactivity system to provide automatic UI updates and consistent state management.


Architecture Components

Plugin Manager

Orchestrates module loading, dependency resolution, and lifecycle management:

class PluginManager {
  async loadModules(modules: ModulePlugin[]): Promise<void>
  getDependencyGraph(): Map<string, string[]>
  validateDependencies(modules: ModulePlugin[]): void
}

Dependency Injection Container

Manages service registration, injection, and lifecycle:

// Service registration
container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub)

// Service consumption
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)

BaseService Architecture

Abstract foundation providing common patterns for all services:

abstract class BaseService {
  protected readonly metadata: ServiceMetadata
  protected dependencies = new Map<string, any>()
  
  abstract initialize(): Promise<void>
  abstract dispose(): Promise<void>
}

Module Plugin Interface

Standardized contract for all feature modules:

interface ModulePlugin {
  name: string
  version: string
  dependencies: string[]
  
  install(app: App, options?: ModuleConfig): Promise<void>
  routes?: RouteRecordRaw[]
  components?: Record<string, Component>
}

Service Abstractions

Core Infrastructure Services

AuthService - User Identity Management

  • Purpose: Centralized authentication with Nostr key handling
  • Features: Session management, profile updates, secure key storage
  • Pattern: BaseService with reactive state management

RelayHub - Nostr Connection Management

  • Purpose: Centralized relay connections and event processing
  • Features: Connection pooling, automatic reconnection, event deduplication
  • Pattern: Service singleton with connection state management

InvoiceService - Lightning Payment Processing

  • Purpose: Unified Lightning Network integration
  • Features: Invoice creation, payment monitoring, QR code generation
  • Pattern: BaseService with LNBits API integration

ToastService - User Notifications

  • Purpose: Consistent notification system across modules
  • Features: Context-specific messages, accessibility support
  • Pattern: Service with method categorization by context

StorageService - User-Scoped Data Persistence

  • Purpose: Secure, user-isolated local storage operations
  • Features: Encryption, type safety, reactive storage
  • Pattern: Service with automatic user prefixing

Shared Composables

useAuth() - Authentication Access

const auth = useAuth()
const { isAuthenticated, currentUser, login, logout } = auth

useToast() - Notification Access

const toast = useToast()
toast.success('Operation completed!')
toast.error('Operation failed')

useStorage() - Storage Access

const storage = useStorage()
const userData = storage.getUserData('preferences', defaultPrefs)

Module Development

Module Structure Pattern

Each module follows a consistent directory structure:

src/modules/[module-name]/
├── index.ts                 # Module plugin definition
├── components/              # Module-specific UI components
├── composables/             # Module composables and hooks
├── services/               # Business logic services
├── stores/                 # Module-specific Pinia stores
├── types/                  # TypeScript type definitions
└── views/                  # Page components and routes

Service Development Pattern

Services extend BaseService for consistent lifecycle management:

export class MyService extends BaseService {
  protected readonly metadata = {
    name: 'MyService',
    dependencies: ['AuthService', 'RelayHub']
  }
  
  constructor() {
    super()
  }
  
  async initialize(): Promise<void> {
    await super.initialize() // Initialize dependencies
    // Service-specific initialization
    this.isInitialized.value = true
  }
  
  async dispose(): Promise<void> {
    // Cleanup logic
    this.isDisposed.value = true
  }
}

Module Registration Pattern

Standardized module plugin structure:

export const myModule: ModulePlugin = {
  name: 'my-module',
  version: '1.0.0',
  dependencies: ['base'],
  
  async install(app: App, options?: MyModuleConfig) {
    // 1. Create and register services
    const myService = new MyService()
    container.provide(SERVICE_TOKENS.MY_SERVICE, myService)
    
    // 2. Register global components
    app.component('MyGlobalComponent', MyGlobalComponent)
    
    // 3. Initialize services
    await myService.initialize()
  },
  
  routes: [
    {
      path: '/my-feature',
      component: () => import('./views/MyFeatureView.vue')
    }
  ]
}

Dependency Declaration

Services declare dependencies through metadata:

protected readonly metadata = {
  name: 'ChatService',
  dependencies: ['AuthService', 'RelayHub', 'StorageService']
}

// Access injected dependencies
private get authService(): AuthService {
  return this.dependencies.get('AuthService') as AuthService
}

Implementation Patterns

Cross-Module Communication

Service Dependencies

For required functionality between modules:

// ✅ Correct: Use dependency injection
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)

// ❌ Wrong: Direct import breaks modularity
import { relayHub } from '../base/services/relay-hub'

Event Bus Communication

For optional cross-module notifications:

// Publishing events
eventBus.emit('user:authenticated', { userId: user.pubkey })
eventBus.emit('payment:received', { amount: 1000, invoiceId: 'abc123' })

// Subscribing to events
eventBus.on('chat:message-received', (message) => {
  // Handle cross-module events
})

Shared Components

Modules can export components for reuse:

// In module plugin definition
export const chatModule: ModulePlugin = {
  components: {
    'ChatAvatar': () => import('./components/ChatAvatar.vue'),
    'MessageBubble': () => import('./components/MessageBubble.vue')
  }
}

// Usage in other modules
<ChatAvatar :pubkey="user.pubkey" :size="32" />

State Management Patterns

Service-Based State

Core state managed by services:

// AuthService manages authentication state
export class AuthService extends BaseService {
  public isAuthenticated = ref(false)
  public user = ref<User | null>(null)
  
  // Computed properties for components
  public userDisplay = computed(() => this.user.value?.name || 'Anonymous')
}

Module-Specific Stores

Complex state handled by Pinia stores:

// Market module store
export const useMarketStore = defineStore('market', () => {
  const products = ref<Product[]>([])
  const cartItems = ref<CartItem[]>([])
  
  // Access shared services
  const auth = useAuth()
  const toast = useToast()
  
  return { products, cartItems }
})

Error Handling Patterns

Service-Level Error Handling

Centralized error handling in services:

export class MyService extends BaseService {
  protected handleError(error: Error, context: string) {
    console.error(`[${this.metadata.name}] ${context}:`, error)
    
    // Use toast service for user notifications
    const toast = injectService(SERVICE_TOKENS.TOAST_SERVICE)
    toast.error(`${context} failed: ${error.message}`)
  }
}

Component-Level Error Handling

Consistent error display in components:

<script setup lang="ts">
const { error, isLoading, execute } = useAsyncOperation()

const handleAction = () => execute(async () => {
  await myService.performAction()
})
</script>

<template>
  <div>
    <button @click="handleAction" :disabled="isLoading">
      {{ isLoading ? 'Loading...' : 'Perform Action' }}
    </button>
    <div v-if="error" class="error">{{ error }}</div>
  </div>
</template>

Benefits & Trade-offs

Achieved Benefits

Development Velocity

  • Consistent Patterns - Standardized development approaches across modules
  • Reusable Services - Shared infrastructure reduces implementation time
  • Type Safety - Dependency injection eliminates as any casting
  • Clear Boundaries - Module separation simplifies feature development

Maintainability

  • Single Source of Truth - Centralized services eliminate duplication
  • Dependency Visibility - Clear service relationships and requirements
  • Testability - Isolated modules and mockable dependencies
  • Extensibility - Easy to add new modules without affecting existing ones

User Experience

  • Consistent Interface - Unified patterns across all features
  • Reliable Performance - Optimized shared services
  • Progressive Loading - Lazy-loaded modules for faster initial load
  • Error Recovery - Consistent error handling and user feedback

Architectural Trade-offs

Complexity vs. Flexibility

  • Increased Initial Setup - More boilerplate for dependency injection
  • Enhanced Flexibility - Easy to swap implementations and add features
  • Learning Curve - Developers need to understand DI patterns
  • Long-term Benefits - Reduced complexity as application grows

Performance Considerations

  • Service Initialization - Dependency resolution overhead at startup
  • Memory Efficiency - Singleton services reduce memory usage
  • Bundle Optimization - Module-based code splitting improves loading
  • Runtime Performance - Optimized shared services benefit all modules

Best Practices Established

  1. Always use dependency injection for cross-module service access
  2. Extend BaseService for all business logic services
  3. Declare dependencies explicitly in service metadata
  4. Use reactive state for UI-affecting service properties
  5. Handle errors consistently through standardized patterns
  6. Test modules in isolation with mocked dependencies

See Also

Architecture Documentation

Development Guides


Tags: #architecture #modular-design #dependency-injection #plugin-system
Last Updated: 2025-09-07
Author: Development Team