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¶
- Business Logic Scattered Everywhere
- Example: Approval rules are duplicated in controllers, services, and even UI components
- Location: src/app/controllers/requisitionController.js, src/app/services/requisitionService.js, src/features/requisition/components/ApprovalButton.jsx
- Inconsistent Terminology
- Example: We use req, requisition, and request to refer to the same concept
- Location: Variable names throughout the codebase
- No Clear Boundaries Between Components
- Example: Purchase Order service directly modifies Requisition data
- Location: src/app/services/purchaseOrderService.js line 120-145
- Database-Driven Design
- Example: Our entities are just database models with no business behavior
- Location: src/infra/database/models/ directory
- Hard to Test
- Example: Business logic is tightly coupled with database access
- Location: Most service classes
Benefits DDD Will Bring to Our Project¶
- Clearer Code Organization
- All code related to requisitions will be in one place
- All code related to purchase orders will be in another place
- Consistent Terminology
- We'll use the same terms throughout the code that business users use
- Explicit Business Rules
- Business rules will be clearly defined in domain entities
- No more hunting through the codebase to find all the places a rule is implemented
- Easier Testing
- Business logic can be tested without database or UI dependencies
- Easier Onboarding for New Developers
- 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:
- Requisition Context: Everything related to creating and approving requisitions
- Canvass Context: Everything related to supplier quotations and selection
- Purchase Order Context: Everything related to creating and managing purchase orders
- Delivery Context: Everything related to receiving goods
- 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:
- 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
- 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
}
- 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:
- 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
- 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
- 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?
- Prevent Bugs: Proper handling of special types like money, dates, and quantities
- Add Behavior: Methods that make sense for that type of value
- Self-Validation: Ensures the data is always valid
- 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 | |
}
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 | |
}
}
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:
- Requisition Aggregate:
- Root: Requisition
- Members: RequisitionItem, RequisitionApproval
- Purchase Order Aggregate:
- Root: PurchaseOrder
- Members: PurchaseOrderItem, PurchaseOrderApproval
- Supplier Aggregate:
- Root: Supplier
- Members: SupplierContact, SupplierAddress
- Delivery Aggregate:
- Root: DeliveryReceipt
- 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 | |
}
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 | |
}
// Helper methods to convert between domain and database models
_toDomainEntity(data) { /* ... */ }
_toDataModel(entity) { /* ... */ }
}
Repositories We Need to Create:
- RequisitionRepository
- PurchaseOrderRepository
- SupplierRepository
- DeliveryReceiptRepository
- 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 | |
}
}
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 | |
}
}
Application Services We Need to Create:
- RequisitionService - For creating, submitting, and approving requisitions
- CanvassService - For managing supplier quotations
- PurchaseOrderService - For creating and managing purchase orders
- DeliveryService - For recording deliveries
- 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 | |
}
}
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:
-
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;
}
} -
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}`;
}
} -
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:
- User Aggregate
- Root: User
- Members: UserPreferences, UserLeaves
- Role Aggregate
- Root: Role
- 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 | |
}
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 | |
}
}
// 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 | |
};
};
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 | |
}
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 | |
}
}
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 | |
}
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 | |
}
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 | |
}
}
// 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 | |
}
}
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 | |
}
}
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 | |
}
}
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 | |
}
}
Step-by-Step Refactoring Guide¶
Phase 1: Preparation¶
- Identify Domain Experts
- Talk to business users who understand the purchase requisition process
- Document the key terms they use
- Map Current Codebase
- Identify where business logic currently lives
- Document current database schema
- List all current services and their responsibilities
- Create Glossary of Terms
- Document all business terms and their meanings
- Identify inconsistencies in terminology
Phase 2: Create Domain Model¶
- Create Domain Entities
- Start with Requisition.js in src/domain/entities/
- Add business methods like submit(), approve(), etc.
- Move validation logic into entities
- Create Value Objects
- Create Money.js in src/domain/valueObjects/
- Create other value objects like Address.js, Quantity.js
- Define Aggregates
- Identify which entities form aggregates
- Ensure all access to child entities goes through the root
Phase 3: Create Infrastructure Layer¶
- Create Repositories
- Create RequisitionRepository.js in src/infra/repositories/
- Implement methods to convert between domain entities and database models
- Create Database Models
- Keep existing database models in src/infra/database/models/
- These will be used by repositories
Phase 4: Create Application Services¶
- Refactor Services
- Create new service classes that use repositories and domain entities
- Move business logic from old services to domain entities
- Create Supporting Services
- Create services like NotificationService for cross-cutting concerns
Phase 5: Update Controllers¶
- Refactor Controllers
- Update controllers to use new application services
- Remove any business logic from controllers
Phase 6: Testing and Validation¶
- Write Unit Tests
- Test domain entities in isolation
- Test repositories with database mocks
- Write Integration Tests
- 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 | |
}
approve(approverId) {
if (this.status !== "PENDING_APPROVAL") {
throw new Error("Only pending requisitions can be approved");
}
| Text Only | |
|---|---|
1 2 3 4 5 6 | |
}
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 | |
}
// 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¶
- Create Core Domain Entities
- Requisition
- PurchaseOrder
- Supplier
- Create Basic Value Objects
- Money
- Address
- Create First Repository
- RequisitionRepository
- Create Security Domain Model
- User entity
- Role entity
- Permission value object
- Authorization service
Week 3-4: First Feature¶
- Refactor Requisition Creation Flow
- Update RequisitionService
- Update RequisitionController
- Write tests
- Refactor Requisition Approval Flow
- Update approval logic
- Implement domain-specific access control
- Write tests
- Implement Security Infrastructure
- Create UserRepository
- Create RoleRepository
- Fix authorization middleware
Week 5-6: Expand to Other Domains¶
- Refactor Purchase Order Domain
- Create PurchaseOrder entity
- Create PurchaseOrderRepository
- Update PurchaseOrderService
- Implement domain-specific access control
- Refactor Supplier Domain
- Create Supplier entity
- Create SupplierRepository
- Update SupplierService
- Implement domain-specific access control
- Enhance Security Domain
- Implement configurable security rules
- Create security value objects for validation
- Implement cross-domain security policies
Week 7-8: Complete Implementation¶
- Refactor Remaining Domains
- Delivery
- Payment
- Implement domain-specific access control for each
- Security Testing and Auditing
- Test authorization rules
- Verify security boundaries
- Ensure proper access control across domains
- Clean Up and Testing
- Remove old code
- Ensure comprehensive test coverage
- 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 |