-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Problem
ProductService is the largest service class in the codebase at 1662 lines, violating the Single Responsibility Principle by handling multiple concerns:
- CRUD operations (create, read, update, delete)
- Search and filtering with complex query building
- Inventory management and stock calculations
- Validation and business rule enforcement
- Slug generation and uniqueness checks
- Image handling and normalization
- Variant management
- Price and discount calculations
- Multi-tenant data isolation
This creates significant maintenance challenges:
- Hard to navigate: Developers must scroll through 1600+ lines to find relevant methods
- Difficult to test: Testing requires understanding the entire class context
- Merge conflicts: Multiple developers editing the same file
- Cognitive overload: Understanding all responsibilities at once
- Tight coupling: Changes to one concern affect others
Current Structure
// src/lib/services/product.service.ts (1662 lines)
export class ProductService {
// Lines 1-175: Types, interfaces, validation schemas
// Lines 176-430: Read operations (getProducts, getById, search, featured, etc.)
// Lines 431-600: Write operations (create, update, delete)
// Lines 601-800: Variant management
// Lines 801-1000: Helper methods (slug generation, validation, normalization)
// Lines 1001-1200: Complex query building (buildWhereClause, buildOrderByClause)
// Lines 1201-1400: Inventory and stock calculations
// Lines 1401-1662: Price calculations, discount logic, image handling
}
````
**Method count**: 40+ public and private methods in a single class
## Proposed Refactoring
### Phase 1: Extract Domain-Specific Services
Decompose `ProductService` into focused, cohesive modules:
````
src/lib/services/product/
├── index.ts # Re-exports all services
├── product.service.ts # Core CRUD (200-300 lines)
├── product-query.service.ts # Search, filtering, queries (200-300 lines)
├── product-validation.service.ts # Business rules, validation (150-200 lines)
├── product-inventory.service.ts # Stock management (150-200 lines)
├── variant.service.ts # Variant operations (200-250 lines)
└── types.ts # Shared types and interfacesProposed Architecture
1. Core Product Service (CRUD only)
// src/lib/services/product/product.service.ts (~250 lines)
export class ProductService {
constructor(
private queryService: ProductQueryService,
private validationService: ProductValidationService,
private inventoryService: ProductInventoryService
) {}
async getProductById(productId: string, storeId: string) {
return this.queryService.findById(productId, storeId);
}
async getProducts(storeId: string, filters: ProductSearchFilters, page: number, perPage: number) {
return this.queryService.findMany(storeId, filters, page, perPage);
}
async createProduct(storeId: string, data: CreateProductData) {
// Validate
await this.validationService.validateCreate(storeId, data);
// Calculate inventory status
const inventoryStatus = this.inventoryService.calculateStatus(
data.inventoryQty,
data.lowStockThreshold
);
// Create product
const product = await prisma.product.create({
data: {
...data,
inventoryStatus,
storeId,
},
});
return product;
}
// Similar methods for update, delete
}2. Query Service (Search & Filtering)
// src/lib/services/product/product-query.service.ts (~250 lines)
export class ProductQueryService {
async findById(productId: string, storeId: string): Promise(ProductWithRelations | null) {
return prisma.product.findFirst({
where: { id: productId, storeId, deletedAt: null },
include: this.buildInclude(),
});
}
async findMany(
storeId: string,
filters: ProductSearchFilters,
page: number,
perPage: number
): Promise(ProductListResult) {
const where = this.buildWhereClause(storeId, filters);
const orderBy = this.buildOrderByClause(filters.sortBy, filters.sortOrder);
const [products, total] = await Promise.all([
prisma.product.findMany({ where, orderBy, take: perPage, skip: (page - 1) * perPage }),
prisma.product.count({ where }),
]);
return {
products: products.map(this.normalizeProduct),
pagination: { page, perPage, total, totalPages: Math.ceil(total / perPage) },
};
}
async searchProducts(storeId: string, query: string): Promise(Product[]) {
// Full-text search implementation
}
private buildWhereClause(storeId: string, filters: ProductSearchFilters) {
// Complex filtering logic (currently 100+ lines)
}
private buildOrderByClause(sortBy?: string, sortOrder?: string) {
// Sorting logic
}
private buildInclude() {
// Relation inclusion logic
}
private normalizeProduct(product: Product): ProductWithRelations {
// Field normalization
}
}3. Validation Service (Business Rules)
// src/lib/services/product/product-validation.service.ts (~180 lines)
export class ProductValidationService {
async validateCreate(storeId: string, data: CreateProductData): Promise(void) {
// Validate with Zod
createProductSchema.parse(data);
// Business rules
await this.validateUniqueSku(storeId, data.sku);
await this.validateUniqueSlug(storeId, data.slug);
await this.validateCategoryExists(data.categoryId);
await this.validateBrandExists(data.brandId);
await this.validatePriceLogic(data);
}
async validateUpdate(storeId: string, productId: string, data: UpdateProductData): Promise(void) {
// Similar validation for updates
}
async generateUniqueSlug(storeId: string, name: string): Promise(string) {
let slug = this.slugify(name);
let attempt = 0;
while (await this.slugExists(storeId, slug)) {
attempt++;
slug = `\$\{this.slugify(name)}-\$\{attempt}`;
}
return slug;
}
private async validateUniqueSku(storeId: string, sku: string): Promise(void) {
// SKU uniqueness check
}
private async validateUniqueSlug(storeId: string, slug: string): Promise(void) {
// Slug uniqueness check
}
private slugify(text: string): string {
// Slug generation logic
}
}4. Inventory Service (Stock Management)
// src/lib/services/product/product-inventory.service.ts (~180 lines)
export class ProductInventoryService {
calculateStatus(qty: number, threshold: number): InventoryStatus {
if (qty === 0) return InventoryStatus.OUT_OF_STOCK;
if (qty <= threshold) return InventoryStatus.LOW_STOCK;
return InventoryStatus.IN_STOCK;
}
async updateStock(productId: string, storeId: string, quantity: number): Promise(Product) {
const product = await prisma.product.findFirst({
where: { id: productId, storeId },
});
if (!product) {
throw new Error('Product not found');
}
const newQty = product.inventoryQty + quantity;
const newStatus = this.calculateStatus(newQty, product.lowStockThreshold);
return prisma.product.update({
where: { id: productId },
data: {
inventoryQty: newQty,
inventoryStatus: newStatus,
},
});
}
async getLowStockProducts(storeId: string, limit: number = 10): Promise(Product[]) {
// Query low-stock products
}
async reserveStock(productId: string, quantity: number): Promise(void) {
// Stock reservation for orders
}
async releaseStock(productId: string, quantity: number): Promise(void) {
// Stock release on order cancellation
}
}5. Variant Service (Variant Management)
// src/lib/services/product/variant.service.ts (~200 lines)
export class VariantService {
async createVariant(productId: string, data: VariantData): Promise(ProductVariant) {
// Variant creation logic
}
async updateVariant(variantId: string, data: Partial(VariantData)): Promise(ProductVariant) {
// Variant update logic
}
async deleteVariant(variantId: string): Promise(void) {
// Variant deletion logic
}
async syncVariants(productId: string, variants: VariantData[]): Promise(ProductVariant[]) {
// Bulk variant sync (currently 100+ lines in main service)
}
}6. Index File (Public API)
// src/lib/services/product/index.ts
export * from './product.service';
export * from './product-query.service';
export * from './product-validation.service';
export * from './product-inventory.service';
export * from './variant.service';
export * from './types';
// Convenience: Create pre-configured instance
export const productService = new ProductService(
new ProductQueryService(),
new ProductValidationService(),
new ProductInventoryService()
);Migration Strategy
Backward Compatibility:
// src/lib/services/product.service.ts (legacy file - becomes thin wrapper)
// Keep this file for backward compatibility during migration
export { productService } from './product';
export type { ProductWithRelations, ProductSearchFilters } from './product';Existing code continues to work:
import { productService } from '@/lib/services/product.service';
// Still works! Internally delegates to new architectureBenefits
- ✅ Improved maintainability: Each service is 150-300 lines (vs 1662)
- ✅ Better testability: Test each concern independently
- ✅ Reduced coupling: Clear separation of responsibilities
- ✅ Easier navigation: Find relevant code quickly
- ✅ Parallel development: Multiple devs can work on different services
- ✅ Reusability: Use inventory service independently for other features
- ✅ Clearer intent: Service names reveal their purpose
- ✅ Scalability: Easy to add new product-related features
Impact Assessment
-
Effort: High - Estimated 10-12 hours
- 2 hours: Create new directory structure and types.ts
- 3 hours: Extract ProductQueryService (complex query building)
- 2 hours: Extract ProductValidationService
- 2 hours: Extract ProductInventoryService
- 2 hours: Extract VariantService
- 1 hour: Create index.ts and backward-compatible wrapper
- 2 hours: Update tests and verify all functionality works
-
Risk: Medium
- Large refactoring affecting core business logic
- Requires careful extraction to preserve behavior
- Need thorough testing of all product operations
- Can be done incrementally (extract one service at a time)
-
Benefit: Very High
- Dramatically improves code organization
- Makes future product feature development much easier
- Reduces cognitive load for developers
- Sets pattern for refactoring other large services (OrderService: 858 lines, InventoryService: 1347 lines)
-
Priority: Medium (High value but requires more effort)
Suggested Implementation Order
- Create directory structure:
src/lib/services/product/with types.ts - Extract ValidationService first (least dependencies)
- Extract InventoryService (clear boundary)
- Extract VariantService (self-contained)
- Extract QueryService (most complex, do last)
- Refactor core ProductService to use extracted services
- Create backward-compatible wrapper in old location
- Update tests to verify behavior unchanged
- Gradually update imports across codebase (optional, wrapper maintains compatibility)
Related Files
- Current:
src/lib/services/product.service.ts(1662 lines - refactor this) - New:
src/lib/services/product/index.ts(exports) - New:
src/lib/services/product/product.service.ts(~250 lines) - New:
src/lib/services/product/product-query.service.ts(~250 lines) - New:
src/lib/services/product/product-validation.service.ts(~180 lines) - New:
src/lib/services/product/product-inventory.service.ts(~180 lines) - New:
src/lib/services/product/variant.service.ts(~200 lines) - New:
src/lib/services/product/types.ts(~100 lines)
Files that import ProductService (update optional with backward compat):
- Multiple API routes in
src/app/api/ - Admin components in
src/components/ - Dashboard pages
Testing Strategy
- Extract existing tests: Copy tests for each extracted service
- Unit tests per service: Test each service independently
- ProductQueryService: Test filtering, sorting, search
- ProductValidationService: Test business rules, slug generation
- ProductInventoryService: Test stock calculations, status updates
- VariantService: Test variant CRUD, sync operations
- Integration tests: Test ProductService with all dependencies
- Regression testing: Run full product test suite
- Manual testing:
- Product creation/update in admin
- Storefront product display
- Inventory management
- Variant management
Success Criteria
- ProductService class is under 300 lines
- Each extracted service is under 300 lines
- All services have clear, single responsibilities
- Backward compatibility maintained via wrapper
- All existing tests pass
- No TypeScript errors
- No functional regressions
- Documentation updated with new architecture
- Code review approved by team
Future Opportunities
After completing this refactoring, apply the same pattern to:
- OrderService (858 lines) → Order CRUD, Order Processing, Order Fulfillment
- InventoryService (1347 lines) → Inventory Tracking, Stock Alerts, Inventory Sync
- IntegrationServices (Facebook: 1454 lines) → Catalog Sync, Event Tracking, Order Sync
AI generated by Daily Codebase Analyzer - Semantic Function Extraction & Refactoring
- expires on Mar 4, 2026, 2:05 PM UTC
Metadata
Metadata
Assignees
Type
Projects
Status