noddde
Persistence

Persistence Adapters

Production-ready persistence using Drizzle, Prisma, TypeORM, or your own custom adapter with any supported database.

noddde provides three ORM adapter packages that implement all persistence interfaces and UnitOfWork using your ORM's native transaction mechanism. Pick the ORM you already use -- each adapter works with whatever database your ORM supports (PostgreSQL, MySQL, SQLite, etc.). If you don't use an ORM, you can build your own adapter for any database driver. See the per-adapter pages for full setup details.

Available Adapters

PackageORMSchema
@noddde/drizzleDrizzle ORMTypeScript table builders (per dialect)
@noddde/prismaPrisma.prisma schema file
@noddde/typeormTypeORMTypeScript entity decorators

Per-Adapter Setup

Dialect Support Matrix

Persistence (event store, state store, saga store, snapshot store, outbox store) and concurrency control work with every dialect supported by your ORM. The only dialect restriction applies to pessimistic locking, which requires database-level advisory locks:

DialectPersistenceNo concurrency / OptimisticPessimistic locking
PostgreSQL✅ All ORMs✅ All ORMs✅ All ORMs
MySQL✅ All ORMs✅ All ORMs✅ All ORMs
MariaDB✅ All ORMs✅ All ORMs✅ All ORMs
SQLite✅ All ORMs✅ All ORMs❌ No advisory locks
MSSQL✅ TypeORM✅ TypeORM✅ TypeORM only

For SQLite or any dialect without advisory lock support, use InMemoryAggregateLocker from @noddde/engine for single-process deployments, or choose the optimistic strategy instead.

Each package exports a class-based adapter that implements the PersistenceAdapter interface from @noddde/core. Pass it to wireDomain via the persistenceAdapter property and the engine resolves all persistence concerns automatically:

main.ts
import { DrizzleAdapter } from "@noddde/drizzle";
import { wireDomain } from "@noddde/engine";

const adapter = new DrizzleAdapter(db);

const domain = await wireDomain(definition, {
  persistenceAdapter: adapter,
  aggregates: {
    Room: { persistence: "event-sourced" },
    Inventory: {}, // defaults to state-stored from adapter
  },
});

The adapter provides all stores (event-sourced, state-stored, saga, snapshot, outbox, UoW, advisory locker). The engine infers what it needs based on the domain definition -- no manual mapping required.

All three adapters also support per-aggregate dedicated state tables via the stateStored() helper method.

The createXxxAdapter factory functions are still supported but deprecated. Prefer the class-based API (DrizzleAdapter, PrismaAdapter, TypeORMAdapter) for new code.

Per-Aggregate Dedicated State Tables

By default, all state-stored aggregates share a single noddde_aggregate_states table. When you need direct SQL access to domain fields, database-level constraints, or compatibility with an existing schema, all three adapters expose a stateStored(table, { mapper }) helper that wires a dedicated per-aggregate table. The framework writes the aggregateId and version columns; a mapper you supply owns the rest of the row — either spread across typed columns or kept opaque via jsonStateMapper(...). See the Drizzle, Prisma, and TypeORM pages for full mapper examples, and Why an Aggregate State Mapper? for the design rationale.

How Transactions Work

Each adapter section above describes its ORM-specific transaction mechanism. Under the hood, all three follow the same pattern for integrating with the Unit of Work:

  1. The adapter opens a database transaction
  2. It sets txStore.current to the transaction-scoped database client
  3. All enlisted persistence operations execute within that transaction, because they read txStore.current for their queries
  4. On success, the transaction commits and deferred events are returned for publishing
  5. On failure, the transaction rolls back and no events are published

This shared transaction store pattern means persistence classes do not need to know whether they are operating inside a unit of work or not -- they always read from txStore.current, which is null outside a transaction and points to the active transaction client inside one.

Concurrency Control

All three adapters support both optimistic and pessimistic concurrency strategies. Here is what each adapter provides at the database level.

Optimistic Concurrency (built-in)

Handled automatically by the persistence implementations via database constraints:

  • Events table: A unique constraint on (aggregate_name, aggregate_id, sequence_number) prevents concurrent appends. Violations throw ConcurrencyError.
  • States table: A version column enables optimistic locking. Updates use WHERE version = expectedVersion; zero rows affected throws ConcurrencyError.

Advisory Lockers (for pessimistic concurrency)

Each adapter exports an advisory locker for use with the pessimistic strategy. See the Dialect Support Matrix above for which databases support locking.

AdapterConstructorDialect Detection
DrizzleAdvisoryLocker(db, dialect)Explicit: "pg" | "mysql" | "sqlite" (throws)
PrismaAdvisoryLocker(prisma, dialect)Explicit: "postgresql" | "mysql" | "mariadb"
TypeORMAdvisoryLocker(dataSource)Auto-detects from dataSource.options.type

Under the hood, each dialect uses the database's native advisory lock mechanism:

DialectLock mechanismLock key format
PostgreSQLpg_advisory_lock / pg_try_advisory_lock64-bit FNV-1a hash of name:id
MySQL/MariaDBGET_LOCK / RELEASE_LOCKFirst 64 chars of name:id (MySQL limit)
MSSQLsp_getapplock / sp_releaseapplockFirst 255 chars of name:id (TypeORM only)

SQLite has no advisory lock mechanism. For single-process SQLite deployments, use InMemoryAggregateLocker from @noddde/engine.

Advisory locks are session-level, spanning beyond the database transaction. This is intentional: the lock covers the entire load→execute→save lifecycle.

Choosing an Adapter

FactorDrizzlePrismaTypeORM
Schema definitionTypeScript table builders.prisma schema fileDecorator-based entities
Code generationNoneRequired (prisma generate)None
Type safetyFull (inferred from schema)Full (generated client)Partial (decorator metadata)
Bundle sizeLightweightHeavier (generated client)Medium
Sync driver supportYes (better-sqlite3)No (async only)Yes
Migration toolingDrizzle KitPrisma MigrateTypeORM migrations

All three provide identical functionality for noddde's purposes. The choice comes down to which ORM your project already uses.

Next Steps

  • Drizzle Adapter -- TypeScript table builders, per-dialect setup, and transaction integration
  • Prisma Adapter -- .prisma schema, generated client, and adapter wiring
  • TypeORM Adapter -- Decorator entities and MSSQL support

On this page