Rich Domain Library with Standard Schema validation support
npm install @woltz/rich-domainA TypeScript library for Domain-Driven Design with Standard Schema validation, automatic change tracking, and enterprise-ready repositories.


- 🎯 Type-Safe DDD Building Blocks - Entities, Aggregates, Value Objects with full TypeScript support
- ✅ Validation Agnostic - Works with Zod, Valibot, ArkType, or any Standard Schema compatible library
- 🔄 Automatic Change Tracking - Track changes across nested entities and collections without boilerplate
- 🗄️ ORM Independent - Use with Prisma, TypeORM, Drizzle, or any persistence layer
- 🔍 Rich Query API - Type-safe Criteria pattern with fluent API for complex queries
- 📦 Repository Pattern - Abstract your persistence layer with built-in pagination and filtering
- 🎭 Domain Events - Built-in event system for cross-aggregate communication
- 🔐 Lifecycle Hooks - onCreate, onBeforeUpdate, and business rule validation
- 🪝 React Integration - Ready-to-use hooks and components via @woltz/react-rich-domain
``bash
npm install @woltz/rich-domain
Quick Start
$3
`typescript
import { Aggregate, Entity, Id } from "@woltz/rich-domain";
import { z } from "zod";// Value Object
class Email extends ValueObject {
protected static validation = {
schema: z.string().email("Invalid email format"),
};
getDomain(): string {
return this.value.split("@")[1];
}
}
// Entity (child of Aggregate)
const PostSchema = z.object({
id: z.custom(),
title: z.string().min(3),
content: z.string(),
published: z.boolean(),
createdAt: z.date(),
});
class Post extends Entity> {
protected static validation = { schema: PostSchema };
publish(): void {
this.props.published = true;
}
get title() {
return this.props.title;
}
}
// Aggregate Root
const UserSchema = z.object({
id: z.custom(),
email: z.custom(),
name: z.string(),
posts: z.array(z.instanceof(Post)),
createdAt: z.date(),
updatedAt: z.date(),
});
class User extends Aggregate> {
protected static validation = { schema: UserSchema };
addPost(title: string, content: string): void {
const post = new Post({
title,
content,
published: false,
createdAt: new Date(),
});
this.props.posts.push(post);
}
get email() {
return this.props.email.value;
}
get posts() {
return this.props.posts;
}
}
`$3
Changes are tracked automatically - no manual tracking needed:
`typescript
// Load existing user
const user = new User({
id: Id.from("user-123"),
email: new Email("john@example.com"),
name: "John Doe",
posts: [
new Post({
id: Id.from("post-1"),
title: "First Post",
content: "Content here",
published: false,
createdAt: new Date(),
}),
],
createdAt: new Date(),
updatedAt: new Date(),
});// Make changes
user.addPost("Second Post", "More content"); // Create
user.posts[0].publish(); // Update
user.posts.splice(0, 1); // Delete
// Get all changes automatically organized
const changes = user.getChanges();
console.log(changes.hasCreates()); // true
console.log(changes.hasUpdates()); // true
console.log(changes.hasDeletes()); // true
// Changes are organized by depth for proper FK handling
const batch = changes.toBatchOperations();
// {
// deletes: [{ entity: "Post", depth: 1, ids: ["post-1"] }],
// creates: [{ entity: "Post", depth: 1, items: [...] }],
// updates: [{ entity: "Post", depth: 1, items: [...] }]
// }
`$3
Build complex queries with full type safety:
`typescript
import { Criteria } from "@woltz/rich-domain";// Simple query
const activePosts = Criteria.create()
.where("published", "equals", true)
.orderBy("createdAt", "desc")
.limit(10);
// Complex query with multiple filters
const criteria = Criteria.create()
.where("name", "contains", "John")
.where("email", "startsWith", "john")
.where("createdAt", "greaterThan", new Date("2024-01-01"))
.orderBy("name", "asc")
.paginate(1, 20);
// Use with repository
const result = await userRepository.find(criteria);
// result: PaginatedResult
console.log(result.data); // User[]
console.log(result.meta); // { page: 1, pageSize: 20, total: 100, totalPages: 5 }
`$3
Abstract your persistence layer:
`typescript
import { IRepository, Criteria } from "@woltz/rich-domain";interface IUserRepository extends IRepository {
findByEmail(email: string): Promise;
findActiveUsers(): Promise;
}
class UserRepository implements IUserRepository {
async save(user: User): Promise {
// Your persistence logic
const changes = user.getChanges();
// Handle deletes (deepest first)
for (const deletion of changes.toBatchOperations().deletes) {
// Delete by entity and IDs
}
// Handle creates (root first)
for (const creation of changes.toBatchOperations().creates) {
// Create new entities
}
// Handle updates
for (const update of changes.toBatchOperations().updates) {
// Update only changed fields
}
}
async findById(id: string): Promise {
// Your query logic
}
async find(criteria: Criteria): Promise> {
// Transform criteria to your ORM query
const filters = criteria.getFilters();
const ordering = criteria.getOrdering();
const pagination = criteria.getPagination();
// Execute query and return paginated result
}
async findByEmail(email: string): Promise {
const criteria = Criteria.create().where("email", "equals", email);
const result = await this.find(criteria);
return result.data[0] ?? null;
}
}
`Advanced Features
$3
Add validation and side effects at key points:
`typescript
class Product extends Aggregate {
protected static validation = {
schema: ProductSchema,
}; protected static hooks = {
onBeforeCreate: (props) => {
// Set default values before validation
if (!props.createdAt) {
props.createdAt = new Date();
}
},
onCreate: (entity) => {
console.log(
Product created: ${entity.name});
}, onBeforeUpdate: (entity, snapshot) => {
// Prevent price changes on inactive products
if (snapshot.status === "inactive" && entity.price !== snapshot.price) {
return false; // Reject the change
}
return true;
},
rules: (entity) => {
if (entity.price > 1000 && entity.stock === 0) {
throw new ValidationError([
{
path: ["stock"],
message: "Premium products must have stock available",
},
]);
}
},
};
}
`$3
Make properties optional at construction but required in the entity:
`typescript
const userSchema = z.object({
id: z.custom(),
email: z.string().email(),
password: z.string().min(8), // Required in entity
createdAt: z.date(), // Required in entity
});type UserProps = z.infer;
// Second generic makes 'password' and 'createdAt' optional at input
class User extends Aggregate {
protected static validation = { schema: userSchema };
protected static hooks = {
onBeforeCreate: (props) => {
// Generate values before validation
if (!props.password) {
props.password = generateEncryptedPassword();
}
if (!props.createdAt) {
props.createdAt = new Date();
}
},
};
get email() {
return this.props.email;
}
}
// ✅ Works without password and createdAt
const user = new User({
email: "user@example.com",
});
// ✅ Also works with explicit values
const customUser = new User({
email: "user@example.com",
password: "custom-pass-12345678",
});
`$3
Communicate across aggregate boundaries:
`typescript
import { DomainEvent } from "@woltz/rich-domain";class OrderConfirmedEvent extends DomainEvent {
constructor(
aggregateId: Id,
public readonly customerId: string,
public readonly total: number
) {
super(aggregateId);
}
protected getPayload() {
return { customerId: this.customerId, total: this.total };
}
}
class Order extends Aggregate {
confirm(): void {
if (this.props.items.length === 0) {
throw new DomainError("Cannot confirm empty order");
}
this.props.status = "confirmed";
// Emit event
this.addDomainEvent(
new OrderConfirmedEvent(this.id, this.customerId, this.total)
);
}
}
// After saving
await orderRepository.save(order);
await order.dispatchAll(eventBus);
order.clearEvents();
`$3
Immutable wrappers for primitive values with domain behavior:
`typescript
import { ValueObject, VOHooks, throwValidationError } from "@woltz/rich-domain";class Price extends ValueObject {
protected static validation = {
schema: z.number().positive("Price must be positive"),
};
protected static hooks: VOHooks = {
rules: (price) => {
if (price.value > 1000000) {
throwValidationError("value", "Price cannot exceed 1,000,000");
}
},
};
addTax(taxRate: number): Price {
return this.clone(this.value * (1 + taxRate));
}
discount(percentage: number): Price {
return this.clone(this.value * (1 - percentage / 100));
}
format(currency: string = "USD"): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency,
}).format(this.value);
}
}
const price = new Price(99.99);
const withTax = price.addTax(0.08);
const discounted = price.discount(10);
console.log(price.format()); // "$99.99"
console.log(withTax.format()); // "$107.99"
console.log(discounted.format()); // "$89.99"
`Integration with ORMs
Rich Domain provides official adapters for popular ORMs:
$3
`bash
npm install @woltz/rich-domain-prisma
``typescript
import {
PrismaRepository,
PrismaToPersistence,
} from "@woltz/rich-domain-prisma";class UserToPersistence extends PrismaToPersistence {
protected readonly registry = schemaRegistry;
protected async onCreate(user: User): Promise {
await this.context.user.create({
data: {
id: user.id.value,
email: user.email,
name: user.name,
},
});
}
protected async onUpdate(
user: User,
changes: AggregateChanges
): Promise {
// Automatic batch operations handling
}
}
`$3
`bash
npm install @woltz/rich-domain-typeorm
``typescript
import { TypeORMRepository } from "@woltz/rich-domain-typeorm";class UserRepository extends TypeORMRepository {
// Automatic change tracking and batch operations
}
`CLI Tool
Bootstrap projects and generate domain code:
`bash
npm install -g @woltz/rich-domain-cliInitialize new project
rich-domain init my-app --template fullstackGenerate domain from Prisma schema
rich-domain generate --schema prisma/schema.prismaAdd entity manually
rich-domain add User name:string email:string --with-repo
`API Reference
$3
####
IdUnique identifier for entities:
`typescript
const id = Id.create(); // Generate new UUID
const existingId = Id.from("uuid-string"); // From existing valueconsole.log(id.value); // string
console.log(id.isNew); // boolean
console.log(id.equals(otherId)); // boolean
`####
EntityBase class for entities:
`typescript
abstract class Entity {
get id(): Id;
get isNew(): boolean;
equals(other: Entity): boolean;
toJSON(): object;
}
`####
AggregateRoot entity with change tracking:
`typescript
abstract class Aggregate extends Entity {
getChanges(): AggregateChanges; // Domain Events
protected addDomainEvent(event: IDomainEvent): void;
getUncommittedEvents(): IDomainEvent[];
clearEvents(): void;
dispatchAll(bus: DomainEventBus): Promise;
}
`####
ValueObjectImmutable object compared by value:
`typescript
abstract class ValueObject {
protected readonly props: T; equals(other: ValueObject): boolean;
toJSON(): T;
protected clone(updates: Partial): this;
}
`$3
`typescript
class Criteria {
static create(): Criteria; // Filters
where>(
field: K,
operator: FilterOperator,
value: any
): this;
// Ordering
orderBy>(field: K, direction: "asc" | "desc"): this;
// Pagination
limit(limit: number): this;
offset(offset: number): this;
paginate(page: number, pageSize: number): this;
// Search
search(term: string): this;
// Getters
getFilters(): Filter[];
getOrdering(): Order | null;
getPagination(): Pagination | null;
getSearch(): string | null;
}
`$3
Rich Domain provides comprehensive exception types:
`typescript
import {
ValidationError,
DomainError,
EntityNotFoundError,
DuplicateEntityError,
ConcurrencyError,
RepositoryError,
} from "@woltz/rich-domain";try {
const user = new User({
/ invalid props /
});
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.entity); // "User"
console.log(error.field); // "email"
console.log(error.message); // "Invalid email format"
}
}
`Package Format
This library is published as a dual package supporting both CommonJS and ES Modules:
`javascript
// CommonJS
const { Id, Entity, Aggregate } = require("@woltz/rich-domain");// ES Modules
import { Id, Entity, Aggregate } from "@woltz/rich-domain";
``Benefits:
- ✅ Universal compatibility (Node.js, Vite, Webpack, etc.)
- ✅ Tree-shaking support for modern bundlers
- ✅ Full TypeScript support with type definitions
- ✅ Zero configuration - automatically uses the correct format
- 📚 Full Documentation
- 🚀 Quick Start Guide
- 📖 Core Concepts
- 🔌 Integrations
- ⚛️ React Components
Check out the examples directory for complete implementations:
- Basic CRUD operations
- Complex aggregate relationships
- Custom validation rules
- Domain events
- Repository implementations for different ORMs
| Package | Description | Version |
| ------------------------------------------------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| @woltz/rich-domain | Core library |  |
| @woltz/rich-domain-prisma | Prisma adapter |  |
| @woltz/rich-domain-typeorm | TypeORM adapter |  |
| @woltz/rich-domain-criteria-zod | Zod criteria builder |  |
| @woltz/rich-domain-cli | CLI tool |  |
Contributions are welcome! Please read our Contributing Guide for details.
MIT © Tarcisio Andrade
- Documentation
- GitHub Repository
- npm Package
- Issues
- Changelog