Skip to content

Domain-Driven Design (DDD) Guide for PRS Project

Introduction: What is Domain-Driven Design?

Domain-Driven Design (DDD) is an approach to software development that focuses on understanding the business domain and organizing code to reflect that understanding.

Simple Explanation

Think of it like organizing a hospital:

  • Traditional Approach (What We Have Now): Organizing by technical specialties (all X-ray machines in one department, all blood testing equipment in another, all doctors in another area). A patient has to move between many departments to get treatment.
  • DDD Approach (What We Want): Organizing by patient needs (a cardiac care unit has its own X-ray machines, blood testing equipment, and cardiac specialists all working together). Everything needed to solve a specific problem is grouped together.

Another Analogy: The Restaurant Kitchen

  • Traditional Approach: Organizing a kitchen by tool type (all knives in one area, all pots in another, all ingredients in another). The chef has to run around the kitchen to make a single dish.
  • DDD Approach: Organizing by meal preparation (pasta station has its own knives, pots, and ingredients needed for pasta dishes). Everything needed to make a specific type of dish is grouped together.

What This Means For Our Code

Traditional Approach (Current) DDD Approach (Target)
Organized by technical layers (controllers folder, models folder, services folder) Organized by business capability (requisition folder with its controllers, models, and services)
Business logic spread across multiple files Business logic centralized in domain entities
Database-centric design Business-centric design
Technical naming (req_slip, po_item) Business naming (Requisition, PurchaseOrderItem)

Why Our PRS Project Needs DDD

Our current PRS (Purchase Requisition System) codebase has several specific problems that DDD can help solve:

Current Problems in Our Codebase

  1. Business Logic Scattered Everywhere
  2. Example: Approval rules are duplicated in controllers, services, and even UI components
  3. Location: src/app/controllers/requisitionController.js, src/app/services/requisitionService.js, src/features/requisition/components/ApprovalButton.jsx
  4. Inconsistent Terminology
  5. Example: We use req, requisition, and request to refer to the same concept
  6. Location: Variable names throughout the codebase
  7. No Clear Boundaries Between Components
  8. Example: Purchase Order service directly modifies Requisition data
  9. Location: src/app/services/purchaseOrderService.js line 120-145
  10. Database-Driven Design
  11. Example: Our entities are just database models with no business behavior
  12. Location: src/infra/database/models/ directory
  13. Hard to Test
  14. Example: Business logic is tightly coupled with database access
  15. Location: Most service classes

Benefits DDD Will Bring to Our Project

  1. Clearer Code Organization
  2. All code related to requisitions will be in one place
  3. All code related to purchase orders will be in another place
  4. Consistent Terminology
  5. We'll use the same terms throughout the code that business users use
  6. Explicit Business Rules
  7. Business rules will be clearly defined in domain entities
  8. No more hunting through the codebase to find all the places a rule is implemented
  9. Easier Testing
  10. Business logic can be tested without database or UI dependencies
  11. Easier Onboarding for New Developers
  12. The code structure will reflect the business domain, making it easier to understand

Core DDD Concepts with PRS Examples

1. Ubiquitous Language

What It Is: Using the same terms in code that business users use in conversation.

Current Problem in Our Code:

// File: src/app/controllers/reqController.js
function createReqSlip(req, res) {
const reqData \= req.body;
// Create a requisition slip
const newReq \= await db.req_slips.create(reqData);
return res.json(newReq);
}

How to Fix It:

// File: src/app/controllers/requisitionController.js
function createRequisition(req, res) {
const requisitionData \= req.body;
// Create a requisition
const newRequisition \= await requisitionService.createRequisition(requisitionData);
return res.json(newRequisition);
}

Specific Terms to Standardize in Our Project:

Current Mixed Terms Standardized Term to Use
req_slip, req, request Requisition
po, purchase_order PurchaseOrder
dr, delivery, delivery_receipt DeliveryReceipt
canvass, canvas, quotation Canvass
supp, supplier, vendor Supplier
payment_req, payment PaymentRequest

2. Bounded Contexts

What It Is: Dividing your system into separate areas where certain terms have specific meanings.

Current Problem in Our Code:

// In src/app/services/requisitionService.js
async function approveRequisition(id) {
const requisition \= await db.Requisition.findByPk(id);
requisition.status \= "APPROVED"; // Means approved by all approvers
await requisition.save();
}

// In src/app/services/purchaseOrderService.js
async function approvePurchaseOrder(id) {
const po \= await db.PurchaseOrder.findByPk(id);
po.status \= "APPROVED"; // Means approved but not yet sent to supplier
await po.save();
}

How to Fix It:

// In src/domain/requisition/Requisition.js
class Requisition {
approve() {
if (this.status !== "PENDING_APPROVAL") {
throw new Error("Only pending requisitions can be approved");
}
this.status \= "FULLY_APPROVED";
}
}

// In src/domain/purchaseOrder/PurchaseOrder.js
class PurchaseOrder {
approve() {
if (this.status !== "DRAFT") {
throw new Error("Only draft purchase orders can be approved");
}
this.status \= "APPROVED_INTERNALLY";
}
}

Our Main Bounded Contexts:

  1. Requisition Context: Everything related to creating and approving requisitions
  2. Canvass Context: Everything related to supplier quotations and selection
  3. Purchase Order Context: Everything related to creating and managing purchase orders
  4. Delivery Context: Everything related to receiving goods
  5. Payment Context: Everything related to processing payments

3. Entities and Value Objects

What It Is:

Entities:

  • Objects that have a unique identity that remains the same even when their attributes change
  • We care about which specific instance we're dealing with
  • Example: A specific Purchase Order with ID #12345 is still the same Purchase Order even if we change the items in it
  • Think of entities like people - you're still the same person even if you change your clothes or haircut
  • In our system: Requisition, PurchaseOrder, Supplier, User

Value Objects:

  • Objects that have no identity - we only care about their attributes/values
  • Two value objects with the same attributes are considered equal and interchangeable
  • Example: A Money object representing PHP 500 is identical to any other Money object representing PHP 500
  • Think of value objects like currency - one PHP 500 bill is interchangeable with any other PHP 500 bill
  • In our system: Money, Address, DateRange, LineItem, Quantity

Value Objects: Super Simple Explanation

Imagine you have PHP 500 in your wallet. If someone swaps your PHP 500 bill with another PHP 500 bill, would you care? No, because they have the same value. That's a value object - we only care about what it represents (500 pesos), not which specific bill it is.

Concrete Examples in Our PRS System:

  1. Money
    // This is how we currently handle money (problematic)
    let itemPrice \= 100.50;
    let totalPrice \= itemPrice * quantity; // Can lead to floating point issues

// With a Money value object
const itemPrice \= new Money(100.50, "PHP");
const totalPrice \= itemPrice.multiply(quantity); // Handles calculations properly

  1. Address
    // Current approach (just data)
    const supplierAddress \= {
    street: "123 Main St",
    city: "Manila",
    zipCode: "1000"
    };

// With an Address value object
const supplierAddress \= new Address("123 Main St", "Manila", "1000");
if (supplierAddress.isInMetroManila()) { // Has behavior
// Apply Metro Manila delivery rules
}

  1. Quantity
    // Current approach (just a number)
    const itemQuantity \= 5;
    if (itemQuantity \<= 0) {
    throw new Error("Invalid quantity");
    }

// With a Quantity value object
const itemQuantity \= new Quantity(5); // Validation happens in constructor
const newQuantity \= itemQuantity.add(3); // Returns a new Quantity object

Key Characteristics of Value Objects:

  1. Immutability: They cannot be changed after creation. Instead of changing a value object, you create a new one.
    // WRONG way (mutation)
    const money \= new Money(100);
    money.amount \= 200; // Don't do this!

// RIGHT way (immutability)
const money \= new Money(100);
const moreMoney \= new Money(200); // Create a new object instead

  1. Equality Based on Attributes: Two value objects are equal if all their attributes are equal.
    const money1 \= new Money(100, "PHP");
    const money2 \= new Money(100, "PHP");

// These are considered equal because they have the same attributes
console.log(money1.equals(money2)); // true

  1. Self-Validation: They validate their own data to ensure they're always in a valid state.
    class Money {
    constructor(amount, currency \= "PHP") {
    if (amount \< 0) {
    throw new Error("Amount cannot be negative");
    }
    this.amount \= amount;
    this.currency \= currency;
    }
    }

Why Use Value Objects?

  1. Prevent Bugs: Proper handling of special types like money, dates, and quantities
  2. Add Behavior: Methods that make sense for that type of value
  3. Self-Validation: Ensures the data is always valid
  4. Better Readability: Code becomes more expressive and easier to understand

Current Problem in Our Code:

// Direct database manipulation
const requisition \= await db.Requisition.findByPk(id);
requisition.totalAmount \= calculateTotal(requisition.items);
requisition.status \= "APPROVED";
await requisition.save();

// Money calculations with potential floating point issues
const total \= item1.price + item2.price;

How to Fix It:

Entity Example:

// src/domain/entities/Requisition.js
class Requisition {
constructor(id, requesterId, department, items \= []) {
this.id \= id;
this.requesterId \= requesterId;
this.department \= department;
this.items \= items;
this.status \= "DRAFT";
this.approvals \= [];
}

addItem(item) {
// Validation logic
if (item.quantity \<= 0) {
throw new Error("Quantity must be greater than zero");
}

Text Only
1
this.items.push(item);

}

calculateTotal() {
return this.items.reduce((total, item) \=>
total.add(item.unitPrice.multiply(item.quantity)),
new Money(0)
);
}

submit() {
if (this.status !== "DRAFT") {
throw new Error("Only draft requisitions can be submitted");
}

Text Only
1
2
3
4
5
if (this.items.length \=== 0\) {  
  throw new Error("Cannot submit empty requisition");  
}

this.status \= "PENDING\_APPROVAL";

}
}

Value Object Example:

// src/domain/valueObjects/Money.js
class Money {
constructor(amount, currency \= "PHP") {
// Store as cents/smallest unit to avoid floating point issues
this.amount \= Math.round(amount * 100);
this.currency \= currency;
}

add(money) {
if (this.currency !== money.currency) {
throw new Error("Cannot add different currencies");
}
return new Money(this.amount/100 + money.amount/100, this.currency);
}

multiply(quantity) {
return new Money((this.amount * quantity)/100, this.currency);
}

equals(money) {
return this.amount \=== money.amount && this.currency \=== money.currency;
}

toString() {
return `${this.currency} ${(this.amount/100).toFixed(2)}`;
}
}

Entities vs. Value Objects in Our Project:

Entities (Have Identity) Value Objects (Defined by Attributes)
Requisition Money
PurchaseOrder Address
Supplier LineItem
User DateRange
DeliveryReceipt Quantity

4. Aggregates and Aggregate Roots

What It Is:

Aggregates and Aggregate Roots Explained:

  • Aggregate: A group of related objects that we treat as a single unit for data changes
  • Aggregate Root: The main entity in the group that controls access to all other objects inside the aggregate
  • Key Concept: You can only access and modify objects within an aggregate through its root
  • Real-World Analogy: Think of a company organization:
  • The CEO (aggregate root) is the only person external departments can officially communicate with
  • To reach anyone in the company (child entities), you must go through proper channels starting with the CEO
  • The CEO enforces company policies (business rules) for all interactions
  • Why This Matters:
  • Ensures business rules are always enforced
  • Prevents data inconsistency
  • Simplifies transaction boundaries
  • Example in Our System:
  • A Requisition (root) contains RequisitionItems (child entities)
  • You should never directly modify a RequisitionItem without going through the Requisition
  • The Requisition enforces rules like "can't modify items after approval"

Current Problem in Our Code:

// Direct access to child objects bypassing business rules
const requisitionItem \= await db.RequisitionItem.findByPk(itemId);
requisitionItem.quantity \= 100; // No validation!
await requisitionItem.save();

// In another part of the code
const requisition \= await db.Requisition.findByPk(requisitionItem.requisitionId);
if (requisition.status !== "DRAFT") {
// Oops, we modified an item for a requisition that's not in draft status!
}

How to Fix It:

// Access only through the aggregate root (Requisition)
const requisition \= await requisitionRepository.findById(requisitionId);

// All changes go through the root, which enforces business rules
requisition.updateItemQuantity(itemId, 100);

// Save the entire aggregate
await requisitionRepository.save(requisition);

Main Aggregates in Our System:

  1. Requisition Aggregate:
  2. Root: Requisition
  3. Members: RequisitionItem, RequisitionApproval
  4. Purchase Order Aggregate:
  5. Root: PurchaseOrder
  6. Members: PurchaseOrderItem, PurchaseOrderApproval
  7. Supplier Aggregate:
  8. Root: Supplier
  9. Members: SupplierContact, SupplierAddress
  10. Delivery Aggregate:
  11. Root: DeliveryReceipt
  12. Members: DeliveryItem, DeliveryInspection

5. Repositories

What It Is: Objects that handle storing and retrieving domain objects, hiding database details.

Current Problem in Our Code:

// Database access scattered throughout the codebase
class RequisitionService {
async getRequisition(id) {
return await db.Requisition.findByPk(id, {
include: [db.RequisitionItem, db.User]
});
}

async updateRequisition(id, data) {
const requisition \= await db.Requisition.findByPk(id);
await requisition.update(data);
return requisition;
}
}

How to Fix It:

// src/infra/repositories/RequisitionRepository.js
class RequisitionRepository {
constructor(db) {
this.db \= db;
}

async findById(id) {
const data \= await this.db.Requisition.findByPk(id, {
include: [this.db.RequisitionItem, this.db.RequisitionApproval]
});

Text Only
1
2
3
4
if (\!data) return null;

// Convert database model to domain entity  
return this.\_toDomainEntity(data);

}

async save(requisition) {
const transaction \= await this.db.sequelize.transaction();

Text Only
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
try {  
  // Convert domain entity to database model  
  const requisitionData \= this.\_toDataModel(requisition);

  if (requisition.id) {  
    // Update existing  
    await this.db.Requisition.update(requisitionData, {  
      where: { id: requisition.id },  
      transaction  
    });  
  } else {  
    // Create new  
    const newRequisition \= await this.db.Requisition.create(requisitionData, {  
      transaction  
    });  
    requisition.id \= newRequisition.id;  
  }

  // Handle items (create, update, delete)  
  // ...

  await transaction.commit();  
  return requisition;  
} catch (error) {  
  await transaction.rollback();  
  throw error;  
}

}

// Helper methods to convert between domain and database models
_toDomainEntity(data) { /* ... */ }
_toDataModel(entity) { /* ... */ }
}

Repositories We Need to Create:

  1. RequisitionRepository
  2. PurchaseOrderRepository
  3. SupplierRepository
  4. DeliveryReceiptRepository
  5. PaymentRequestRepository

6. Application Services

What It Is: Services that orchestrate the use cases of the application, coordinating between repositories and domain objects.

Current Problem in Our Code:

// Mixed concerns in service
class RequisitionService {
async approveRequisition(id, approverId) {
const requisition \= await db.Requisition.findByPk(id);

Text Only
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Business logic mixed with data access  
if (requisition.status \!== "PENDING") {  
  throw new Error("Cannot approve non-pending requisition");  
}

// Direct database manipulation  
await db.RequisitionApproval.create({  
  requisitionId: id,  
  approverId,  
  date: new Date()  
});

// More database queries  
const approvals \= await db.RequisitionApproval.findAll({  
  where: { requisitionId: id }  
});

// More business logic  
if (approvals.length \=== requisition.requiredApprovals) {  
  requisition.status \= "APPROVED";  
  await requisition.save();

  // Email sending mixed in  
  await sendEmail(requisition.createdBy, "Your requisition was approved");  
}

return requisition;

}
}

How to Fix It:

// src/app/services/RequisitionService.js
class RequisitionService {
constructor(requisitionRepository, notificationService) {
this.requisitionRepository \= requisitionRepository;
this.notificationService \= notificationService;
}

async approveRequisition(requisitionId, approverId) {
// Get domain entity from repository
const requisition \= await this.requisitionRepository.findById(requisitionId);

Text Only
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
if (\!requisition) {  
  throw new Error("Requisition not found");  
}

// Business logic in domain entity  
requisition.approve(approverId);

// Save changes through repository  
await this.requisitionRepository.save(requisition);

// Notification handled by separate service  
if (requisition.isFullyApproved()) {  
  await this.notificationService.notifyRequisitionApproved(requisition);  
}

return requisition;

}
}

Application Services We Need to Create:

  1. RequisitionService - For creating, submitting, and approving requisitions
  2. CanvassService - For managing supplier quotations
  3. PurchaseOrderService - For creating and managing purchase orders
  4. DeliveryService - For recording deliveries
  5. PaymentService - For processing payments

Security in Domain-Driven Design

Security is a critical aspect of any enterprise application, and Domain-Driven Design provides powerful patterns for implementing robust security models. This section explores how to integrate security concerns into our DDD approach for the PRS project.

1. Security as a Bounded Context

What It Is: Treating security as its own bounded context with specific models, language, and rules.

Why It Matters: Security has its own specialized concepts (authentication, authorization, roles, permissions) that are distinct from business domains.

Current Problem in Our Code:

// Security concerns mixed with business logic
// In src/app/services/requisitionService.js
async function approveRequisition(id, approverId) {
const requisition \= await db.Requisition.findByPk(id);

// Security check mixed with business logic
const user \= await db.User.findByPk(approverId);
if (!user.hasPermission('approve_requisition')) {
throw new Error("User does not have permission to approve requisitions");
}

// Business logic continues...
requisition.status \= "APPROVED";
await requisition.save();
}

How to Fix It:

// Security bounded context
// In src/domain/security/Authorization.js
class Authorization {
constructor(permissionRepository) {
this.permissionRepository \= permissionRepository;
}

async canApproveRequisition(userId) {
const permissions \= await this.permissionRepository.getPermissionsForUser(userId);
return permissions.some(p \=>
p.module \=== 'dashboard' && p.action \=== 'approval'
);
}
}

// Business bounded context
// In src/app/services/RequisitionService.js
class RequisitionService {
constructor(requisitionRepository, authorizationService) {
this.requisitionRepository \= requisitionRepository;
this.authorizationService \= authorizationService;
}

async approveRequisition(requisitionId, approverId) {
// Security check through dedicated service
const canApprove \= await this.authorizationService.canApproveRequisition(approverId);
if (!canApprove) {
throw new Error("User does not have permission to approve requisitions");
}

Text Only
1
2
3
4
// Business logic in domain entity  
const requisition \= await this.requisitionRepository.findById(requisitionId);  
requisition.approve(approverId);  
await this.requisitionRepository.save(requisition);

}
}

2. Security Entities and Value Objects

What It Is: Modeling security concepts as domain entities and value objects.

Key Security Entities and Value Objects for PRS:

  1. User Entity
    // In src/domain/security/entities/User.js
    class User {
    constructor(id, username, email, roleId, departmentId) {
    this.id \= id;
    this.username \= username;
    this.email \= email;
    this.roleId \= roleId;
    this.departmentId \= departmentId;
    this.permissions \= [];
    }

    hasPermission(module, action) {
    return this.permissions.some(p \=>
    p.module \=== module && p.action \=== action
    );
    }

    isInRole(roleId) {
    return this.roleId \=== roleId;
    }
    }

  2. Permission Value Object
    // In src/domain/security/valueObjects/Permission.js
    class Permission {
    constructor(module, action) {
    this.module \= module;
    this.action \= action;
    }

    equals(permission) {
    return this.module \=== permission.module &&
    this.action \=== permission.action;
    }

    toString() {
    return `${this.module}:${this.action}`;
    }
    }

  3. Role Entity
    // In src/domain/security/entities/Role.js
    class Role {
    constructor(id, name, permissions \= []) {
    this.id \= id;
    this.name \= name;
    this.permissions \= permissions;
    }

    hasPermission(module, action) {
    return this.permissions.some(p \=>
    p.module \=== module && p.action \=== action
    );
    }

    addPermission(permission) {
    if (!this.hasPermission(permission.module, permission.action)) {
    this.permissions.push(permission);
    }
    }

    removePermission(permission) {
    this.permissions \= this.permissions.filter(p \=>
    !(p.module \=== permission.module && p.action \=== permission.action)
    );
    }
    }

3. Security Aggregates

What It Is: Grouping security entities into aggregates with clear boundaries.

Main Security Aggregates for PRS:

  1. User Aggregate
  2. Root: User
  3. Members: UserPreferences, UserLeaves
  4. Role Aggregate
  5. Root: Role
  6. Members: RolePermissions

Example Implementation:

// In src/domain/security/aggregates/UserAggregate.js
class User {
constructor(id, username, email, roleId, departmentId) {
this.id \= id;
this.username \= username;
this.email \= email;
this.roleId \= roleId;
this.departmentId \= departmentId;
this.preferences \= null;
this.leaves \= [];
}

// User entity methods

// Methods to manage child entities
addLeave(startDate, endDate, reason) {
// Validation logic
if (startDate > endDate) {
throw new Error("Start date cannot be after end date");
}

Text Only
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Check for overlapping leaves  
const hasOverlap \= this.leaves.some(leave \=\>  
  (startDate \<= leave.endDate && endDate \>= leave.startDate)  
);

if (hasOverlap) {  
  throw new Error("Leave period overlaps with existing leave");  
}

this.leaves.push({ startDate, endDate, reason });

}

setPreferences(preferences) {
this.preferences \= preferences;
}
}

4. Authorization in Domain Services

What It Is: Implementing authorization logic in domain services.

Current Problem in Our Code:

// Authorization disabled in middleware
// From src/app/handlers/middlewares/authorize.js
const authorize \= function (requiredPermissions \= [], requireAll \= false) {
return async function (request, _reply) {
// disable permission checking, kasi may bug
return true;
// Actual authorization code never runs...
};
};

How to Fix It:

// In src/domain/security/services/AuthorizationService.js
class AuthorizationService {
constructor(userRepository, roleRepository) {
this.userRepository \= userRepository;
this.roleRepository \= roleRepository;
}

async authorize(userId, requiredPermissions, requireAll \= false) {
const user \= await this.userRepository.findById(userId);

Text Only
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
if (\!user) {  
  return false;  
}

const role \= await this.roleRepository.findById(user.roleId);

if (\!role) {  
  return false;  
}

// Support for OR and AND logic  
const checkPermissions \= requireAll ? 'every' : 'some';

return requiredPermissions\[checkPermissions\](permission \=\>  
  role.hasPermission(permission.module, permission.action)  
);

}
}

// In src/app/handlers/middlewares/authorize.js
const authorize \= function (requiredPermissions \= [], requireAll \= false) {
const permissions \= Array.isArray(requiredPermissions)
? requiredPermissions
: [requiredPermissions];

return async function (request, _reply) {
const clientErrors \= this.diScope.resolve('clientErrors');
const authorizationService \= this.diScope.resolve('authorizationService');
const errorMessage \= 'You do not have permission to perform this action';
const { userFromToken } \= request;

Text Only
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
if (\!userFromToken) {  
  throw clientErrors.FORBIDDEN({  
    message: errorMessage,  
  });  
}

const hasPermission \= await authorizationService.authorize(  
  userFromToken.id,  
  permissions,  
  requireAll  
);

if (\!hasPermission) {  
  throw clientErrors.FORBIDDEN({  
    message: errorMessage,  
  });  
}

return true;

};
};

5. Security Patterns for DDD

5.1 Permission-Based Security

What It Is: Defining permissions as combinations of modules and actions.

Implementation for PRS:

// In src/domain/constants/permissionConstants.js
const MODULES \= {
ROLES: 'roles',
USERS: 'users',
COMPANIES: 'companies',
PROJECTS: 'projects',
DEPARTMENTS: 'departments',
SUPPLIERS: 'suppliers',
DASHBOARD: 'dashboard', // Requisition Slip
ORDERS: 'orders',
DELIVERY: 'delivery',
PAYMENTS: 'payments',
};

const ACTIONS \= {
VIEW: 'view',
GET: 'get',
CREATE: 'create',
UPDATE: 'update',
DELETE: 'delete',
SYNC: 'sync',
APPROVAL: 'approval',
};

const PERMISSIONS \= {
VIEW_REQUISITIONS: { module: MODULES.DASHBOARD, action: ACTIONS.VIEW },
CREATE_REQUISITIONS: { module: MODULES.DASHBOARD, action: ACTIONS.CREATE },
APPROVE_REQUISITIONS: { module: MODULES.DASHBOARD, action: ACTIONS.APPROVAL },
// More permissions...
};

5.2 Domain-Specific Access Control

What It Is: Implementing access control rules specific to domain entities.

Implementation for PRS:

// In src/domain/entities/Requisition.js
class Requisition {
constructor(id, requesterId, department, items \= []) {
this.id \= id;
this.requesterId \= requesterId;
this.department \= department;
this.items \= items;
this.status \= "DRAFT";
this.approvals \= [];
}

// Business methods

// Domain-specific access control
canBeViewedBy(userId, userDepartment, userPermissions) {
// Requesters can view their own requisitions
if (userId \=== this.requesterId) {
return true;
}

Text Only
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Department members can view their department's requisitions  
if (userDepartment \=== this.department) {  
  return true;  
}

// Users with view permission can view all requisitions  
if (userPermissions.some(p \=\>  
  p.module \=== 'dashboard' && p.action \=== 'view'  
)) {  
  return true;  
}

return false;

}

canBeApprovedBy(userId, userPermissions) {
// Only pending requisitions can be approved
if (this.status !== "PENDING_APPROVAL") {
return false;
}

Text Only
1
2
3
4
5
6
7
8
9
// Check if user has already approved  
if (this.approvals.some(a \=\> a.approverId \=== userId)) {  
  return false;  
}

// Check if user has approval permission  
return userPermissions.some(p \=\>  
  p.module \=== 'dashboard' && p.action \=== 'approval'  
);

}
}

5.3 Security Value Objects for Validation

What It Is: Using value objects to enforce security validation rules.

Implementation for PRS:

// In src/domain/security/valueObjects/Password.js
class Password {
constructor(value) {
// Validate password strength
if (value.length \< 8) {
throw new Error("Password must be at least 8 characters long");
}

Text Only
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if (\!/\[A-Z\]/.test(value)) {  
  throw new Error("Password must contain at least one uppercase letter");  
}

if (\!/\[0-9\]/.test(value)) {  
  throw new Error("Password must contain at least one number");  
}

if (\!/\[^A-Za-z0-9\]/.test(value)) {  
  throw new Error("Password must contain at least one special character");  
}

// Store hashed password  
this.value \= this.hash(value);

}

hash(plaintext) {
// In a real implementation, use bcrypt or similar
// This is a simplified example
return `hashed_${plaintext}`;
}

verify(plaintext) {
return this.value \=== this.hash(plaintext);
}
}

6. Common Security Mistakes in DDD

6.1 Mixing Security and Business Logic

What It Is: Embedding security checks directly in business logic.

Example of the Mistake:

// Security mixed with business logic
class Requisition {
approve(approverId) {
// Security check in business logic
if (!this.userHasPermission(approverId, 'approve_requisitions')) {
throw new Error("User does not have permission to approve");
}

Text Only
1
2
// Business logic  
this.status \= "APPROVED";

}

userHasPermission(userId, permission) {
// Database call in domain entity
return db.UserPermission.findOne({
where: { userId, permission }
});
}
}

How to Fix It:

// Security handled separately from business logic
// In application service
class RequisitionService {
constructor(requisitionRepository, authorizationService) {
this.requisitionRepository \= requisitionRepository;
this.authorizationService \= authorizationService;
}

async approveRequisition(requisitionId, approverId) {
// Security check through dedicated service
const canApprove \= await this.authorizationService.canApproveRequisition(approverId);
if (!canApprove) {
throw new Error("User does not have permission to approve requisitions");
}

Text Only
1
2
3
4
// Business logic in domain entity  
const requisition \= await this.requisitionRepository.findById(requisitionId);  
requisition.approve(approverId);  
await this.requisitionRepository.save(requisition);

}
}

// Pure business logic in domain entity
class Requisition {
approve(approverId) {
if (this.status !== "PENDING_APPROVAL") {
throw new Error("Only pending requisitions can be approved");
}

Text Only
1
2
3
4
5
this.approvals.push({ approverId, date: new Date() });

if (this.isFullyApproved()) {  
  this.status \= "APPROVED";  
}

}
}

6.2 Ignoring Security in Domain Model

What It Is: Treating security as purely an infrastructure concern.

Example of the Mistake:

// Security only in middleware, not in domain model
// In src/app/handlers/middlewares/authorize.js
const authorize \= function (requiredPermissions) {
return async function (request, _reply) {
// Check permissions
};
};

// No security awareness in domain model
class Requisition {
approve(approverId) {
// No security checks or rules
this.status \= "APPROVED";
}
}

How to Fix It:

// Security in middleware
const authorize \= function (requiredPermissions) {
return async function (request, _reply) {
// Check permissions
};
};

// Domain model aware of security rules
class Requisition {
approve(approverId) {
// Business rules that enforce security constraints
if (this.status !== "PENDING_APPROVAL") {
throw new Error("Only pending requisitions can be approved");
}

Text Only
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Domain-specific security rule  
if (this.requesterId \=== approverId) {  
  throw new Error("Requesters cannot approve their own requisitions");  
}

this.approvals.push({ approverId, date: new Date() });

if (this.isFullyApproved()) {  
  this.status \= "APPROVED";  
}

}
}

6.3 Hardcoding Security Rules

What It Is: Embedding security rules directly in code instead of making them configurable.

Example of the Mistake:

// Hardcoded security rules
class RequisitionService {
async approveRequisition(requisitionId, approverId) {
const user \= await this.userRepository.findById(approverId);

Text Only
1
2
3
4
5
6
// Hardcoded role check  
if (user.roleId \!== 2 && user.roleId \!== 3\) { // 2 \= Manager, 3 \= Director  
  throw new Error("Only managers and directors can approve requisitions");  
}

// Business logic continues...

}
}

How to Fix It:

// Configurable security rules
// In src/domain/security/rules/ApprovalRules.js
class ApprovalRules {
constructor(roleRepository) {
this.roleRepository \= roleRepository;
}

async canApproveRequisition(userId, roleId) {
const approverRoles \= await this.roleRepository.getApproverRoles();
return approverRoles.some(role \=> role.id \=== roleId);
}
}

// In application service
class RequisitionService {
constructor(requisitionRepository, approvalRules) {
this.requisitionRepository \= requisitionRepository;
this.approvalRules \= approvalRules;
}

async approveRequisition(requisitionId, approverId, userRoleId) {
// Configurable role check
const canApprove \= await this.approvalRules.canApproveRequisition(
approverId, userRoleId
);

Text Only
1
2
3
4
5
if (\!canApprove) {  
  throw new Error("User does not have permission to approve requisitions");  
}

// Business logic continues...

}
}

Step-by-Step Refactoring Guide

Phase 1: Preparation

  1. Identify Domain Experts
  2. Talk to business users who understand the purchase requisition process
  3. Document the key terms they use
  4. Map Current Codebase
  5. Identify where business logic currently lives
  6. Document current database schema
  7. List all current services and their responsibilities
  8. Create Glossary of Terms
  9. Document all business terms and their meanings
  10. Identify inconsistencies in terminology

Phase 2: Create Domain Model

  1. Create Domain Entities
  2. Start with Requisition.js in src/domain/entities/
  3. Add business methods like submit(), approve(), etc.
  4. Move validation logic into entities
  5. Create Value Objects
  6. Create Money.js in src/domain/valueObjects/
  7. Create other value objects like Address.js, Quantity.js
  8. Define Aggregates
  9. Identify which entities form aggregates
  10. Ensure all access to child entities goes through the root

Phase 3: Create Infrastructure Layer

  1. Create Repositories
  2. Create RequisitionRepository.js in src/infra/repositories/
  3. Implement methods to convert between domain entities and database models
  4. Create Database Models
  5. Keep existing database models in src/infra/database/models/
  6. These will be used by repositories

Phase 4: Create Application Services

  1. Refactor Services
  2. Create new service classes that use repositories and domain entities
  3. Move business logic from old services to domain entities
  4. Create Supporting Services
  5. Create services like NotificationService for cross-cutting concerns

Phase 5: Update Controllers

  1. Refactor Controllers
  2. Update controllers to use new application services
  3. Remove any business logic from controllers

Phase 6: Testing and Validation

  1. Write Unit Tests
  2. Test domain entities in isolation
  3. Test repositories with database mocks
  4. Write Integration Tests
  5. Test the full flow from controller to database

Folder Structure Before and After

Current Structure (Before)

src/
├── app/
│ ├── controllers/
│ │ ├── requisitionController.js
│ │ ├── purchaseOrderController.js
│ │ └── ...
│ ├── services/
│ │ ├── requisitionService.js
│ │ ├── purchaseOrderService.js
│ │ └── ...
│ └── middlewares/
│ ├── auth.js
│ └── ...
├── infra/
│ └── database/
│ └── models/
│ ├── Requisition.js
│ ├── PurchaseOrder.js
│ └── ...
└── interfaces/
└── http/
└── routes/
├── requisitionRoutes.js
├── purchaseOrderRoutes.js
└── ...

Target Structure (After)

src/
├── domain/
│ ├── entities/
│ │ ├── Requisition.js
│ │ ├── PurchaseOrder.js
│ │ └── ...
│ ├── valueObjects/
│ │ ├── Money.js
│ │ ├── Address.js
│ │ └── ...
│ └── services/
│ ├── DomainRequisitionService.js
│ └── ...
├── app/
│ └── services/
│ ├── RequisitionService.js
│ ├── PurchaseOrderService.js
│ └── ...
├── infra/
│ ├── database/
│ │ └── models/
│ │ ├── Requisition.js
│ │ ├── PurchaseOrder.js
│ │ └── ...
│ └── repositories/
│ ├── RequisitionRepository.js
│ ├── PurchaseOrderRepository.js
│ └── ...
├── interfaces/
│ └── http/
│ ├── controllers/
│ │ ├── RequisitionController.js
│ │ ├── PurchaseOrderController.js
│ │ └── ...
│ └── routes/
│ ├── requisitionRoutes.js
│ ├── purchaseOrderRoutes.js
│ └── ...
└── shared/
├── utils/
└── middlewares/
├── auth.js
└── ...

Common Mistakes and How to Avoid Them

1. Anemic Domain Model

What It Is: Creating entities that are just data containers without behavior.

Example of the Mistake:

// Just a data container with no behavior
class Requisition {
constructor(id, requesterId, department, items, status) {
this.id \= id;
this.requesterId \= requesterId;
this.department \= department;
this.items \= items;
this.status \= status;
}

// Only getters and setters, no business logic
getStatus() { return this.status; }
setStatus(status) { this.status \= status; }
}

How to Fix It:

// Rich domain model with behavior
class Requisition {
constructor(id, requesterId, department, items \= [], status \= "DRAFT") {
this.id \= id;
this.requesterId \= requesterId;
this.department \= department;
this.items \= items;
this.status \= status;
this.approvals \= [];
}

// Business methods that enforce rules
submit() {
if (this.status !== "DRAFT") {
throw new Error("Only draft requisitions can be submitted");
}

Text Only
1
2
3
4
5
if (this.items.length \=== 0\) {  
  throw new Error("Cannot submit empty requisition");  
}

this.status \= "PENDING\_APPROVAL";

}

approve(approverId) {
if (this.status !== "PENDING_APPROVAL") {
throw new Error("Only pending requisitions can be approved");
}

Text Only
1
2
3
4
5
6
// Check if already approved  
if (this.approvals.some(a \=\> a.approverId \=== approverId)) {  
  throw new Error("Already approved by this approver");  
}

this.approvals.push({ approverId, date: new Date() });

}

isFullyApproved() {
// Business logic to determine if fully approved
return this.approvals.length >= this.requiredApprovalCount;
}
}

2. Leaky Abstractions

What It Is: Letting database concerns leak into domain logic.

Example of the Mistake:

class Requisition {
constructor(id, requesterId, department, items, status) {
this.id \= id;
this.requesterId \= requesterId;
this.department \= department;
this.items \= items;
this.status \= status;
}

// Database concerns leaking into domain logic
async save() {
if (this.id) {
await db.Requisition.update(this, { where: { id: this.id } });
} else {
const result \= await db.Requisition.create(this);
this.id \= result.id;
}
}

// More database leakage
static async findById(id) {
return await db.Requisition.findByPk(id);
}
}

How to Fix It:

// Domain entity with no database concerns
class Requisition {
constructor(id, requesterId, department, items, status) {
this.id \= id;
this.requesterId \= requesterId;
this.department \= department;
this.items \= items;
this.status \= status;
}

// Pure business logic, no database concerns
submit() {
if (this.status !== "DRAFT") {
throw new Error("Only draft requisitions can be submitted");
}
this.status \= "PENDING_APPROVAL";
}
}

// Repository handles database concerns
class RequisitionRepository {
async save(requisition) {
if (requisition.id) {
await this.db.Requisition.update(requisition, {
where: { id: requisition.id }
});
} else {
const result \= await this.db.Requisition.create(requisition);
requisition.id \= result.id;
}
return requisition;
}

async findById(id) {
return await this.db.Requisition.findByPk(id);
}
}

3. Over-Engineering

What It Is: Creating too many small classes that make the system harder to understand.

Example of the Mistake:

// Too many tiny classes
class RequisitionId {
constructor(value) {
this.value \= value;
}
}

class RequisitionStatus {
static DRAFT \= new RequisitionStatus("DRAFT");
static PENDING \= new RequisitionStatus("PENDING");
static APPROVED \= new RequisitionStatus("APPROVED");

constructor(value) {
this.value \= value;
}
}

class RequisitionItem {
constructor(id, productId, description, quantity, unitPrice) {
this.id \= id;
this.productId \= productId;
this.description \= description;
this.quantity \= new Quantity(quantity);
this.unitPrice \= new Money(unitPrice);
}
}

class Quantity {
constructor(value) {
if (value \<= 0) throw new Error("Quantity must be positive");
this.value \= value;
}
}

// And so on...

How to Fix It:

// Simpler, more pragmatic approach
class Requisition {
constructor(id, requesterId, department, items \= [], status \= "DRAFT") {
this.id \= id;
this.requesterId \= requesterId;
this.department \= department;
this.items \= items;
this.status \= status; // Simple string: "DRAFT", "PENDING", "APPROVED"
}

addItem(productId, description, quantity, unitPrice) {
if (quantity \<= 0) throw new Error("Quantity must be positive");

Text Only
1
2
3
4
5
6
this.items.push({  
  productId,  
  description,  
  quantity,  
  unitPrice: new Money(unitPrice)  
});

}

// Other business methods...
}

// Only create separate classes when they provide real value
class Money {
constructor(amount, currency \= "PHP") {
this.amount \= Math.round(amount * 100) / 100;
this.currency \= currency;
}

// Money operations...
}

Practical Implementation Plan

Week 1-2: Foundation

  1. Create Core Domain Entities
  2. Requisition
  3. PurchaseOrder
  4. Supplier
  5. Create Basic Value Objects
  6. Money
  7. Address
  8. Create First Repository
  9. RequisitionRepository
  10. Create Security Domain Model
  11. User entity
  12. Role entity
  13. Permission value object
  14. Authorization service

Week 3-4: First Feature

  1. Refactor Requisition Creation Flow
  2. Update RequisitionService
  3. Update RequisitionController
  4. Write tests
  5. Refactor Requisition Approval Flow
  6. Update approval logic
  7. Implement domain-specific access control
  8. Write tests
  9. Implement Security Infrastructure
  10. Create UserRepository
  11. Create RoleRepository
  12. Fix authorization middleware

Week 5-6: Expand to Other Domains

  1. Refactor Purchase Order Domain
  2. Create PurchaseOrder entity
  3. Create PurchaseOrderRepository
  4. Update PurchaseOrderService
  5. Implement domain-specific access control
  6. Refactor Supplier Domain
  7. Create Supplier entity
  8. Create SupplierRepository
  9. Update SupplierService
  10. Implement domain-specific access control
  11. Enhance Security Domain
  12. Implement configurable security rules
  13. Create security value objects for validation
  14. Implement cross-domain security policies

Week 7-8: Complete Implementation

  1. Refactor Remaining Domains
  2. Delivery
  3. Payment
  4. Implement domain-specific access control for each
  5. Security Testing and Auditing
  6. Test authorization rules
  7. Verify security boundaries
  8. Ensure proper access control across domains
  9. Clean Up and Testing
  10. Remove old code
  11. Ensure comprehensive test coverage
  12. Document security model

Glossary of Terms

Term Definition Example in Code
Domain The subject area to which the application applies Purchase requisition processing
Entity An object with a distinct identity that persists over time Requisition, PurchaseOrder
Value Object An object defined only by its attributes Money, Address
Aggregate A cluster of associated objects treated as a unit Requisition with its items
Aggregate Root The entry point entity to an aggregate Requisition is the root for RequisitionItem
Repository An object that provides access to aggregates RequisitionRepository
Service An operation that doesn't belong to any entity NotificationService
Ubiquitous Language A common language used by developers and domain experts Using "Requisition" instead of "req_slip"
Bounded Context A boundary within which a particular model applies Requisition context, Purchase Order context
Anemic Domain Model A model with entities that lack behavior Entities with only getters/setters
Rich Domain Model A model with entities that contain business logic Entities with methods like approve()
Security Context A bounded context focused on security concerns Authentication, authorization, and access control
Permission A specific action allowed on a specific module { module: 'dashboard', action: 'approval' }
Role A collection of permissions assigned to users Admin role, Manager role
Authorization The process of determining if a user has permission to perform an action authorizationService.authorize(userId, permissions)
Domain-Specific Access Control Access rules specific to domain entities requisition.canBeApprovedBy(userId, permissions)
Security Value Object A value object that enforces security rules Password, EncryptedData
Security Aggregate An aggregate that manages security concerns User aggregate with preferences and permissions