Domain modeling

The @beignet/core/domain subpath provides small helpers for domain-driven design: entities and value objects.

These helpers are optional. Plain TypeScript objects and functions are fine domain code; reach for the helpers when validation or immutable-update patterns start repeating across a feature.

bun add @beignet/core

Placement

Put domain code with the feature that owns the business concept:

features/posts/domain/post.ts
features/posts/domain/events/published.ts
features/comments/domain/events/comment-added.ts

Use features/shared/domain/ only for true shared-kernel concepts that are genuinely used by multiple features, such as EmailAddress, Money, or TenantId. Do not put feature-specific domain code there.

Value objects

Immutable, validated types that represent a concept with no identity (e.g. an email address, a currency amount):

import { defineValueObject } from "@beignet/core/domain";
import { z } from "zod";

const Email = defineValueObject("Email")
  .schema(z.string().email())
  .build();

const email = await Email.create("user@example.com"); // validated string
await Email.isValid("not-an-email"); // false

Entities

Domain objects with identity and behavior. Entities are immutable — methods return new instances:

import { defineEntity } from "@beignet/core/domain";
import { z } from "zod";

const Todo = defineEntity("Todo")
  .props(z.object({
    id: z.string(),
    title: z.string(),
    completed: z.boolean(),
  }))
  .methods((self) => ({
    complete: () => self.with({ completed: true }),
    rename: (title: string) => self.with({ title }),
  }))
  .build();

const todo = await Todo.create({ id: "1", title: "Buy milk", completed: false });
const done = await todo.complete(); // new instance with completed: true

Every entity gets a .with() method for partial updates, returning a new instance.

Domain events

Feature event declarations live in features/<feature>/domain/events/, but the event APIs are owned elsewhere: declare events with defineEvent(...) from Events, and emit them from use cases with .emits(...) as shown in Use cases.

Schema libraries

Both helpers work with any Standard Schema library — Zod, Valibot, ArkType, etc.