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
| Package | ORM | Schema |
|---|---|---|
@noddde/drizzle | Drizzle ORM | TypeScript table builders (per dialect) |
@noddde/prisma | Prisma | .prisma schema file |
@noddde/typeorm | TypeORM | TypeScript 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:
| Dialect | Persistence | No concurrency / Optimistic | Pessimistic 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:
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:
- The adapter opens a database transaction
- It sets
txStore.currentto the transaction-scoped database client - All enlisted persistence operations execute within that transaction, because they read
txStore.currentfor their queries - On success, the transaction commits and deferred events are returned for publishing
- 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 throwConcurrencyError. - States table: A
versioncolumn enables optimistic locking. Updates useWHERE version = expectedVersion; zero rows affected throwsConcurrencyError.
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.
| Adapter | Constructor | Dialect 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:
| Dialect | Lock mechanism | Lock key format |
|---|---|---|
| PostgreSQL | pg_advisory_lock / pg_try_advisory_lock | 64-bit FNV-1a hash of name:id |
| MySQL/MariaDB | GET_LOCK / RELEASE_LOCK | First 64 chars of name:id (MySQL limit) |
| MSSQL | sp_getapplock / sp_releaseapplock | First 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
| Factor | Drizzle | Prisma | TypeORM |
|---|---|---|---|
| Schema definition | TypeScript table builders | .prisma schema file | Decorator-based entities |
| Code generation | None | Required (prisma generate) | None |
| Type safety | Full (inferred from schema) | Full (generated client) | Partial (decorator metadata) |
| Bundle size | Lightweight | Heavier (generated client) | Medium |
| Sync driver support | Yes (better-sqlite3) | No (async only) | Yes |
| Migration tooling | Drizzle Kit | Prisma Migrate | TypeORM 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 --
.prismaschema, generated client, and adapter wiring - TypeORM Adapter -- Decorator entities and MSSQL support