Code Organization

Code structure patterns, module organization, and maintainable code architecture strategies.

Code Organization

This section covers comprehensive code organization strategies, module patterns, and architectural approaches that ensure maintainable, scalable, and readable codebases.

Code Architecture Philosophy

1. Clean Architecture Principles

The codebase follows clean architecture principles with clear separation of concerns:

┌─────────────────────────────────────────────────────────────┐
│                   Presentation Layer                        │
│  • React Components                                         │
│  • UI State Management                                      │
│  • User Interaction Handlers                               │
└─────────────────────┬───────────────────────────────────────┘
┌─────────────────────▼───────────────────────────────────────┐
│                  Application Layer                          │
│  • Use Cases / Business Logic                               │
│  • Application Services                                     │
│  • Data Transformation                                      │
└─────────────────────┬───────────────────────────────────────┘
┌─────────────────────▼───────────────────────────────────────┐
│                   Domain Layer                              │
│  • Entities and Value Objects                               │
│  • Domain Services                                          │
│  • Business Rules                                           │
└─────────────────────┬───────────────────────────────────────┘
┌─────────────────────▼───────────────────────────────────────┐
│                Infrastructure Layer                         │
│  • Database Access                                          │
│  • External APIs                                            │
│  • File System                                              │
└─────────────────────────────────────────────────────────────┘

2. Module Organization Strategy

lib/
├── auth/                        # Authentication module
│   ├── index.ts                 # Public API exports
│   ├── types.ts                 # Type definitions
│   ├── services/                # Business logic
│   │   ├── auth.service.ts
│   │   ├── token.service.ts
│   │   └── session.service.ts
│   ├── repositories/            # Data access
│   │   ├── user.repository.ts
│   │   └── session.repository.ts
│   ├── utils/                   # Utility functions
│   │   ├── validators.ts
│   │   ├── formatters.ts
│   │   └── helpers.ts
│   ├── hooks/                   # React hooks
│   │   ├── use-auth.ts
│   │   ├── use-session.ts
│   │   └── use-permissions.ts
│   └── constants.ts             # Module constants
├── products/                    # Product management module
│   ├── index.ts
│   ├── types.ts
│   ├── services/
│   │   ├── product.service.ts
│   │   ├── category.service.ts
│   │   └── pricing.service.ts
│   ├── repositories/
│   │   ├── product.repository.ts
│   │   └── category.repository.ts
│   ├── utils/
│   │   ├── sku-generator.ts
│   │   ├── price-calculator.ts
│   │   └── validators.ts
│   ├── hooks/
│   │   ├── use-products.ts
│   │   ├── use-product-form.ts
│   │   └── use-categories.ts
│   └── constants.ts
├── inventory/                   # Inventory management module
│   ├── index.ts
│   ├── types.ts
│   ├── services/
│   │   ├── inventory.service.ts
│   │   ├── movement.service.ts
│   │   └── adjustment.service.ts
│   ├── repositories/
│   │   ├── inventory.repository.ts
│   │   └── movement.repository.ts
│   ├── utils/
│   │   ├── stock-calculator.ts
│   │   ├── movement-tracker.ts
│   │   └── validators.ts
│   ├── hooks/
│   │   ├── use-inventory.ts
│   │   ├── use-movements.ts
│   │   └── use-adjustments.ts
│   └── constants.ts
├── shared/                      # Shared utilities
│   ├── api/                     # API utilities
│   │   ├── client.ts
│   │   ├── types.ts
│   │   └── error-handler.ts
│   ├── utils/                   # General utilities
│   │   ├── date.ts
│   │   ├── format.ts
│   │   ├── validation.ts
│   │   └── helpers.ts
│   ├── hooks/                   # Shared hooks
│   │   ├── use-debounce.ts
│   │   ├── use-local-storage.ts
│   │   └── use-async.ts
│   ├── types/                   # Global types
│   │   ├── api.ts
│   │   ├── common.ts
│   │   └── database.ts
│   └── constants/               # Global constants
│       ├── app.ts
│       ├── api.ts
│       └── ui.ts
└── config/                      # Configuration
    ├── database.ts
    ├── api.ts
    ├── env.ts
    └── app.ts

Service Layer Architecture

1. Service Pattern Implementation

// lib/products/services/product.service.ts
import { ProductRepository } from '../repositories/product.repository'
import { CategoryRepository } from '../repositories/category.repository'
import { InventoryService } from '../../inventory/services/inventory.service'
import { AuditService } from '../../shared/services/audit.service'
import type { Product, CreateProductData, UpdateProductData } from '../types'

export class ProductService {
  constructor(
    private productRepository: ProductRepository,
    private categoryRepository: CategoryRepository,
    private inventoryService: InventoryService,
    private auditService: AuditService
  ) {}

  async createProduct(data: CreateProductData): Promise<Product> {
    // Validate business rules
    await this.validateProductData(data)
    
    // Check SKU uniqueness
    const existingProduct = await this.productRepository.findBySku(data.sku)
    if (existingProduct) {
      throw new Error(`Product with SKU ${data.sku} already exists`)
    }
    
    // Create product
    const product = await this.productRepository.create(data)
    
    // Initialize inventory
    await this.inventoryService.initializeProduct(product.id, {
      initial_quantity: data.initial_quantity || 0
    })
    
    // Audit log
    await this.auditService.log('product_created', {
      product_id: product.id,
      sku: product.sku
    })
    
    return product
  }

  async updateProduct(id: string, data: UpdateProductData): Promise<Product> {
    const existingProduct = await this.productRepository.findById(id)
    if (!existingProduct) {
      throw new Error('Product not found')
    }
    
    // Validate business rules
    await this.validateProductData(data, id)
    
    // Check SKU uniqueness if changed
    if (data.sku && data.sku !== existingProduct.sku) {
      const existingSku = await this.productRepository.findBySku(data.sku)
      if (existingSku) {
        throw new Error(`Product with SKU ${data.sku} already exists`)
      }
    }
    
    // Update product
    const updatedProduct = await this.productRepository.update(id, data)
    
    // Handle price changes
    if (data.price && data.price !== existingProduct.price) {
      await this.handlePriceChange(id, existingProduct.price, data.price)
    }
    
    // Audit log
    await this.auditService.log('product_updated', {
      product_id: id,
      changes: this.getChanges(existingProduct, data)
    })
    
    return updatedProduct
  }

  async deleteProduct(id: string): Promise<void> {
    const product = await this.productRepository.findById(id)
    if (!product) {
      throw new Error('Product not found')
    }
    
    // Check if product has inventory
    const inventory = await this.inventoryService.getByProductId(id)
    if (inventory && inventory.quantity > 0) {
      throw new Error('Cannot delete product with existing inventory')
    }
    
    // Soft delete
    await this.productRepository.softDelete(id)
    
    // Audit log
    await this.auditService.log('product_deleted', {
      product_id: id,
      sku: product.sku
    })
  }

  async getProducts(filters: ProductFilters): Promise<PaginatedResult<Product>> {
    return this.productRepository.findMany(filters)
  }

  async getProductById(id: string): Promise<Product | null> {
    return this.productRepository.findById(id)
  }

  async searchProducts(query: string, filters?: ProductFilters): Promise<Product[]> {
    return this.productRepository.search(query, filters)
  }

  private async validateProductData(data: Partial<CreateProductData>, excludeId?: string): Promise<void> {
    // Validate category exists
    if (data.category_id) {
      const category = await this.categoryRepository.findById(data.category_id)
      if (!category) {
        throw new Error('Invalid category')
      }
    }
    
    // Validate price
    if (data.price !== undefined && data.price < 0) {
      throw new Error('Price must be non-negative')
    }
    
    // Validate cost vs price
    if (data.cost !== undefined && data.price !== undefined && data.cost > data.price) {
      throw new Error('Cost cannot be greater than price')
    }
  }

  private async handlePriceChange(productId: string, oldPrice: number, newPrice: number): Promise<void> {
    // Notify inventory service about price change
    await this.inventoryService.updateProductPrice(productId, newPrice)
    
    // Log price history
    await this.auditService.log('price_changed', {
      product_id: productId,
      old_price: oldPrice,
      new_price: newPrice
    })
  }

  private getChanges(original: Product, updates: UpdateProductData): Record<string, any> {
    const changes: Record<string, any> = {}
    
    Object.keys(updates).forEach(key => {
      const originalValue = original[key as keyof Product]
      const newValue = updates[key as keyof UpdateProductData]
      
      if (originalValue !== newValue) {
        changes[key] = { from: originalValue, to: newValue }
      }
    })
    
    return changes
  }
}

2. Repository Pattern Implementation

// lib/products/repositories/product.repository.ts
import { supabase } from '@/lib/supabase/client'
import type { Product, CreateProductData, UpdateProductData, ProductFilters } from '../types'

export class ProductRepository {
  private readonly table = 'products'

  async create(data: CreateProductData): Promise<Product> {
    const { data: product, error } = await supabase
      .from(this.table)
      .insert([data])
      .select()
      .single()

    if (error) {
      throw new Error(`Failed to create product: ${error.message}`)
    }

    return product
  }

  async findById(id: string): Promise<Product | null> {
    const { data: product, error } = await supabase
      .from(this.table)
      .select(`
        *,
        category:categories(id, name),
        inventory:inventory(quantity, allocated, available)
      `)
      .eq('id', id)
      .eq('deleted_at', null)
      .single()

    if (error) {
      if (error.code === 'PGRST116') return null // Not found
      throw new Error(`Failed to find product: ${error.message}`)
    }

    return product
  }

  async findBySku(sku: string): Promise<Product | null> {
    const { data: product, error } = await supabase
      .from(this.table)
      .select()
      .eq('sku', sku)
      .eq('deleted_at', null)
      .single()

    if (error) {
      if (error.code === 'PGRST116') return null
      throw new Error(`Failed to find product by SKU: ${error.message}`)
    }

    return product
  }

  async findMany(filters: ProductFilters): Promise<PaginatedResult<Product>> {
    let query = supabase
      .from(this.table)
      .select(`
        *,
        category:categories(id, name),
        inventory:inventory(quantity, allocated, available)
      `, { count: 'exact' })
      .eq('deleted_at', null)

    // Apply filters
    if (filters.category_id) {
      query = query.eq('category_id', filters.category_id)
    }

    if (filters.search) {
      query = query.or(`name.ilike.%${filters.search}%,sku.ilike.%${filters.search}%`)
    }

    if (filters.min_price !== undefined) {
      query = query.gte('price', filters.min_price)
    }

    if (filters.max_price !== undefined) {
      query = query.lte('price', filters.max_price)
    }

    if (filters.in_stock !== undefined) {
      if (filters.in_stock) {
        query = query.gt('inventory.quantity', 0)
      } else {
        query = query.eq('inventory.quantity', 0)
      }
    }

    // Apply sorting
    const sortField = filters.sort || 'created_at'
    const sortOrder = filters.order || 'desc'
    query = query.order(sortField, { ascending: sortOrder === 'asc' })

    // Apply pagination
    const page = filters.page || 1
    const limit = filters.limit || 20
    const offset = (page - 1) * limit
    query = query.range(offset, offset + limit - 1)

    const { data: products, error, count } = await query

    if (error) {
      throw new Error(`Failed to fetch products: ${error.message}`)
    }

    return {
      data: products || [],
      pagination: {
        page,
        limit,
        total: count || 0,
        totalPages: Math.ceil((count || 0) / limit),
        hasNext: offset + limit < (count || 0),
        hasPrev: page > 1
      }
    }
  }

  async update(id: string, data: UpdateProductData): Promise<Product> {
    const { data: product, error } = await supabase
      .from(this.table)
      .update({
        ...data,
        updated_at: new Date().toISOString()
      })
      .eq('id', id)
      .eq('deleted_at', null)
      .select()
      .single()

    if (error) {
      throw new Error(`Failed to update product: ${error.message}`)
    }

    return product
  }

  async softDelete(id: string): Promise<void> {
    const { error } = await supabase
      .from(this.table)
      .update({ deleted_at: new Date().toISOString() })
      .eq('id', id)

    if (error) {
      throw new Error(`Failed to delete product: ${error.message}`)
    }
  }

  async search(query: string, filters?: ProductFilters): Promise<Product[]> {
    let searchQuery = supabase
      .from(this.table)
      .select(`
        *,
        category:categories(id, name),
        inventory:inventory(quantity, allocated, available)
      `)
      .eq('deleted_at', null)
      .or(`name.ilike.%${query}%,sku.ilike.%${query}%,barcode.eq.${query}`)

    // Apply additional filters
    if (filters?.category_id) {
      searchQuery = searchQuery.eq('category_id', filters.category_id)
    }

    if (filters?.in_stock) {
      searchQuery = searchQuery.gt('inventory.quantity', 0)
    }

    searchQuery = searchQuery
      .order('name')
      .limit(50) // Limit search results

    const { data: products, error } = await searchQuery

    if (error) {
      throw new Error(`Failed to search products: ${error.message}`)
    }

    return products || []
  }
}

3. Custom Hooks Pattern

// lib/products/hooks/use-products.ts
import { useState, useEffect, useCallback } from 'react'
import { ProductService } from '../services/product.service'
import { useDebounce } from '@/lib/shared/hooks/use-debounce'
import type { Product, ProductFilters } from '../types'

interface UseProductsProps {
  initialFilters?: ProductFilters
  enabled?: boolean
}

export function useProducts({ initialFilters = {}, enabled = true }: UseProductsProps = {}) {
  const [products, setProducts] = useState<Product[]>([])
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<Error | null>(null)
  const [filters, setFilters] = useState<ProductFilters>(initialFilters)
  const [pagination, setPagination] = useState({
    page: 1,
    limit: 20,
    total: 0,
    totalPages: 0,
    hasNext: false,
    hasPrev: false
  })

  // Debounce search to avoid excessive API calls
  const debouncedFilters = useDebounce(filters, 300)

  const fetchProducts = useCallback(async (searchFilters: ProductFilters) => {
    if (!enabled) return

    try {
      setLoading(true)
      setError(null)

      const result = await ProductService.getProducts(searchFilters)
      setProducts(result.data)
      setPagination(result.pagination)
    } catch (err) {
      setError(err as Error)
      setProducts([])
    } finally {
      setLoading(false)
    }
  }, [enabled])

  // Fetch products when filters change
  useEffect(() => {
    fetchProducts(debouncedFilters)
  }, [debouncedFilters, fetchProducts])

  const updateFilters = useCallback((newFilters: Partial<ProductFilters>) => {
    setFilters(prev => ({
      ...prev,
      ...newFilters,
      page: newFilters.page !== undefined ? newFilters.page : 1 // Reset to page 1 for new filters
    }))
  }, [])

  const refetch = useCallback(() => {
    fetchProducts(filters)
  }, [fetchProducts, filters])

  const nextPage = useCallback(() => {
    if (pagination.hasNext) {
      updateFilters({ page: pagination.page + 1 })
    }
  }, [pagination.hasNext, pagination.page, updateFilters])

  const prevPage = useCallback(() => {
    if (pagination.hasPrev) {
      updateFilters({ page: pagination.page - 1 })
    }
  }, [pagination.hasPrev, pagination.page, updateFilters])

  const goToPage = useCallback((page: number) => {
    updateFilters({ page })
  }, [updateFilters])

  return {
    products,
    loading,
    error,
    filters,
    pagination,
    updateFilters,
    refetch,
    nextPage,
    prevPage,
    goToPage
  }
}

// Usage in component
export function ProductList() {
  const {
    products,
    loading,
    error,
    filters,
    pagination,
    updateFilters,
    nextPage,
    prevPage
  } = useProducts({
    initialFilters: { page: 1, limit: 20 }
  })

  if (loading) return <Spinner />
  if (error) return <ErrorMessage error={error} />

  return (
    <div>
      <ProductFilters 
        filters={filters} 
        onFiltersChange={updateFilters} 
      />
      
      <ProductGrid products={products} />
      
      <Pagination
        current={pagination.page}
        total={pagination.totalPages}
        onNext={nextPage}
        onPrev={prevPage}
        hasNext={pagination.hasNext}
        hasPrev={pagination.hasPrev}
      />
    </div>
  )
}

Error Handling Strategy

1. Error Classification

// lib/shared/errors/index.ts
export abstract class AppError extends Error {
  abstract readonly code: string
  abstract readonly statusCode: number
  
  constructor(message: string, public readonly context?: Record<string, any>) {
    super(message)
    this.name = this.constructor.name
  }
}

export class ValidationError extends AppError {
  readonly code = 'VALIDATION_ERROR'
  readonly statusCode = 400
  
  constructor(
    message: string,
    public readonly fieldErrors: Record<string, string[]> = {}
  ) {
    super(message)
  }
}

export class NotFoundError extends AppError {
  readonly code = 'NOT_FOUND'
  readonly statusCode = 404
}

export class UnauthorizedError extends AppError {
  readonly code = 'UNAUTHORIZED'
  readonly statusCode = 401
}

export class ForbiddenError extends AppError {
  readonly code = 'FORBIDDEN'
  readonly statusCode = 403
}

export class ConflictError extends AppError {
  readonly code = 'CONFLICT'
  readonly statusCode = 409
}

export class InternalServerError extends AppError {
  readonly code = 'INTERNAL_SERVER_ERROR'
  readonly statusCode = 500
}

// Error handling utilities
export function isAppError(error: unknown): error is AppError {
  return error instanceof AppError
}

export function getErrorMessage(error: unknown): string {
  if (isAppError(error)) {
    return error.message
  }
  
  if (error instanceof Error) {
    return error.message
  }
  
  return 'An unexpected error occurred'
}

export function getErrorCode(error: unknown): string {
  if (isAppError(error)) {
    return error.code
  }
  
  return 'UNKNOWN_ERROR'
}

2. Global Error Handler

// lib/shared/utils/error-handler.ts
import { isAppError, InternalServerError } from '../errors'
import { logger } from './logger'

export function handleServiceError(error: unknown, context?: Record<string, any>): never {
  // Log error for debugging
  logger.error('Service error occurred', {
    error: error instanceof Error ? error.message : String(error),
    stack: error instanceof Error ? error.stack : undefined,
    context
  })
  
  // Re-throw app errors as-is
  if (isAppError(error)) {
    throw error
  }
  
  // Handle specific error types
  if (error instanceof Error) {
    // Database constraint violations
    if (error.message.includes('unique constraint')) {
      throw new ConflictError('Resource already exists')
    }
    
    // Foreign key violations
    if (error.message.includes('foreign key constraint')) {
      throw new ValidationError('Invalid reference to related resource')
    }
  }
  
  // Default to internal server error
  throw new InternalServerError('An unexpected error occurred')
}

Best Practices

1. Module Design

  • Single Responsibility: Each module has one clear purpose
  • Dependency Injection: Use constructor injection for testability
  • Interface Segregation: Create focused interfaces
  • Barrel Exports: Use index files for clean imports

2. Service Layer

  • Stateless Services: Services should not maintain state
  • Transaction Management: Handle database transactions properly
  • Error Propagation: Let errors bubble up with context
  • Logging: Log important business events

3. Repository Pattern

  • Data Access Abstraction: Hide database implementation details
  • Query Optimization: Use efficient database queries
  • Error Handling: Provide meaningful error messages
  • Type Safety: Use TypeScript for all data operations

4. Custom Hooks

  • Single Concern: Each hook should have one responsibility
  • Memoization: Use useMemo and useCallback appropriately
  • Cleanup: Handle cleanup in useEffect
  • Testing: Make hooks testable in isolation

This code organization strategy provides a solid foundation for building maintainable, scalable applications with clear separation of concerns and consistent patterns throughout the codebase.