> ## Documentation Index
> Fetch the complete documentation index at: https://woltz.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# With Prisma

> Complete guide to using Rich Domain with Prisma ORM

<Note>
  For a complete working example, see the [fastify-with-prisma example](https://github.com/tarcisioandrade/rich-domain/tree/main/examples/backend/fastify-with-prisma) in the repository.
</Note>

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npm install @woltz/rich-domain @woltz/rich-domain-prisma @prisma/client zod
  npm install -D prisma
  ```

  ```bash pnpm theme={null}
  pnpm add @woltz/rich-domain @woltz/rich-domain-prisma @prisma/client zod
  pnpm add -D prisma
  ```

  ```bash yarn theme={null}
  yarn add @woltz/rich-domain @woltz/rich-domain-prisma @prisma/client zod
  yarn add -D prisma
  ```
</CodeGroup>

## Setup Prisma

Initialize and configure Prisma:

```bash theme={null}
npx prisma init
```

Define your schema in `prisma/schema.prisma`:

```typescript theme={null}
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// Aggregate Root
model User {
  id        String   @id @default(uuid())
  email     String   @unique
  name      String
  posts     Post[]   // 1:N owned collection
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

// Owned Entity (belongs to User)
model Post {
  id        String     @id @default(uuid())
  title     String
  content   String
  published Boolean    @default(false)
  authorId  String
  author    User       @relation(fields: [authorId], references: [id], onDelete: Cascade)
  tagPosts  TagPost[]  // N:N reference collection
  createdAt DateTime   @default(now())
  updatedAt DateTime   @updatedAt
}

// Reference Entity (independent, shared across Posts)
model Tag {
  id       String    @id @default(uuid())
  name     String    @unique
  tagPosts TagPost[]
}

// Junction table for N:N (manually created)
model TagPost {
  postId String
  tagId  String
  post   Post   @relation(fields: [postId], references: [id], onDelete: Cascade)
  tag    Tag    @relation(fields: [tagId], references: [id], onDelete: Cascade)

  @@id([postId, tagId])
}
```

Generate Prisma Client:

```bash theme={null}
npx prisma generate
npx prisma db push
```

## Initialize Prisma Client

```typescript theme={null}
import { PrismaClient } from "@prisma/client";
import { PrismaUnitOfWork } from "@woltz/rich-domain-prisma";

export const prisma = new PrismaClient();
export const uow = new PrismaUnitOfWork(prisma);
```

## Define Domain Models

### Domain Entities

```typescript theme={null}
import { z } from "zod";
import { Aggregate, Entity, EntityValidation, Id } from "@woltz/rich-domain";

// Tag (Entity - independent reference, tracked by identity in N:N relations)
export class Tag extends Entity<{ id: Id }> {}

// Post (Entity - owned by User)
const postSchema = z.object({
  id: z.custom<Id>((val) => val instanceof Id),
  title: z.string().min(3),
  content: z.string(),
  published: z.boolean(),
  authorId: z.string(),
  tags: z.array(z.custom<Tag>()),
  createdAt: z.date(),
  updatedAt: z.date(),
});

type PostProps = z.infer<typeof postSchema>;

export class Post extends Entity<PostProps> {
  protected static validation: EntityValidation<PostProps> = {
    schema: postSchema
  };

  get title() {
    return this.props.title;
  }

  get content() {
    return this.props.content;
  }

  get published() {
    return this.props.published;
  }

  get authorId() {
    return this.props.authorId;
  }

  get tags() {
    return this.props.tags;
  }

  addTag(tag: Tag) {
    if (!this.props.tags.some(t => t.id.equals(tag.id))) {
      this.props.tags.push(tag);
    }
  }

  removeTag(tagId: Id) {
    const index = this.props.tags.findIndex(t => t.id.equals(tagId));
    if (index !== -1) {
      this.props.tags.splice(index, 1);
    }
  }

  publish() {
    this.props.published = true;
  }
}

// User (Aggregate Root)
const userSchema = z.object({
  id: z.custom<Id>((val) => val instanceof Id),
  email: z.string().email(),
  name: z.string().min(2),
  posts: z.array(z.custom<Post>()),
  createdAt: z.date(),
  updatedAt: z.date(),
});

type UserProps = z.infer<typeof userSchema>;

export class User extends Aggregate<UserProps> {
  protected static validation: EntityValidation<UserProps> = {
    schema: userSchema
  };

  get email() {
    return this.props.email;
  }

  get name() {
    return this.props.name;
  }

  get posts() {
    return this.props.posts;
  }

  addPost(post: Post) {
    this.props.posts.push(post);
  }

  removePost(postId: Id) {
    const index = this.props.posts.findIndex(p => p.id.equals(postId));
    if (index !== -1) {
      this.props.posts.splice(index, 1);
    }
  }

  updateName(newName: string) {
    this.props.name = newName;
  }
}
```

## Create Schema Types

```typescript theme={null}
import { Prisma } from "@prisma/client";

export type UserSchema = Prisma.UserGetPayload<{
  include: {
    posts: {
      include: {
        tagPosts: {
          include: {
            tag: true;
          };
        };
      };
    };
  };
}>;
```

## Create Mappers

### ToDomain Mapper

```typescript theme={null}
import { Mapper, Id } from "@woltz/rich-domain";
import { UserSchema } from "./schemas/user.schema";

export class UserToDomainMapper extends Mapper<UserSchema, User> {
  build(raw: UserSchema): User {
    return new User({
      id: Id.from(raw.id),
      email: raw.email,
      name: raw.name,
      posts: raw.posts.map(post =>
        new Post({
          id: Id.from(post.id),
          title: post.title,
          content: post.content,
          published: post.published,
          authorId: post.authorId,
          tags: post.tagPosts.map(tp => new Tag({ id: Id.from(tp.tag.id) })),
          createdAt: post.createdAt,
          updatedAt: post.updatedAt,
        })
      ),
      createdAt: raw.createdAt,
      updatedAt: raw.updatedAt,
    });
  }
}
```

### ToPersistence Mapper

```typescript theme={null}
import { PrismaClient } from "@prisma/client";
import { PrismaToPersistence, PrismaBatchExecutor } from "@woltz/rich-domain-prisma";
import { AggregateChanges, EntitySchemaRegistry } from "@woltz/rich-domain";

export class UserToPersistenceMapper extends PrismaToPersistence<User, PrismaClient> {
  protected readonly registry = new EntitySchemaRegistry()
    .register({
      entity: "User",
      table: "user",
      collections: {
        // 'owned': Children entities that belong exclusively to this aggregate
        // They are created/deleted with the parent
        posts: {
          type: "owned",
          entity: "Post",
        },
      },
    })
    .register({
      entity: "Post",
      table: "post",
      // Parent foreign key for owned entities
      parentFk: {
        field: "authorId",
        parentEntity: "User",
      },
      collections: {
        // Field name related to the relationship in the domain;
        // 'posts.tags' <- Domain Relation field name is 'tags'  
        tags: {
        // 'reference': Independent entities connected via junction table
        // They exist independently and are only linked/unlinked
          type: "reference",
          entity: "Tag",
          // Junction config required for manually created pivot tables
          junction: {
            table: "tagPost",    // Pivot table name
            sourceKey: "postId", // FK to Post
            targetKey: "tagId",  // FK to Tag
          },
        },
      },
    });

  protected async onCreate(user: User): Promise<void> {
    // Create aggregate root
    await this.context.user.create({
      data: {
        id: user.id.value,
        email: user.email,
        name: user.name,
        createdAt: new Date(),
        updatedAt: new Date(),
      },
    });

    // Create owned entities (Posts)
    for (const post of user.posts) {
      await this.context.post.create({
        data: {
          id: post.id.value,
          title: post.title,
          content: post.content,
          published: post.published,
          authorId: user.id.value,
          createdAt: new Date(),
          updatedAt: new Date(),
        },
      });

      // Create junction records for referenced entities (Tags)
      if (post.tags.length > 0) {
        await this.context.tagPost.createMany({
          data: post.tags.map(tag => ({
            postId: post.id.value,
            tagId: tag.id.value,
          })),
          skipDuplicates: true,
        });
      }
    }
  }

  // onUpdate uses PrismaBatchExecutor by default — override only if needed
}
```

<Note>
  **Default `onUpdate` behavior** (via `PrismaBatchExecutor`):

  * Root updates (User fields)
  * Owned entity changes (Post creates/updates/deletes)
  * Reference changes (Tag link/unlink in junction table)

  Override `onUpdate` only when you need custom persistence logic. Use [`changes.without()`](/core/change-tracking#excluding-entities) to handle one entity manually and delegate the rest to `super.onUpdate()`.
</Note>

<Note>
  **Collection Types:**

  * **`owned`**: Entity belongs to and is lifecycle-managed by the aggregate (e.g., User → Posts). Created and deleted with the parent.
  * **`reference`**: Independent entity that exists separately (e.g., Post ↔ Tags). Only the relationship is managed via junction table.

  **Junction Table:**

  * Only needed when you manually create the pivot table (like `TagPost`)
  * If using Prisma's implicit many-to-many (`@relation`), no junction config needed
</Note>

## Create Repository

```typescript theme={null}
import { PrismaRepository } from "@woltz/rich-domain-prisma";
import { WriteAndRead } from "@woltz/rich-domain";
import { Prisma } from "@prisma/client";

interface IUserRepository extends WriteAndRead<User> {
  findByEmail(email: string): Promise<User | null>;
}

export class UserRepository
  extends PrismaRepository<User, UserSchema, PrismaClient>
  implements IUserRepository
{
  constructor() {
    super(
      new UserToPersistenceMapper(prisma, uow),
      new UserToDomainMapper(),
      prisma,
      uow
    );
  }

  protected get model() {
    return "user";
  }

  protected generateSearchQuery(search: string) {
    return [
      { name: { contains: search, mode: "insensitive" } },
      { email: { contains: search, mode: "insensitive" } },
    ] satisfies Prisma.UserWhereInput[];
  }

  protected readonly includes = {
    posts: {
      include: {
        tagPosts: {
          include: {
            tag: true,
          },
        },
      },
    },
  } satisfies Prisma.UserInclude;

  async findByEmail(email: string): Promise<User | null> {
    const data = await this.context.user.findUnique({
      where: { email },
      include: this.includes,
    });

    return data ? this.toDomainMapper.build(data) : null;
  }
}
```

## Usage

```typescript theme={null}
const userRepo = new UserRepository();

// Create user with posts and tags
const user = new User({
  email: "john@example.com",
  name: "John Doe",
  posts: [],
  createdAt: new Date(),
  updatedAt: new Date(),
});

// Add post with tags
const post = Post.restore({
  title: "My First Post",
  content: "Hello World",
  published: false,
  authorId: user.id.value,
  tags: [
    new Tag({ id: Id.from("tag-1") }),
    new Tag({ id: Id.from("tag-2") }),
  ],
  createdAt: new Date(),
  updatedAt: new Date(),
});

user.addPost(post);

// Save everything in one transaction
await userRepo.save(user);
// ✅ Creates: User, Post, and junction records (TagPost)

// Update - add another tag
const foundUser = await userRepo.findByEmail("john@example.com");
foundUser!.posts[0].addTag(new Tag({ id: Id.from("tag-3") }));

await userRepo.save(foundUser!);
// ✅ Only creates new junction record for tag-3

// Update - remove a post
foundUser!.removePost(foundUser!.posts[0].id);

await userRepo.save(foundUser!);
// ✅ Deletes: Post and all its junction records (cascade)

// Query with Criteria
const criteria = Criteria.create<User>()
  .where("email", "contains", "john")
  .orderBy("name", "asc")
  .paginate(1, 10);

const results = await userRepo.find(criteria);
```

## Generate from Schema (CLI)

Auto-generate all domain code from your Prisma schema:

```bash theme={null}
npx rich-domain generate
```

This creates:

* ✅ Aggregates/Entities
* ✅ Value Objects
* ✅ Repository interfaces
* ✅ Repository implementations
* ✅ Mappers (ToDomain & ToPersistence)

<Tip>
  The CLI analyzes your Prisma schema relationships and generates proper DDD aggregates. See the [CLI documentation](/CLI) for more details.
</Tip>

## Transactional Outbox (optional)

If your aggregates emit domain events, pass an optional **`outboxStore`** as the fifth argument to `PrismaRepository`'s constructor. When set, `save()` persists uncommitted events to the outbox table in the **same transaction** as the aggregate — so events are not lost if the process crashes before `dispatchAll()`.

See **[Transactional Outbox](/integrations/outbox)** for the full setup (Prisma model, event bus decorator, background publisher). Repository wiring is documented under [Prisma Integration → Transactional Outbox](/integrations/prisma#transactional-outbox).

## Next Steps

<CardGroup cols={2}>
  <Card title="Prisma Integration" icon="database" href="/integrations/prisma">
    Deep dive into Prisma adapter features
  </Card>

  <Card title="Transactional Outbox" icon="inbox" href="/integrations/outbox">
    Guaranteed domain event delivery with the outbox pattern
  </Card>

  <Card title="Repository Pattern" icon="book" href="/repository/overview">
    Learn advanced repository patterns
  </Card>

  <Card title="Change Tracking" icon="clock-rotate-left" href="/core/change-tracking">
    Understand how changes are tracked
  </Card>

  <Card title="CLI Reference" icon="terminal" href="/CLI">
    Complete CLI documentation
  </Card>
</CardGroup>
