What is an Entity?
An Entity is a domain object defined by its identity, not its attributes. Two entities with the same attributes but different IDs are considered different objects.
import { Entity, Id } from "@woltz/rich-domain";
interface OrderItemProps {
id: Id;
productId: string;
quantity: number;
unitPrice: number;
}
class OrderItem extends Entity<OrderItemProps> {
get productId() {
return this.props.productId;
}
get quantity() {
return this.props.quantity;
}
get unitPrice() {
return this.props.unitPrice;
}
get subtotal() {
return this.props.quantity * this.props.unitPrice;
}
updateQuantity(quantity: number) {
this.props.quantity = quantity;
}
}
Entity vs Aggregate
| Feature | Entity | Aggregate |
|---|
| Has identity | ✅ | ✅ |
| Can be persisted | ✅ | ✅ |
| Consistency boundary | ❌ | ✅ |
| Accessed directly from repository | ❌ | ✅ |
| Manages child entities | ❌ | ✅ |
Use Entity for objects that live inside an Aggregate. Use Aggregate
for root objects that are loaded and saved independently.
Creating Entities
New Entity (Auto-generated ID)
const item = new OrderItem({
productId: "prod-123",
quantity: 2,
unitPrice: 49.99,
});
console.log(item.isNew()); // true
console.log(item.id.value); // "550e8400-..." (auto-generated UUID)
Existing Entity (From Database)
const item = new OrderItem({
id: Id.from("item-456"), // marks as existing
productId: "prod-123",
quantity: 2,
unitPrice: 49.99,
});
console.log(item.isNew()); // false
Identity & Equality
Entities are compared by their ID, not their attributes:
const item1 = new OrderItem({
id: Id.from("item-1"),
productId: "prod-123",
quantity: 2,
unitPrice: 49.99,
});
const item2 = new OrderItem({
id: Id.from("item-1"),
productId: "prod-999", // different product
quantity: 10, // different quantity
unitPrice: 99.99, // different price
});
const item3 = new OrderItem({
id: Id.from("item-2"), // different ID
productId: "prod-123",
quantity: 2,
unitPrice: 49.99,
});
item1.equals(item2); // true - same ID
item1.equals(item3); // false - different ID
item1.equals("item-1"); // true - can compare with string
item1.equals(Id.from("item-1")); // true - can compare with Id
Adding Validation
Entities support the same validation as Aggregates:
import { z } from "zod";
import { Entity, EntityValidation, Id } from "@woltz/rich-domain";
const orderItemSchema = z.object({
id: z.custom<Id>((val) => val instanceof Id),
productId: z.string().min(1, "Product ID is required"),
quantity: z.number().int().positive("Quantity must be positive"),
unitPrice: z.number().positive("Price must be positive"),
});
type OrderItemProps = z.infer<typeof orderItemSchema>;
class OrderItem extends Entity<OrderItemProps> {
protected static validation: EntityValidation<OrderItemProps> = {
schema: orderItemSchema,
config: {
onCreate: true,
onUpdate: true,
throwOnError: true,
},
};
// ... getters and methods
}
Serialization
Convert entities to plain objects for APIs or persistence:
const item = new OrderItem({
id: Id.from("item-123"),
productId: "prod-456",
quantity: 3,
unitPrice: 29.99,
});
const json = item.toJSON();
// {
// id: "item-123",
// productId: "prod-456",
// quantity: 3,
// unitPrice: 29.99
// }
Working with Collections
Entities are commonly used inside Aggregates as collections:
class Order extends Aggregate<OrderProps> {
get items() {
return this.props.items;
}
addItem(productId: string, quantity: number, unitPrice: number) {
const item = new OrderItem({ productId, quantity, unitPrice });
this.props.items.push(item);
}
removeItem(itemId: Id) {
this.props.items = this.props.items.filter(
(item) => !item.id.equals(itemId)
);
}
findItem(itemId: Id): OrderItem | undefined {
return this.props.items.find((item) => item.id.equals(itemId));
}
get total() {
return this.props.items.reduce((sum, item) => sum + item.subtotal, 0);
}
}