Domain Model Implementation
A rich domain model is at the heart of Domain-Driven Design. It encapsulates both data and behavior, ensuring that business rules are enforced consistently throughout the application.
Current State
The current domain model is anemic:
- Domain entities are primarily data validation schemas
- Business logic is scattered across services
- Entities lack behavior and methods
- No clear aggregates or value objects
Implementing Rich Domain Models
Entity Structure
A rich domain entity should:
- Encapsulate both state and behavior
- Enforce invariants
- Expose methods that represent business operations
- Hide implementation details
Example: Requisition Entity
Here's how we can transform the current anemic Requisition entity into a rich domain model:
| JavaScript |
|---|
| // src/domain/requisitionManagement/entities/Requisition.js
const RequisitionStatus = require('../valueObjects/RequisitionStatus');
const RequisitionSubmittedEvent = require('../events/RequisitionSubmittedEvent');
const RequisitionApprovedEvent = require('../events/RequisitionApprovedEvent');
const RequisitionRejectedEvent = require('../events/RequisitionRejectedEvent');
class Requisition {
constructor(props) {
this.id = props.id;
this.rsNumber = props.rsNumber;
this.rsLetter = props.rsLetter;
this.companyCode = props.companyCode;
this.createdBy = props.createdBy;
this.status = new RequisitionStatus(props.status || 'DRAFT');
this.items = props.items || [];
this.approvers = props.approvers || [];
this.dateRequired = props.dateRequired;
this.deliveryAddress = props.deliveryAddress;
this.purpose = props.purpose;
this.type = props.type;
// Private properties
this._validateState();
}
// Domain behavior
addItem(item) {
this.items.push(item);
}
removeItem(itemId) {
this.items = this.items.filter(item => item.id !== itemId);
}
submit() {
if (this.status.value !== 'DRAFT') {
throw new Error('Only draft requisitions can be submitted');
}
if (this.items.length === 0) {
throw new Error('Cannot submit requisition without items');
}
this.status = new RequisitionStatus('SUBMITTED');
return new RequisitionSubmittedEvent(this);
}
approve(approverId) {
if (this.status.value !== 'SUBMITTED' && this.status.value !== 'PARTIALLY_APPROVED') {
throw new Error('Only submitted or partially approved requisitions can be approved');
}
const approver = this.approvers.find(a => a.id === approverId);
if (!approver) {
throw new Error('Approver not found');
}
if (approver.status === 'APPROVED') {
throw new Error('Approver has already approved this requisition');
}
approver.status = 'APPROVED';
// Check if all approvers have approved
const allApproved = this.approvers.every(a => a.status === 'APPROVED');
if (allApproved) {
this.status = new RequisitionStatus('APPROVED');
return new RequisitionApprovedEvent(this);
} else {
this.status = new RequisitionStatus('PARTIALLY_APPROVED');
}
return null;
}
reject(approverId, reason) {
if (this.status.value !== 'SUBMITTED' && this.status.value !== 'PARTIALLY_APPROVED') {
throw new Error('Only submitted or partially approved requisitions can be rejected');
}
const approver = this.approvers.find(a => a.id === approverId);
if (!approver) {
throw new Error('Approver not found');
}
approver.status = 'REJECTED';
this.status = new RequisitionStatus('REJECTED');
return new RequisitionRejectedEvent(this, reason);
}
// Private methods
_validateState() {
if (this.type !== 'ofm' &&
this.type !== 'non-ofm' &&
this.type !== 'ofm-tom' &&
this.type !== 'non-ofm-tom' &&
this.type !== 'transfer') {
throw new Error('Invalid requisition type');
}
}
// Factory methods
static create(props) {
return new Requisition({
...props,
status: 'DRAFT'
});
}
static reconstitute(props) {
return new Requisition(props);
}
}
module.exports = Requisition;
|
Value Objects
Value objects represent concepts that are defined by their attributes rather than an identity:
| JavaScript |
|---|
| // src/domain/sharedKernel/valueObjects/Money.js
class Money {
constructor(amount, currency = 'PHP') {
this._amount = amount;
this._currency = currency;
this._validate();
}
get amount() {
return this._amount;
}
get currency() {
return this._currency;
}
add(money) {
if (this._currency !== money.currency) {
throw new Error('Cannot add different currencies');
}
return new Money(this._amount + money.amount, this._currency);
}
subtract(money) {
if (this._currency !== money.currency) {
throw new Error('Cannot subtract different currencies');
}
return new Money(this._amount - money.amount, this._currency);
}
equals(money) {
return this._amount === money.amount && this._currency === money.currency;
}
toString() {
return `${this._currency} ${this._amount.toFixed(2)}`;
}
_validate() {
if (isNaN(this._amount)) {
throw new Error('Amount must be a number');
}
if (this._amount < 0) {
throw new Error('Amount cannot be negative');
}
}
}
module.exports = Money;
|
| JavaScript |
|---|
| // src/domain/requisitionManagement/valueObjects/RequisitionStatus.js
class RequisitionStatus {
static DRAFT = 'DRAFT';
static SUBMITTED = 'SUBMITTED';
static PARTIALLY_APPROVED = 'PARTIALLY_APPROVED';
static APPROVED = 'APPROVED';
static REJECTED = 'REJECTED';
static CANVASSING = 'CANVASSING';
static ORDERED = 'ORDERED';
static DELIVERED = 'DELIVERED';
static COMPLETED = 'COMPLETED';
static CANCELLED = 'CANCELLED';
constructor(value) {
const validStatuses = [
RequisitionStatus.DRAFT,
RequisitionStatus.SUBMITTED,
RequisitionStatus.PARTIALLY_APPROVED,
RequisitionStatus.APPROVED,
RequisitionStatus.REJECTED,
RequisitionStatus.CANVASSING,
RequisitionStatus.ORDERED,
RequisitionStatus.DELIVERED,
RequisitionStatus.COMPLETED,
RequisitionStatus.CANCELLED
];
if (!validStatuses.includes(value)) {
throw new Error(`Invalid requisition status: ${value}`);
}
this._value = value;
}
get value() {
return this._value;
}
equals(status) {
return this._value === status.value;
}
canTransitionTo(newStatus) {
// Define valid state transitions
const validTransitions = {
[RequisitionStatus.DRAFT]: [RequisitionStatus.SUBMITTED],
[RequisitionStatus.SUBMITTED]: [
RequisitionStatus.PARTIALLY_APPROVED,
RequisitionStatus.APPROVED,
RequisitionStatus.REJECTED
],
[RequisitionStatus.PARTIALLY_APPROVED]: [
RequisitionStatus.APPROVED,
RequisitionStatus.REJECTED
],
[RequisitionStatus.APPROVED]: [RequisitionStatus.CANVASSING],
[RequisitionStatus.CANVASSING]: [RequisitionStatus.ORDERED],
[RequisitionStatus.ORDERED]: [RequisitionStatus.DELIVERED],
[RequisitionStatus.DELIVERED]: [RequisitionStatus.COMPLETED],
// Any status can be cancelled
};
if (newStatus.value === RequisitionStatus.CANCELLED) {
return true;
}
return validTransitions[this._value]?.includes(newStatus.value) || false;
}
}
module.exports = RequisitionStatus;
|