createEntity(config)

Creates an Entity — an object with a unique identity (id), mutable state changed only through defined actions, and computed (derived) properties.

import { z } from 'zod';
import { createEntity } from '@sotajs/ddd';

const UserSchema = z.object({
  id: z.string().uuid(),
  username: z.string(),
  bio: z.string().optional(),
  emailVerified: z.boolean().default(false),
});

const User = createEntity({
  schema: UserSchema,
  actions: {
    verifyEmail(state) {
      if (state.emailVerified) throw new Error('Already verified');
      state.emailVerified = true;
    },
  },
  computed: {
    displayName(props) { return props.username; },
  },
});
type User = ReturnType<typeof User.create>;

const user = User.create({ id: 'b3f1ae2c-abcd', username: 'alice' });
user.actions.verifyEmail();
console.log(user.displayName); // 'alice'

How does displayName work? It’s a getter defined on the entity prototype. Each computed key becomes a property — not a function call. Defined once in config, reads like native data. Recalculates from current props on every access. Source: entity.ts


Config

FieldRequiredDescription
schemaNo*Zod/Valibot schema — .parse() interface. Provides type inference + auto-setters.
typeNo*ArkType type — callable. Fastest validation path.
validateNo*Generic (data: unknown) => TProps. Use with any validator or plain function.
actionsYesNamed operations that mutate state. First argument is a mutable draft.
computedNoDerived getters. Each key becomes a property on the entity instance.

*One of schema, type, or validate is required. Pick the one that matches your validator.


Instance API

MemberTypeDescription
Entity.create(data)Entity instanceStatic factory. Validates input through schema/type/validate, returns entity.
entity.idstringThe entity’s identity.
entity.propsReadonly<TProps>Frozen snapshot of current state.
entity.actions.*(...args) => voidAction methods. Each mutates state through a shallow copy.
entity.equals(other)booleanIdentity comparison by id.

How state mutation works

When you call an action, the library:

  1. Reads current state from an internal WeakMap
  2. Creates a shallow copy ({ ...current })
  3. Passes the copy to your action function — you mutate it freely
  4. Checks invariants (only on Aggregate)
  5. Writes the copy back to the WeakMap
// Your action
verifyEmail(state) {
  if (state.emailVerified) throw new Error('Already verified');
  state.emailVerified = true; // mutating a draft copy
}
// The library swaps the draft in — atomic from your perspective

State never lives on this. It’s stored in a WeakMap keyed by the entity instance. No public fields, no accidental exposure through iteration or logging. Source: entity.ts:152-217


Why can’t I mutate props directly?

user.props.bio = 'x'; // TypeError: Cannot assign to read only property

props returns deepFreeze({ ...current }) — a recursively frozen copy. Every read creates a new snapshot. The original state is untouched. This is not a convention — it’s a physical constraint of the runtime. Source: entity.ts:112-115


Auto-setters

For every non-id field in your schema, the library generates a setField(value) method:

// Schema: { id, username, bio }
user.actions.setUsername('new-name'); // generated automatically
user.actions.setBio('new-bio');       // generated automatically

How? The library introspects schema.shape at definition time. Every key except id gets a setter. For ArkType or validate callbacks, auto-setters are discovered by calling validate({ id: "any" }) and inspecting the returned keys. Source: entity.ts:83-124


Validator-agnostic

Three equivalent ways to define the same entity:

// Zod / Valibot
createEntity({ schema: z.object({ id: z.string(), name: z.string() }), actions: {...} })

// ArkType
createEntity({ type: type({ id: 'string', name: 'string' }), actions: {...} })

// Plain function
createEntity({ validate: (data: unknown) => data as { id: string; name: string }, actions: {...} })

The library only cares that your validator has a .parse() method (schema), is callable (type), or is a function (validate). Everything else is your choice.


Pitfalls

  • Computed values are not cached. They recalculate on every access. Keep them fast.
  • State is shallow-copied during actions. If your state has nested objects, the draft shares references with the original for unchanged properties. deepFreeze on .props prevents external mutation.
  • Auto-setters skip when validate({ id: "any" }) throws. Define explicit setters in actions for strict validators.