It is one of the building blocks in Domain Model Pattern. They are very similar to value objects but what makes them different is that they require an identifier to distinguish different instance of the entity and that they are mutable (apart from their identifier as this should remain immutable through the lifecycle of entity). Entities can be composed of value objects as values.
Example
class Money {
private readonly amount: number;
private readonly currency: string;
constructor(amount: number, currency: string) {
if (amount < 0) {
throw new Error('Amount cannot be negative');
}
if (currency.length !== 3) {
throw new Error('Currency must be a 3-letter ISO code');
}
this.amount = amount;
this.currency = currency.toUpperCase();
}
public getAmount(): number {
return this.amount;
}
public getCurrency(): string {
return this.currency;
}
public equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
public toString(): string {
return `${this.amount.toFixed(2)} ${this.currency}`;
}
}
class ProductId {
private readonly value: string;
constructor(value: string) {
if (!value.match(/^PROD-\d{6}$/)) {
throw new Error('Invalid product ID format. Must be PROD-XXXXXX where X is a digit.');
}
this.value = value;
}
public getValue(): string {
return this.value;
}
public equals(other: ProductId): boolean {
return this.value === other.value;
}
public toString(): string {
return this.value;
}
}
class Product {
private readonly id: ProductId;
private name: string;
private price: Money;
private stockQuantity: number;
constructor(id: ProductId, name: string, price: Money, stockQuantity: number) {
this.id = id;
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
}
public getId(): ProductId {
return this.id;
}
public getName(): string {
return this.name;
}
public setName(name: string): void {
this.name = name;
}
public getPrice(): Money {
return this.price;
}
public setPrice(price: Money): void {
this.price = price;
}
public getStockQuantity(): number {
return this.stockQuantity;
}
public addStock(quantity: number): void {
if (quantity < 0) {
throw new Error('Quantity to add must be positive');
}
this.stockQuantity += quantity;
}
public removeStock(quantity: number): void {
if (quantity < 0) {
throw new Error('Quantity to remove must be positive');
}
if (this.stockQuantity < quantity) {
throw new Error('Not enough stock');
}
this.stockQuantity -= quantity;
}
public equals(other: Product): boolean {
return this.id.equals(other.id);
}
}
// Creating a product
const productId = new ProductId('PROD-123456');
const productPrice = new Money(29.99, 'USD');
const product = new Product(productId, 'Fancy Gadget', productPrice, 100);
// Using the product
console.log(product.getName()); // Output: Fancy Gadget
console.log(product.getPrice().toString()); // Output: 29.99 USD
console.log(product.getStockQuantity()); // Output: 100
// Modifying the product
product.setName('Super Fancy Gadget');
product.setPrice(new Money(39.99, 'USD'));
product.removeStock(10);
console.log(product.getName()); // Output: Super Fancy Gadget
console.log(product.getPrice().toString()); // Output: 39.99 USD
console.log(product.getStockQuantity()); // Output: 90