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

@ministryplace/kaironic

v0.1.0

Published

Event-sourced domain model toolkit for TypeScript that puts Developer Experience first

Downloads

5

Readme

Kaironic

[![npm version][npm-version-src]][npm-version-href] [![npm downloads][npm-downloads-src]][npm-downloads-href] [![Github Actions][github-actions-src]][github-actions-href] [![Codecov][codecov-src]][codecov-href]

Kaironic helps you model complex business processes without drowning in boilerplate.

🚧 Under active development

Kaironic helps you model complex business processes without drowning in boilerplate.

Event-sourced domain model toolkit for TypeScript that puts Developer Experience first. Define aggregates with decorators, emit strongly-typed domain events, enforce invariants, and persist with pluggable stores. Snapshots, optimistic concurrency, idempotency, and an optional outbox are built in.

Highlights

  • Declarative decorators for aggregates, commands, invariants, and event handlers
  • Strict mutation control: all state changes happen inside event appliers
  • Serializable state and events with rich metadata (correlation, causation, idempotency)
  • Version tracking and optimistic concurrency baked in
  • Rehydrate from snapshot plus history with pluggable snapshot strategy
  • Optional outbox for reliable, transactional event publishing
  • Minimal test DSL (given/when/then) for readable specs
  • Zero runtime deps except reflect-metadata

Documentation

Examples

The examples directory contains complete, working applications demonstrating Kaironic features:

Bank Account Example

A comprehensive example showing:

  • Event-sourced aggregates with decorators
  • Class-based domain events with perfect type inference
  • Domain invariants and business rules
  • Optimistic concurrency control
  • Idempotency handling
  • Error handling patterns
  • Outbox pattern for reliable event publishing
  • Audit logging and notifications
  • Testing with the DSL
cd examples
pnpm install
pnpm run bank-account

Features demonstrated:

  • Event Sourcing: Complete event-sourced bank account
  • Domain Modeling: Aggregates, events, commands, and invariants
  • Error Handling: Domain rule violations and recovery
  • Concurrency: Optimistic locking with conflict resolution
  • Idempotency: Duplicate prevention with keys
  • Outbox Pattern: Reliable event publishing
  • Testing: Given/When/Then test patterns
  • Notifications: Event-driven architecture
  • Audit Trail: Compliance and observability

Running Examples

Each example includes:

  • Complete TypeScript implementation
  • Comprehensive tests
  • Demo scripts showing real usage
  • README with detailed explanations

The examples serve as both learning resources and reference implementations for production use.

Install

npm install kaironic reflect-metadata
# or
yarn add kaironic reflect-metadata
# or
pnpm add kaironic reflect-metadata

Concepts

  • Aggregate Root Your consistency boundary. Tracks serializable state, versions, and uncommitted events. Mutations are only allowed inside event appliers.

  • Domain Event Immutable fact with type, data, and rich metadata including aggregate, aggregateId, version, occurredAt, and optional correlationId, causationId, idempotencyKey.

  • Invariant Synchronous rule that must hold before and after commands. Returns Ok(void) or Err(DomainRuleViolation).

  • Command Business operation that returns Result<DomainEvent[] | object[], DomainRuleViolation>. Kaironic converts plain objects or class instances to fully-stamped events, applies them, and accumulates them for persistence.

  • Repository Loads an aggregate from snapshot plus history and saves uncommitted events with optimistic concurrency, snapshotting, and optional outbox write.

  • Entity Base class for domain entities with identity and mutable state. Provides JSON serialization and identity comparison.

  • Value Object Base class for immutable value objects. Provides structural equality comparison and JSON serialization.

Decorators

  • @DomainAggregate(options) Registers the aggregate. Options:

    • name string override
    • snapshotEvery number
    • initialState: () => S
    • streamCategory?: string
    • doc?: string freeform description
  • @DomainCommand(options?) Wraps a method as a command. Options:

    • idempotencyKey?: (...args) => string | undefined
    • correlationId?: (...args) => string | undefined
    • doc?: string
  • @DomainInvariant(doc?) Registers a rule method that returns Result<void, DomainRuleViolation>.

  • @OnDomainEvent(type, doc?) Registers an event applier method with signature (event: DomainEvent, draft: S): void. Directly mutate the draft parameter to update state.

Aggregate lifecycle

  1. Construct aggregate with id, state seeded from initialState.
  2. Execute a command; invariants are checked before and after.
  3. Command returns one or more events; they are stamped, applied, and staged as uncommitted.
  4. Repository save persists events with optimistic concurrency; snapshots may be taken.
  5. Outbox is optionally written inside the same transaction.
  6. Aggregate clears its uncommitted events and updates originalVersion.

Persistence interfaces

Implement these to back Kaironic with your database:

export interface EventStore {
  append: (aggregate: string, id: string, events: DomainEvent[], expectedVersion: number) => Promise<Result<void, OptimisticConcurrencyViolation>>
  load: (aggregate: string, id: string, afterVersion: number) => Promise<DomainEvent[]>
  getVersion: (aggregate: string, id: string) => Promise<number>
  hasIdempotencyKey?: (aggregate: string, id: string, key: string) => Promise<boolean>
}

export interface SnapshotStore {
  load: (aggregate: string, id: string) => Promise<AggregateSnapshot | null>
  save: (s: AggregateSnapshot) => Promise<void>
}

Transactional variants are supported for atomic append plus snapshot (and outbox) with begin/commit/rollback.

Snapshot strategies

Snapshots improve performance by reducing the number of events that need to be loaded and replayed. Kaironic provides flexible snapshot strategies to control when snapshots are taken.

Built-in Strategies

If no strategy is provided, the repository uses the aggregate's built-in shouldSnapshot() method, which respects the snapshotEvery option from @DomainAggregate.

You might also consider other strategies, like time-based or size-based.

Performance Considerations

  • Small aggregates: May not need snapshots at all
  • Large aggregates: Benefit from frequent snapshots (every 50-200 events)
  • Write-heavy aggregates: Consider size or time-based strategies
  • Read-heavy aggregates: More frequent snapshots improve load performance

Snapshot Storage

Snapshots are stored separately from events.

When loading an aggregate, the repository:

  1. Loads the most recent snapshot
  2. Loads events after the snapshot version
  3. Replays events on top of the snapshot state

Outbox publishing

Add an Outbox implementation to AggregateRepository to stage committed events for reliable, transactional publishing to message brokers or webhooks. Use a separate process to drain the outbox.

Idempotency and retries

  • Provide idempotencyKey in @DomainCommand options to make commands naturally retry-safe.
  • In-memory guard prevents accidental re-application during the aggregate’s lifetime.
  • Store-level hasIdempotencyKey allows dedup across restarts; back it with an index like (aggregate, aggregate_id, metadata->>'idempotencyKey').

Optimistic concurrency

Repositories perform a preflight getVersion check and enforce expectedVersion on append. On mismatch, you receive OptimisticConcurrencyViolation with expected and actual versions so you can reload, recompute, and retry.

Entities and Value Objects

Kaironic provides base classes for domain entities and value objects to help structure your domain model.

Introspection and self-documentation

Kaironic provides runtime introspection capabilities to help with documentation, testing, and operational visibility.

Aggregate Introspection

Use AggregateRoot.describe() to get metadata about your aggregates:

const description = AggregateRoot.describe(accountInstance)

console.log(description)
/*
{
  doc: 'Simple event-sourced bank account with negative-balance and closed-account invariants.',
  invariants: [
    { name: 'mustNotBeNegative', doc: 'Balance must never be negative' },
    { name: 'cannotOperateIfAlreadyClosed', doc: 'Cannot operate on closed accounts' }
  ],
  commands: [
    { name: 'open', doc: 'Open a new account with duplicate detection' },
    { name: 'deposit', doc: 'Deposit positive funds' },
    { name: 'withdraw', doc: 'Withdraw positive funds' },
    { name: 'close', doc: 'Close account when balance is zero' }
  ],
  handlers: [
    { event: 'AccountOpened', handler: 'onOpened', doc: 'Set currency and openedAt' },
    { event: 'FundsDeposited', handler: 'onDeposited', doc: 'Increase balance' },
    { event: 'FundsWithdrawn', handler: 'onWithdrawn', doc: 'Decrease balance' },
    { event: 'AccountClosed', handler: 'onClosed', doc: 'Mark as closed' }
  ]
}
*/

Use Cases

  • API Documentation: Generate OpenAPI specs from aggregate metadata
  • Admin Interfaces: Build management UIs that show available operations
  • Testing: Validate that all expected commands and events are implemented
  • Monitoring: Track aggregate usage patterns and performance
  • Migration Planning: Understand the current domain model structure

Production adapters

Postgres event store (suggested schema):

  • events(stream_key text, aggregate text, aggregate_id text, version int, id uuid, type text, data jsonb, metadata jsonb, occurred_at timestamptz, primary key (stream_key, version))
  • Unique index for idempotency: (aggregate, aggregate_id, (metadata->>'idempotencyKey')) where (metadata->>'idempotencyKey') is not null
  • snapshots(aggregate text, id text, version int, state jsonb, captured_at timestamptz, primary key (aggregate, id))
  • outbox(id uuid primary key, available_at timestamptz, payload jsonb, status text, retries int default 0)

Design choices

  • Event applications are the only place state can change. Any attempt to mutate elsewhere throws with an explanatory message.
  • Commands are pure in the sense that they return events; side effects (like publishing) happen after persistence via the outbox pattern.
  • JSON-serializable state and events keep persistence adapters simple and portable.

FAQ

  • Can I use class-based events Yes. Give the class a static type string. Plain objects work too.

  • Do I need snapshots For small streams, no. For long-lived aggregates, snapshots reduce load time. Turn on with snapshotEvery or a custom strategy.

  • How do I handle cross-aggregate transactions Use repo.saveAll([aggA, aggB]) with a transactional store. Otherwise, model sagas/process managers driven by events from the outbox.

Contributing

Issues and PRs are welcome. Please include tests and keep DX front and center.

License

MIT