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

v3.2.0

Published

Domain Driven Design package for Rineex core modules

Readme

@rineex/ddd

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

(Test change for version-in-same-PR workflow verification.)

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/authentication 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,
  DomainError,
  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: 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/authentication 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<Meta, Code> with code, type, and constructor super(message, metadata).

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

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

class IdentityDisabledError extends DomainError<Props> {
  readonly code: DomainErrorCode = 'AUTH_CORE_IDENTITY.DISABLED_ERROR';
  readonly type: DomainErrorType = 'DOMAIN.INVALID_STATE';

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

  static create(message: string, props: Props) {
    return new IdentityDisabledError(message, props);
  }
}

Extending Error Namespaces

Declare namespaces via module augmentation for type-safe codes:

// your-module.d.ts
import '@rineex/ddd';

declare module '@rineex/ddd' {
  interface DomainErrorNamespaces {
    USER: ['NOT_FOUND', 'INVALID_EMAIL'];
    ORDER: ['NOT_FOUND', 'INVALID_STATUS'];
  }
}

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> for explicit success/failure without throwing. Default error type is DomainError.

Example (from result.spec.ts)

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

// Creation
const ok = Result.ok(42);
const fail = Result.fail(new InvalidValueError('Invalid'));

// Checks
ok.isSuccess; // true
fail.isFailure; // true

// Extraction
ok.getValue(); // 42
fail.getError(); // InvalidValueError

// Type guards
if (result.isSuccessResult()) {
  const v = result.getValue(); // T
}
if (result.isFailureResult()) {
  const e = result.getError(); // E
}

Validation pattern

function validateAge(age: number): Result<number, DomainError> {
  if (age < 0) {
    return Result.fail(new InvalidValueError('Age cannot be negative'));
  }
  if (age > 150) {
    return Result.fail(new InvalidValueError('Age seems unrealistic'));
  }
  return Result.ok(age);
}

Chaining

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

function createAccount(email: string): Result<{ email: string }, DomainError> {
  const emailResult = validateEmail(email);
  if (emailResult.isFailureResult()) return emailResult;

  const validated = emailResult.getValue()!;
  return Result.ok({ email: validated });
}

Application Services

Use ApplicationServicePort<I, O> for use-case orchestration.

import { ApplicationServicePort, Result } 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 };
  }
}

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. Extend DomainErrorNamespaces in a .d.ts file:

declare module '@rineex/ddd' {
  interface DomainErrorNamespaces {
    MY_MODULE: ['NOT_FOUND', 'INVALID_INPUT'];
  }
}
  1. Custom IDs: Extend DomainID and use generate() / fromString().

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

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


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>

| Member | Description | | ---------------------------------------- | -------------- | | Result.ok(value) | Success | | Result.fail(err) | Failure | | isSuccess, isFailure | Booleans | | getValue(), getError() | Value or error | | isSuccessResult(), isFailureResult() | Type guards |


License

Apache-2.0 – see LICENSE.