Skip to main content

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

FeatureEntityAggregate
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);
  }
}