npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rineex/ddd

v6.2.1

Published

Domain Driven Design package for Rineex core modules

Readme

@rineex/ddd

Domain-Driven Design (DDD) primitives for building maintainable, scalable TypeScript applications.

npm version License: Apache-2.0 TypeScript

Table of Contents


Overview

@rineex/ddd provides type-safe building blocks for implementing Domain-Driven Design patterns. Used by @rineex/auth-core and other Rineex packages.

Features: Value Objects, Entities, Aggregate Roots, Domain Events, Domain Errors (extensible namespaces), Result type, Application Service port, Clock port, HTTP status constants.


Installation

pnpm add @rineex/ddd

Requirements: Node.js 18+, TypeScript 5.0+, ES2020+ target


Package Exports

import {
  ValueObject,
  PrimitiveValueObject,
  Entity,
  AggregateRoot,
  DomainEvent,
  AggregateId,
  DomainID,
  Email,
  IPAddress,
  DomainError,
  InferErrorCodes,
  registryErrorCodes,
  CoreDomainErrorRegistry,
  BaseMapper,
  InvalidValueObjectError,
  EntityValidationError,
  InvalidValueError,
  InvalidStateError,
  InternalError,
  TimeoutError,
  ApplicationError,
  ApplicationServicePort,
  Result,
  ClockPort,
  EntityId,
  EntityProps,
  DomainEventPayload,
  CreateEventProps,
  UnixTimestampMillis,
  HttpStatus,
  HttpStatusMessage,
  deepFreeze,
} from '@rineex/ddd';

Value Objects

Value objects are immutable and defined by attributes. Use ValueObject<T> for composite structures. Props are deep-frozen in the constructor.

Example (from vo.spec.ts)

import { ValueObject, InvalidValueObjectError } from '@rineex/ddd';

class TestValueObject extends ValueObject<{ name: string; age: number }> {
  constructor(props: { name: string; age: number }) {
    super(props);
  }

  protected validate(props: { name: string; age: number }): void {
    if (!props.name?.trim()) {
      throw InvalidValueObjectError.create('Name is required');
    }
    if (props.age < 0 || props.age > 150) {
      throw InvalidValueObjectError.create('Age must be between 0 and 150');
    }
  }
}

// Usage
const vo = new TestValueObject({ name: 'John', age: 30 });
vo.value; // { name: 'John', age: 30 }
vo.equals(other); // deep equality
vo.toJSON(); // returns props
vo.toString(); // JSON.stringify(props)
ValueObject.is(vo); // type guard

Simple Value Object (wraps a single value)

class SimpleValueObject extends ValueObject<string> {
  constructor(value: string) {
    super(value);
  }

  protected validate(value: string): void {
    if (!value?.length) {
      throw InvalidValueObjectError.create('Value cannot be empty');
    }
  }
}

Primitive Value Objects

For single primitives (string, number, boolean), extend PrimitiveValueObject<T>. Equality is by reference (===).

Example (from primitive-vo.spec.ts)

import { PrimitiveValueObject, InvalidValueObjectError } from '@rineex/ddd';

class StringVO extends PrimitiveValueObject<string> {
  constructor(value: string) {
    super(value);
  }

  protected validate(value: string): void {
    if (!value?.length) {
      throw InvalidValueObjectError.create('String cannot be empty');
    }
  }
}

class NumberVO extends PrimitiveValueObject<number> {
  constructor(value: number) {
    super(value);
  }

  protected validate(value: number): void {
    if (value < 0) {
      throw InvalidValueObjectError.create('Number must be non-negative');
    }
  }
}

// Usage
const s = new StringVO('test');
s.value; // 'test'
s.getValue(); // deprecated, use .value
s.toString(); // 'test'
s.equals(new StringVO('test')); // true

Pre-built: Email

import { Email } from '@rineex/ddd';

const email = Email.fromString('[email protected]');
// or: new Email('[email protected]')
email.value; // '[email protected]'
email.toString();

Pre-built: IPAddress

import { IPAddress } from '@rineex/ddd';

const ip = IPAddress.fromString('192.168.1.1');
ip.value; // '192.168.1.1'

Pre-built: AggregateId & DomainID

import { AggregateId, DomainID } from '@rineex/ddd';

// AggregateId
const id = AggregateId.generate();
const fromStr = AggregateId.fromString('550e8400-e29b-41d4-a716-446655440000');

// DomainID – extend for custom IDs
class AuthAttemptId extends DomainID {}

const attemptId = AuthAttemptId.generate();
const parsed = AuthAttemptId.fromString('550e8400-e29b-41d4-a716-446655440000');

Entities

Entities have stable identity. Equality is by id, not attributes. Use mutate(updater) for state changes; it re-freezes and re-validates. Use AggregateId or extend DomainID for custom identity types.

Example (from @rineex/auth-core OAuthAuthorization)

import { Entity, EntityProps, DomainID } from '@rineex/ddd';

// Custom ID – extend DomainID for domain-specific identifiers
class OAuthAuthorizationId extends DomainID {}

export interface OAuthAuthorizationProps {
  provider: string;
  redirectUri: string;
  scope: readonly string[];
}

export class OAuthAuthorization extends Entity<
  OAuthAuthorizationId,
  OAuthAuthorizationProps
> {
  constructor(
    props: EntityProps<OAuthAuthorizationId, OAuthAuthorizationProps>,
  ) {
    super({ ...props });
  }

  toObject(): Record<string, unknown> {
    return {
      id: this.id.value,
      provider: this.props.provider,
      redirectUri: this.props.redirectUri,
      scope: this.props.scope,
    };
  }

  validate(): void {
    if (!this.props.redirectUri.startsWith('https://')) {
      throw new Error('Redirect URI must use HTTPS');
    }
  }
}

// Usage
const auth = new OAuthAuthorization({
  id: OAuthAuthorizationId.generate(),
  props: {
    provider: 'google',
    redirectUri: 'https://app.example.com/callback',
    scope: ['openid', 'email'],
  },
});
auth.equals(other); // true iff same id

Aggregate Roots

Aggregate roots extend Entity and add domain event support.

Example (from aggregate-root.spec.ts)

import {
  AggregateRoot,
  DomainEvent,
  AggregateId,
  EntityValidationError,
} from '@rineex/ddd';

interface OrderProps {
  customerId: string;
  total: number;
}

class OrderCreatedEvent extends DomainEvent<
  AggregateId,
  { customerId: string }
> {
  readonly eventName = 'OrderCreated';

  static create(props: {
    id?: string;
    aggregateId: AggregateId;
    schemaVersion: number;
    occurredAt: number;
    payload: { customerId: string };
  }) {
    return new OrderCreatedEvent(props);
  }
}

class OrderCompletedEvent extends DomainEvent<AggregateId, { total: number }> {
  readonly eventName = 'OrderCompleted';

  static create(props: {
    id?: string;
    aggregateId: AggregateId;
    schemaVersion: number;
    occurredAt: number;
    payload: { total: number };
  }) {
    return new OrderCompletedEvent(props);
  }
}

class Order extends AggregateRoot<AggregateId, OrderProps> {
  constructor(params: {
    id: AggregateId;
    createdAt?: Date;
    props: OrderProps;
  }) {
    super(params);
  }

  create(): void {
    this.addEvent(
      OrderCreatedEvent.create({
        aggregateId: this.id,
        schemaVersion: 1,
        occurredAt: Date.now(),
        payload: { customerId: this.props.customerId },
      }),
    );
  }

  complete(): void {
    this.addEvent(
      OrderCompletedEvent.create({
        aggregateId: this.id,
        schemaVersion: 1,
        occurredAt: Date.now(),
        payload: { total: this.props.total },
      }),
    );
  }

  validate(): void {
    if (!this.props.customerId?.trim()) {
      throw EntityValidationError.create('Customer ID is required', {});
    }
    if (this.props.total < 0) {
      throw EntityValidationError.create('Total must be non-negative', {});
    }
  }

  toObject() {
    return {
      id: this.id.toString(),
      createdAt: this.createdAt.toISOString(),
      customerId: this.props.customerId,
      total: this.props.total,
    };
  }
}

// Usage
const order = new Order({
  id: AggregateId.generate(),
  props: { customerId: 'customer-1', total: 100 },
});
order.create();
order.complete();

order.domainEvents; // readonly copy
const events = order.pullDomainEvents(); // returns and clears

Domain Events

Events are immutable. Payload must be Serializable (primitives, arrays, plain objects). id is auto-generated if omitted.

Example (from domain.event.spec.ts)

import { DomainEvent, DomainEventPayload, AggregateId } from '@rineex/ddd';

interface TestPayload extends DomainEventPayload {
  userId: string;
  action: string;
}

class TestDomainEvent extends DomainEvent<AggregateId, TestPayload> {
  readonly eventName = 'TestEvent';

  static create(props: {
    id?: string;
    aggregateId: AggregateId;
    schemaVersion: number;
    occurredAt: number;
    payload: TestPayload;
  }) {
    return new TestDomainEvent(props);
  }
}

// Usage
const event = TestDomainEvent.create({
  aggregateId: AggregateId.generate(),
  schemaVersion: 1,
  occurredAt: Date.now(),
  payload: { userId: 'user-1', action: 'login' },
});

event.id;
event.eventName;
event.aggregateId;
event.schemaVersion;
event.occurredAt;
event.payload;

event.toPrimitives();
// { id, eventName, aggregateId, schemaVersion, occurredAt, payload }

Domain Errors

Base DomainError

Extend DomainError<Code, Meta> with an explicit code literal and optional primitive metadata. Register codes in a bounded-context registry const.

import { DomainError, InferErrorCodes, Metadata } from '@rineex/ddd';

export const UserErrorRegistry = {
  USER: ['NOT_FOUND', 'INVALID_EMAIL'],
} as const;

export type UserDomainErrorCode = InferErrorCodes<typeof UserErrorRegistry>;

type Props = Metadata<{ identityId: string }>;

class IdentityDisabledError extends DomainError<'USER.NOT_FOUND', Props> {
  readonly code = 'USER.NOT_FOUND' as const;

  constructor(message: string, props: Props) {
    super(message, props);
  }
}

Core error registry

@rineex/ddd ships CoreDomainErrorRegistry and InferErrorCodes for built-in codes (DOMAIN.*, CORE.*, SYSTEM.*). Each bounded context defines its own registry:

import type { InferErrorCodes } from '@rineex/ddd';

export const MyModuleErrorRegistry = {
  MY_MODULE: ['NOT_FOUND', 'INVALID_INPUT'],
} as const;

export type MyModuleDomainErrorCode = InferErrorCodes<
  typeof MyModuleErrorRegistry
>;

Use registryErrorCodes(registry) in architecture tests to verify every error class code is registered.

Built-in Errors

| Error | Code | Use case | | ------------------------- | ------------------------ | ------------------------------------ | | InvalidValueObjectError | DOMAIN.INVALID_VALUE | Value object validation failure | | EntityValidationError | CORE.VALIDATION_FAILED | Entity/aggregate invariant violation | | InvalidValueError | DOMAIN.INVALID_VALUE | Value constraint violation | | InvalidStateError | DOMAIN.INVALID_STATE | Invalid state for operation | | InternalError | CORE.INTERNAL_ERROR | Unexpected/programming errors | | TimeoutError | SYSTEM.TIMEOUT | Operation timeout | | ApplicationError | (extends Error) | Application/HTTP layer errors |

// InvalidValueError – optional metadata
throw new InvalidValueError('Age cannot be negative');
throw new InvalidValueError('Validation failed', {
  field: 'age',
  min: 18,
  max: 100,
});

// InvalidStateError – no metadata
throw new InvalidStateError('Cannot cancel completed order');

// EntityValidationError – props required
throw EntityValidationError.create('Name is required', {});

// ApplicationError – structured params
class UserNotFoundError extends ApplicationError {
  constructor(userId: string) {
    super({
      message: `User ${userId} not found`,
      code: 'USER_NOT_FOUND',
      isOperational: true,
      metadata: { userId },
    });
  }
}

Result Type

Result<T, E extends UseCaseError> models application use-case outcomes — return expected failures instead of throwing. Domain entities and value objects throw DomainError on invariant violations; application services return Result.

E is a per-use-case error union (ApplicationError, DomainError, etc.) — there is no DomainError default.

Layer contract

| Layer | Mechanism | | -------------------------------- | ------------------------------------------ | | Domain (entity, VO, aggregate) | Throw on invariant violation | | Application (use case) | Return Result<O, E> from service methods | | Infrastructure (HTTP, messaging) | Unwrap Result at the boundary |

ApplicationServicePort returns Promise<O> — it is a structural seam for orchestration. Application services that model expected failures should return Result from their own execute methods (or wrap the port call at the composition root).

Example

import { Result, UseCaseError, InvalidValueError } from '@rineex/ddd';

// Creation
const ok = Result.ok(42);
const voidOk = Result.ok(); // Ok<void> for command use cases with no return value
const failed = Result.err(new InvalidValueError('Invalid'));

// Narrowing
if (Result.isOk(ok)) {
  const value = ok.value; // number
}
if (Result.isErr(failed)) {
  const error = failed.error; // InvalidValueError
}

// match
const message = Result.match(failed, {
  ok: v => `ok ${v}`,
  err: e => e.message,
});

Validation + chaining

function validateEmail(email: string): Result<string, InvalidValueError> {
  if (!email.includes('@')) {
    return Result.err(new InvalidValueError('Invalid email format'));
  }
  return Result.ok(email);
}

function createAccount(
  email: string,
): Result<{ email: string }, InvalidValueError> {
  return Result.flatMap(validateEmail(email), validated =>
    Result.ok({ email: validated }),
  );
}

v4 → v5 migration

| v4 | v5 | | ---------------------------------- | ---------------------------------------------- | | Result.fail(e) | Result.err(e) | | result.isSuccess / isFailure | Result.isOk(result) / Result.isErr(result) | | result.getValue() / getError() | result.value / result.error (after narrow) |


Application Services

Use ApplicationServicePort<I, O> for use-case orchestration. The port signature is execute(args: I): Promise<O> — it does not return Result. Services that need explicit failure channels return Result from a dedicated method or wrap domain outcomes at the caller.

import { ApplicationServicePort, Result, InvalidValueError } from '@rineex/ddd';

interface CreateUserInput {
  name: string;
  email: string;
}

interface CreateUserOutput {
  id: string;
  name: string;
}

class CreateUserService implements ApplicationServicePort<
  CreateUserInput,
  CreateUserOutput
> {
  async execute(args: CreateUserInput): Promise<CreateUserOutput> {
    // validate, create entity, persist, publish events
    return { id: '...', name: args.name };
  }
}

// Command with no return value — use Result.ok()
async function deactivateUser(
  id: string,
): Promise<Result<void, InvalidValueError>> {
  if (!id) return Result.err(new InvalidValueError('ID required'));
  // ... persist
  return Result.ok();
}

Ports & Utilities

ClockPort

import type { ClockPort } from '@rineex/ddd';

const clock: ClockPort = {
  now: () => new Date(),
};

HttpStatus & HttpStatusMessage

import { HttpStatus, HttpStatusMessage } from '@rineex/ddd';

HttpStatus.OK; // 200
HttpStatus.NOT_FOUND; // 404
HttpStatusMessage[404]; // 'Not Found'

deepFreeze

import { deepFreeze } from '@rineex/ddd';

const frozen = deepFreeze({ a: 1, nested: { b: 2 } });

Integration Guide

  1. Add dependency: pnpm add @rineex/ddd

  2. Define an error registry per bounded context (see Domain Errors).

  3. Custom IDs: Extend DomainID and use generate() / fromString().

  4. Use mutate() for entity/aggregate state changes.

  5. Persist then publish: Save aggregate, then call pullDomainEvents() and publish.


Core Concepts

Value Objects, Entities, Aggregate Roots, and Domain Events are documented in the sections above. Domain errors use registry-backed codes; application outcomes use Result.


Examples

See Aggregate Roots (Order example) and Integration Guide for end-to-end patterns.


Best Practices

  • Extend DomainID for branded aggregate identifiers
  • Use mutate() for entity state changes — never mutate props directly
  • Throw DomainError in domain layer; return Result in application layer
  • Register error codes in a bounded-context registry and verify with architecture tests
  • Call pullDomainEvents() after persistence, then publish

Error Handling

See Domain Errors for DomainError, registries, and built-in error classes.


Contributing

Develop in packages/ddd. Run pnpm test, pnpm lint, and pnpm check-types from the package directory. Add a changeset for publishable changes: pnpm changeset from the monorepo root.


API Reference

ValueObject<T>

| Member | Description | | -------------------- | ------------------------ | | value | Read-only props | | equals(other) | Deep equality | | toJSON() | Returns props | | toString() | JSON.stringify(props) | | ValueObject.is(vo) | Type guard | | validate(props) | Abstract, must implement |

PrimitiveValueObject<T>

| Member | Description | | ----------------- | --------------------- | | value | Primitive value | | getValue() | Same (deprecated) | | equals(other) | Reference equality | | toString() | String representation | | validate(value) | Abstract |

Entity<ID, Props>

| Member | Description | | ----------------- | ------------------------------ | | id | Identity | | createdAt | Creation date | | props | Read-only (protected) | | equals(other) | By id | | mutate(updater) | Safe state change + revalidate | | validate() | Abstract | | toObject() | Abstract |

AggregateRoot<ID, Props>

Extends Entity. Adds:

| Member | Description | | -------------------- | ------------------- | | addEvent(event) | Append domain event | | domainEvents | Read-only copy | | pullDomainEvents() | Return and clear |

DomainEvent<AggregateId, Payload>

| Member | Description | | ---------------- | ------------------- | | id | Event ID | | aggregateId | Aggregate reference | | schemaVersion | Version | | occurredAt | Unix ms | | payload | Serializable data | | eventName | Abstract | | toPrimitives() | Plain object |

Result<T, E extends UseCaseError>

| Member | Description | | ---------------------------------------------- | ----------------------------------------- | | Result.ok() | Success with no value (Ok<void>) | | Result.ok(value) | Success ({ kind: 'ok', value }) | | Result.err(error) | Failure ({ kind: 'err', error }) | | Result.isOk(r) / Result.isErr(r) | Type guards | | Result.match(r, { ok, err }) | Exhaustive fold | | Result.flatMap(r, fn) | Chain use cases; forwards err | | Result.map(r, fn) / Result.mapError(r, fn) | Transform value or error | | UseCaseError | { code: string } seam for error channel |


License

Apache-2.0 – see LICENSE.