Skip to content

DDD Refactoring Strategy for PRS Backend

This document outlines a comprehensive strategy for refactoring the PRS Backend using Domain-Driven Design (DDD) principles. The goal is to improve code organization, maintainability, and debuggability while preserving existing functionality.

Current vs. Proposed Architecture

graph TD
    subgraph "Current Architecture"
        A1[Controllers] --> B1[Business Logic]
        A1 --> C1[Data Access]
        A1 --> D1[Error Handling]
        A1 --> E1[Response Formatting]
    end

    subgraph "DDD Architecture"
        A2[Controllers] --> B2[Application Services]
        B2 --> C2[Domain Services]
        B2 --> D2[Repositories]
        C2 --> E2[Domain Entities]
        E2 --> F2[Business Logic]
        E2 --> G2[Value Objects]
        A2 --> H2[Response Formatters]
        B2 --> I2[Error Handlers]
    end

In the current architecture, controllers handle multiple responsibilities including business logic, data access, error handling, and response formatting. This leads to large, complex controllers that are difficult to maintain and test.

The proposed DDD architecture separates these concerns: - Controllers handle HTTP requests and responses - Application Services orchestrate use cases - Domain Services implement complex business logic that spans multiple entities - Domain Entities encapsulate business rules and data - Repositories handle data access - Value Objects represent immutable concepts - Response Formatters standardize API responses - Error Handlers provide consistent error handling

Code Organization

graph TD
    subgraph "src"
        subgraph "domain"
            A[Entities] --> A1[requisitionEntity.js]
            A[Entities] --> A2[userEntity.js]
            A[Entities] --> A3[purchaseOrderEntity.js]

            B[ValueObjects] --> B1[RequisitionStatus.js]
            B[ValueObjects] --> B2[Money.js]

            C[Services] --> C1[requisitionDomainService.js]

            D[Constants] --> D1[statusConstants.js]
            D[Constants] --> D2[errorMessages.js]
        end

        subgraph "application"
            E[Services] --> E1[requisitionService.js]
            E[Services] --> E2[userService.js]

            F[DTOs] --> F1[requisitionDTO.js]
            F[DTOs] --> F2[responseDTO.js]
        end

        subgraph "infrastructure"
            G[Repositories] --> G1[requisitionRepository.js]
            G[Repositories] --> G2[userRepository.js]

            H[Database] --> H1[models]
            H[Database] --> H2[migrations]

            I[ErrorHandling] --> I1[domainErrors.js]
            I[ErrorHandling] --> I2[applicationErrors.js]
        end

        subgraph "interfaces"
            J[Controllers] --> J1[requisitionController.js]
            J[Controllers] --> J2[userController.js]

            K[Routes] --> K1[requisitionRoutes.js]
            K[Routes] --> K2[userRoutes.js]

            L[Middleware] --> L1[authentication.js]
            L[Middleware] --> L2[validation.js]
        end
    end

The proposed code organization follows a layered architecture:

  1. Domain Layer: Contains the core business logic, entities, value objects, and domain services
  2. Application Layer: Orchestrates use cases, transforms data, and coordinates between domain and infrastructure
  3. Infrastructure Layer: Handles technical concerns like data access, error handling, and external services
  4. Interfaces Layer: Manages communication with the outside world through controllers, routes, and middleware

Business Logic Flow

flowchart TD
    A[Controller] -->|Request| B[Input Validation]
    B -->|Valid Input| C[Application Service]
    B -->|Invalid Input| D[Validation Error]
    C -->|Business Operation| E[Domain Entity]
    E -->|Apply Business Rules| F[Business Logic]
    F -->|Success| G[Repository]
    F -->|Failure| H[Domain Error]
    G -->|Data Operation| I[Database]
    I -->|Result| J[Response Formatting]
    H -->|Error Handling| K[Error Response]
    J -->|Response| L[Controller Response]
    K -->|Error Response| L

This flow diagram illustrates how a request is processed in the refactored architecture:

  1. The controller receives a request and validates the input
  2. If validation fails, a validation error is returned
  3. If validation succeeds, the application service is called
  4. The application service uses domain entities to apply business rules
  5. If business rules are satisfied, the repository is used to persist changes
  6. If business rules are violated, a domain error is returned
  7. The result is formatted and returned to the client

Requisition Processing Workflow

stateDiagram-v2
    [*] --> DRAFT: Create
    DRAFT --> SUBMITTED: Submit
    SUBMITTED --> PARTIALLY_APPROVED: Approve (some)
    SUBMITTED --> REJECTED: Reject
    PARTIALLY_APPROVED --> APPROVED: Approve (all)
    PARTIALLY_APPROVED --> REJECTED: Reject
    APPROVED --> ASSIGNED: Assign
    ASSIGNED --> CANVASSING: Start Canvass
    CANVASSING --> ORDERED: Create PO
    ORDERED --> PARTIALLY_DELIVERED: Partial Delivery
    ORDERED --> DELIVERED: Full Delivery
    PARTIALLY_DELIVERED --> DELIVERED: Complete Delivery
    DELIVERED --> CLOSED: Close
    PARTIALLY_DELIVERED --> CLOSED: Close

This state diagram shows the lifecycle of a requisition in the PRS system. Each state transition represents a business operation that should be governed by business rules in the domain layer.

Detailed Refactoring Plan for Requisition Entity

classDiagram
    class RequisitionEntity {
        +schemas: Object
        +businessLogic: Object
        +constants: Object
        +errorMessages: Object
    }

    class RequisitionSchemas {
        +createRequisitionSchema: ZodSchema
        +submitRequisition: ZodSchema
        +getRequisitionByIdSchema: ZodSchema
        +getRequisitionsSchema: ZodSchema
        +updateRequisitionSchema: ZodSchema
    }

    class RequisitionBusinessLogic {
        +canBeSubmitted(requisition): Boolean
        +canBeApprovedBy(requisition, userId): Boolean
        +canBeRejectedBy(requisition, userId): Boolean
        +getNextStatusAfterApproval(requisition, approverId): String
        +canBeClosed(requisition): Boolean
        +canBeAssigned(requisition): Boolean
        +hasItemSufficientQuantity(item, requestedQuantity): Boolean
    }

    class RequisitionConstants {
        +REQUISITION_STATUS: Object
        +REQUISITION_TYPES: Array
        +CHARGE_TO_LIST: Array
        +REQUISITION_CATEGORIES: Array
    }

    class RequisitionErrorMessages {
        +SUBMISSION_ERRORS: Object
        +APPROVAL_ERRORS: Object
        +REJECTION_ERRORS: Object
        +ASSIGNMENT_ERRORS: Object
        +CLOSURE_ERRORS: Object
    }

    RequisitionEntity --> RequisitionSchemas
    RequisitionEntity --> RequisitionBusinessLogic
    RequisitionEntity --> RequisitionConstants
    RequisitionEntity --> RequisitionErrorMessages

This class diagram shows how the Requisition entity should be structured:

  1. RequisitionEntity: The main entity that exports all requisition-related functionality
  2. RequisitionSchemas: Contains validation schemas for requisition operations
  3. RequisitionBusinessLogic: Contains business rules for requisition operations
  4. RequisitionConstants: Contains constants used in requisition operations
  5. RequisitionErrorMessages: Contains error messages for requisition operations

Error Handling Strategy

flowchart TD
    A[Error Occurs] --> B{Error Type?}
    B -->|Validation Error| C[Return 400 Bad Request]
    B -->|Domain Error| D[Return 422 Unprocessable Entity]
    B -->|Not Found| E[Return 404 Not Found]
    B -->|Authorization| F[Return 403 Forbidden]
    B -->|Authentication| G[Return 401 Unauthorized]
    B -->|Server Error| H[Return 500 Internal Server Error]

    C --> I[Include Validation Details]
    D --> J[Include Business Rule Violation]
    E --> K[Include Resource Information]
    F --> L[Include Permission Information]
    G --> M[Include Auth Information]
    H --> N[Log Error Details]

    I --> O[Standardized Error Response]
    J --> O
    K --> O
    L --> O
    M --> O
    N --> O

A consistent error handling strategy is crucial for a maintainable application. This flowchart shows how different types of errors should be handled:

  1. Validation Errors: Return 400 Bad Request with details about the validation failure
  2. Domain Errors: Return 422 Unprocessable Entity with details about the business rule violation
  3. Not Found Errors: Return 404 Not Found with information about the resource
  4. Authorization Errors: Return 403 Forbidden with information about the required permissions
  5. Authentication Errors: Return 401 Unauthorized with information about the authentication issue
  6. Server Errors: Return 500 Internal Server Error and log detailed error information

All errors should use a standardized error response format to ensure consistency across the API.

Implementation Steps

gantt
    title DDD Refactoring Implementation Plan
    dateFormat  YYYY-MM-DD
    section Analysis
    Identify Domain Entities           :a1, 2023-07-01, 7d
    Map Business Processes             :a2, after a1, 5d
    Define Bounded Contexts            :a3, after a2, 3d

    section Core Domain
    Create Domain Entities             :b1, after a3, 10d
    Implement Business Logic           :b2, after b1, 14d
    Create Value Objects               :b3, after a3, 7d
    Define Domain Services             :b4, after b2, 7d

    section Application Layer
    Create Application Services        :c1, after b4, 10d
    Implement DTOs                     :c2, after b3, 5d
    Create Response Formatters         :c3, after c2, 5d

    section Infrastructure
    Refactor Repositories              :d1, after c1, 10d
    Implement Error Handling           :d2, after c3, 7d

    section Interfaces
    Refactor Controllers               :e1, after d1, 14d
    Update Routes                      :e2, after e1, 3d

    section Testing
    Unit Tests                         :f1, after b2, 10d
    Integration Tests                  :f2, after e2, 7d

    section Deployment
    Code Review                        :g1, after f2, 5d
    Deployment                         :g2, after g1, 2d

This Gantt chart outlines a phased approach to implementing the DDD refactoring:

  1. Analysis Phase: Identify domain entities, map business processes, and define bounded contexts
  2. Core Domain Phase: Create domain entities, implement business logic, create value objects, and define domain services
  3. Application Layer Phase: Create application services, implement DTOs, and create response formatters
  4. Infrastructure Phase: Refactor repositories and implement error handling
  5. Interfaces Phase: Refactor controllers and update routes
  6. Testing Phase: Write unit tests and integration tests
  7. Deployment Phase: Conduct code review and deploy the refactored code

Specific Refactoring for Selected Code

flowchart TD
    A[Current: Mixed Schema in Entity] --> B[Separate Concerns]
    B --> C[Move Schema to ValueObjects]
    B --> D[Extract Constants]
    B --> E[Define Business Rules]

    C --> F[Create RequisitionQueryParams.js]
    D --> G[Create sortConstants.js]
    D --> H[Create filterConstants.js]
    E --> I[Create RequisitionQueryService.js]

    F --> J[Export Schema]
    G --> J
    H --> J
    I --> K[Export Business Logic]

    J --> L[Import in Controller]
    K --> L
    L --> M[Use for Validation & Processing]

This flowchart shows how to refactor a specific piece of code:

  1. Identify the mixed concerns in the current code
  2. Separate these concerns into validation, constants, and business rules
  3. Create appropriate files for each concern
  4. Export the functionality from these files
  5. Import and use the functionality in the controller

Recommendations for Refactoring

  1. Separate Validation from Business Logic:
  2. Move Zod schemas to a dedicated validation layer
  3. Keep business rules in domain entities

  4. Organize Constants:

  5. Move sort/filter constants to dedicated files
  6. Use enums for status values and other fixed lists

  7. Standardize Error Handling:

  8. Create domain-specific error classes
  9. Provide consistent error messages and codes

  10. Improve Response Formatting:

  11. Create DTOs for consistent response structure
  12. Separate data transformation from business logic

  13. Enhance Testability:

  14. Make business logic pure functions where possible
  15. Use dependency injection for services and repositories

Conclusion

This refactoring strategy provides a roadmap for transforming the PRS Backend into a well-structured, maintainable application based on DDD principles. By separating concerns, centralizing business logic, and standardizing error handling, the refactored code will be easier to understand, debug, and extend.