Skip to content

Practical DDD Refactoring Guide for PRS Backend

This guide provides a practical, step-by-step approach to refactoring the PRS Backend using Domain-Driven Design (DDD) principles. It focuses on concrete examples and folder structures to make the transition clear and manageable.

Current Folder Structure

Text Only
src/
├── app/
│   ├── errors/
│   ├── handlers/
│   │   ├── controllers/
│   │   │   ├── requisitionController.js
│   │   │   ├── userController.js
│   │   │   └── ...
│   │   └── routes/
│   ├── middlewares/
│   ├── repositories/
│   │   ├── requisitionRepository.js
│   │   ├── userRepository.js
│   │   └── ...
│   ├── services/
│   │   ├── requisitionService.js
│   │   ├── userService.js
│   │   └── ...
│   └── utils/
├── domain/
│   ├── constants/
│   └── entities/
│       ├── requisitionEntity.js
│       ├── userEntity.js
│       └── ...
└── db/
    ├── models/
    └── migrations/

Target DDD Folder Structure

Text Only
src/
├── domain/                  # Core business logic
│   ├── entities/            # Business entities with rules
│   │   ├── requisition/
│   │   │   ├── index.js
│   │   │   ├── RequisitionEntity.js
│   │   │   ├── RequisitionBusinessLogic.js
│   │   │   └── RequisitionErrors.js
│   │   └── user/
│   ├── valueObjects/        # Immutable objects
│   │   ├── RequisitionStatus.js
│   │   └── Money.js
│   ├── services/            # Domain services
│   │   └── RequisitionDomainService.js
│   └── constants/           # Domain constants
│       ├── statusConstants.js
│       └── errorMessages.js
├── application/             # Use case orchestration
│   ├── services/            # Application services
│   │   ├── requisition/
│   │   │   ├── RequisitionService.js
│   │   │   └── RequisitionQueryService.js
│   │   └── user/
│   └── dtos/                # Data Transfer Objects
│       ├── requisition/
│       │   ├── RequisitionDTO.js
│       │   └── RequisitionResponseDTO.js
│       └── common/
│           └── PaginatedResponseDTO.js
├── infrastructure/          # Technical concerns
│   ├── repositories/        # Data access
│   │   ├── requisition/
│   │   │   └── RequisitionRepository.js
│   │   └── user/
│   ├── database/            # Database configuration
│   │   ├── models/
│   │   └── migrations/
│   └── errors/              # Error handling
│       ├── DomainError.js
│       └── ApplicationError.js
└── interfaces/              # External communication
    ├── http/                # HTTP interface
    │   ├── controllers/     # Request handlers
    │   │   ├── requisition/
    │   │   │   └── RequisitionController.js
    │   │   └── user/
    │   ├── routes/          # Route definitions
    │   │   ├── requisitionRoutes.js
    │   │   └── userRoutes.js
    │   └── middleware/      # HTTP middleware
    │       ├── authentication.js
    │       └── validation.js
    └── events/              # Event handlers

Practical Refactoring Steps

Step 1: Reorganize the Domain Layer

Current:

JavaScript
// src/domain/entities/requisitionEntity.js
const { z } = require('zod');
const { sort, requisition, filter } = require('../constants');

// Validation schemas
const createRequisitionSchema = z.object({...});
const submitRequisition = z.object({...});

// Export everything
module.exports = {
  createRequisitionSchema,
  submitRequisition,
  // ... other schemas
};

Refactored:

JavaScript
// src/domain/entities/requisition/RequisitionEntity.js
const { RequisitionStatus } = require('../../valueObjects/RequisitionStatus');

class RequisitionEntity {
  constructor(data) {
    this.id = data.id;
    this.status = data.status;
    this.type = data.type;
    this.items = data.requisitionItemLists || [];
    // ... other properties
  }

  // Domain methods that enforce business rules
  canBeSubmitted() {
    return this.status === RequisitionStatus.DRAFT &&
           this.items.length > 0;
  }

  canBeApprovedBy(userId) {
    if (this.status !== RequisitionStatus.SUBMITTED &&
        this.status !== RequisitionStatus.PARTIALLY_APPROVED) {
      return false;
    }

    const approver = (this.requisitionApprovers || [])
      .find(a => a.userId === userId || a.approverId === userId);

    if (!approver) {
      return false;
    }

    return approver.status !== 'APPROVED';
  }

  // ... other business methods
}

module.exports = RequisitionEntity;
JavaScript
// src/domain/entities/requisition/RequisitionBusinessLogic.js
const { RequisitionStatus } = require('../../valueObjects/RequisitionStatus');

const requisitionBusinessLogic = {
  canBeSubmitted(requisition) {
    return requisition.status === RequisitionStatus.DRAFT &&
           Array.isArray(requisition.requisitionItemLists) &&
           requisition.requisitionItemLists.length > 0;
  },

  canBeApprovedBy(requisition, userId) {
    // ... business logic
  },

  // ... other business methods
};

module.exports = requisitionBusinessLogic;
JavaScript
// src/domain/entities/requisition/index.js
const RequisitionEntity = require('./RequisitionEntity');
const requisitionBusinessLogic = require('./RequisitionBusinessLogic');
const requisitionErrors = require('./RequisitionErrors');

module.exports = {
  RequisitionEntity,
  requisitionBusinessLogic,
  requisitionErrors
};

Step 2: Create Value Objects

JavaScript
// src/domain/valueObjects/RequisitionStatus.js
const RequisitionStatus = {
  DRAFT: 'DRAFT',
  SUBMITTED: 'SUBMITTED',
  PARTIALLY_APPROVED: 'PARTIALLY_APPROVED',
  APPROVED: 'APPROVED',
  ASSIGNED: 'ASSIGNED',
  CANVASSING: 'CANVASSING',
  ORDERED: 'ORDERED',
  PARTIALLY_DELIVERED: 'PARTIALLY_DELIVERED',
  DELIVERED: 'DELIVERED',
  CLOSED: 'CLOSED',
  REJECTED: 'REJECTED',
  CANCELLED: 'CANCELLED'
};

module.exports = { RequisitionStatus };

Step 3: Move Validation to Application Layer

JavaScript
// src/application/dtos/requisition/RequisitionValidation.js
const { z } = require('zod');
const { RequisitionStatus } = require('../../../domain/valueObjects/RequisitionStatus');

const createRequisitionSchema = z.object({
  isDraft: z.enum(['true']).transform(value => value === 'true'),
  type: z.enum(['ofm', 'non-ofm', 'ofm-tom', 'non-ofm-tom', 'transfer']),
  // ... other fields
});

const submitRequisitionSchema = z.object({
  id: z.number().positive().or(z.string().transform(Number)),
  isDraft: z.enum(['false']),
  // ... other fields
});

module.exports = {
  createRequisitionSchema,
  submitRequisitionSchema
};

Step 4: Refactor Application Services

JavaScript
// src/application/services/requisition/RequisitionService.js
const { RequisitionEntity, requisitionBusinessLogic } = require('../../../domain/entities/requisition');
const { DomainError } = require('../../../infrastructure/errors/DomainError');

class RequisitionService {
  constructor(requisitionRepository, userRepository) {
    this.requisitionRepository = requisitionRepository;
    this.userRepository = userRepository;
  }

  async submitRequisition(requisitionId, userId) {
    // Get the requisition
    const requisitionData = await this.requisitionRepository.getById(requisitionId);

    if (!requisitionData) {
      throw new DomainError('Requisition not found');
    }

    // Check business rules
    if (!requisitionBusinessLogic.canBeSubmitted(requisitionData)) {
      throw new DomainError(
        'Requisition cannot be submitted. It must be in DRAFT status and have at least one item.'
      );
    }

    // Update status
    await this.requisitionRepository.update(
      { id: requisitionId },
      { status: 'SUBMITTED' }
    );

    return { message: `Requisition with id ${requisitionId} submitted successfully` };
  }

  // ... other methods
}

module.exports = RequisitionService;

Step 5: Refactor Controllers

JavaScript
// src/interfaces/http/controllers/requisition/RequisitionController.js
class RequisitionController {
  constructor(requisitionService, requisitionValidation) {
    this.requisitionService = requisitionService;
    this.requisitionValidation = requisitionValidation;
  }

  async submitRequisition(request, reply) {
    try {
      // Validate input
      const { requisitionId } = this.requisitionValidation.submitRequisitionSchema.parse(request.body);

      // Call application service
      const result = await this.requisitionService.submitRequisition(
        requisitionId,
        request.userFromToken.id
      );

      // Return response
      return reply.status(200).send(result);
    } catch (error) {
      // Handle errors
      if (error.name === 'DomainError') {
        return reply.status(422).send({
          message: error.message
        });
      }

      if (error.name === 'ZodError') {
        return reply.status(400).send({
          message: 'Validation error',
          errors: error.errors
        });
      }

      return reply.status(500).send({
        message: 'Internal server error'
      });
    }
  }

  // ... other methods
}

module.exports = RequisitionController;

Step 6: Update Routes

JavaScript
// src/interfaces/http/routes/requisitionRoutes.js
const RequisitionController = require('../controllers/requisition/RequisitionController');
const authMiddleware = require('../middleware/authentication');

module.exports = function(fastify, opts, done) {
  const requisitionController = new RequisitionController(
    fastify.requisitionService,
    fastify.requisitionValidation
  );

  fastify.post(
    '/requisitions/:requisitionId/submit',
    { preHandler: authMiddleware },
    requisitionController.submitRequisition.bind(requisitionController)
  );

  // ... other routes

  done();
};

Practical Example: Refactoring the Requisition Submission Flow

Before Refactoring:

  1. Controller (src/app/handlers/controllers/requisitionController.js):
  2. Contains validation logic
  3. Contains business rules
  4. Handles HTTP concerns
  5. Calls repository directly

  6. Entity (src/domain/entities/requisitionEntity.js):

  7. Only contains validation schemas
  8. No business logic

  9. Repository (src/app/repositories/requisitionRepository.js):

  10. Handles data access

After Refactoring:

  1. Controller (src/interfaces/http/controllers/requisition/RequisitionController.js):
  2. Only handles HTTP concerns
  3. Calls application service
  4. Handles errors

  5. Application Service (src/application/services/requisition/RequisitionService.js):

  6. Orchestrates the use case
  7. Calls domain entities and repositories
  8. Handles transactions

  9. Domain Entity (src/domain/entities/requisition/RequisitionEntity.js):

  10. Contains business rules
  11. Enforces invariants

  12. Business Logic (src/domain/entities/requisition/RequisitionBusinessLogic.js):

  13. Contains pure business logic functions
  14. Can be easily tested

  15. Value Objects (src/domain/valueObjects/RequisitionStatus.js):

  16. Represents immutable concepts
  17. Provides type safety

  18. Validation (src/application/dtos/requisition/RequisitionValidation.js):

  19. Contains validation schemas
  20. Separate from business logic

  21. Repository (src/infrastructure/repositories/requisition/RequisitionRepository.js):

  22. Handles data access
  23. Returns domain entities

Benefits of This Approach

  1. Clear Separation of Concerns:
  2. Controllers handle HTTP
  3. Application services handle use cases
  4. Domain entities handle business rules
  5. Repositories handle data access

  6. Improved Testability:

  7. Business logic can be tested independently
  8. Controllers can be tested with mocked services
  9. Services can be tested with mocked repositories

  10. Better Error Handling:

  11. Domain errors are separate from technical errors
  12. Consistent error responses

  13. Enhanced Maintainability:

  14. Business rules are centralized
  15. Code is organized by domain concepts
  16. Changes are localized to specific components

Implementation Strategy

  1. Start Small:
  2. Begin with one domain entity (e.g., Requisition)
  3. Refactor one use case at a time (e.g., submission flow)

  4. Incremental Approach:

  5. Create the new structure alongside the existing code
  6. Gradually migrate functionality
  7. Test thoroughly after each step

  8. Focus on Business Logic First:

  9. Extract business rules to domain entities
  10. Ensure they work correctly before refactoring controllers

  11. Update Controllers Last:

  12. Once domain and application layers are in place
  13. Redirect controllers to use the new services

This practical guide provides a clear path to refactoring the PRS Backend using DDD principles, with concrete examples and a step-by-step approach that minimizes risk and maximizes value.