Skip to main content

Architecture

Kysera follows a modular, layered architecture designed for flexibility, type safety, and production readiness. The architecture is built on three core layers: Core UtilitiesExecutor (Foundation)Data Access Patterns (DAL/Repository).

Design Principles

1. Minimal Core, Optional Everything

The core package is intentionally minimal (~8KB):

  • Error handling and error codes
  • Pagination helpers (offset and cursor-based)
  • Type definitions (Executor, Timestamps, etc.)
  • Logger interface

Infrastructure utilities (health, retry, debug, testing) are separate opt-in packages:

  • @kysera/infra - Health checks, retry, circuit breaker, shutdown
  • @kysera/debug - Query logging, profiling, SQL formatting
  • @kysera/testing - Test utilities (transaction rollback, factories)

Everything else (repository pattern, plugins) is optional and tree-shakeable.

2. Minimal External Dependencies

Core packages have minimal runtime dependencies:

  • @kysera/executor has zero runtime dependencies (only kysely as a peer dependency)
  • @kysera/core lists @kysera/executor as a dependency but only imports types from it
  • All other core packages depend only on internal Kysera packages
{
"dependencies": {
"@kysera/executor": "workspace:*"
},
"peerDependencies": {
"kysely": ">=0.29.0"
}
}

This ensures:

  • Minimal security surface
  • No bloat from transitive dependencies
  • Full control over code execution
  • Tree-shakeable exports

3. ESM-Only Architecture

Kysera is ESM-only for modern environments:

{
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}
}

Benefits:

  • Faster module loading
  • Better tree-shaking
  • Deno and Bun compatible
  • No CommonJS overhead

4. TypeScript Strict Mode

All packages use the strictest TypeScript configuration:

{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true
}
}

3-Layer Architecture

The modern architecture features @kysera/executor as the foundation layer:

The solid path is the query path: your application calls a repository or DAL query, the executor applies registered plugins to the query builder, and the final SQL runs through Kysely against the database. Details the diagram compresses:

  • Plugins register with the executor once (createExecutor(db, [...])) and from then on intercept every query, no matter which data access pattern issued it.
  • @kysera/executor is ~8KB with zero runtime dependencies; its KyseraExecutor type extends Kysely<DB>, so it drops in anywhere a Kysely instance is expected.
  • Utility packages sit beside the stack, not inside the query path: @kysera/core (~8KB) is a runtime dependency of the pattern and plugin packages, while @kysera/infra, @kysera/debug, and @kysera/testing are opt-in and depended on by nothing else.

Dependency Flow

Arrows point from a package to what it depends on:

Two edges deserve a note: @kysera/core lists @kysera/executor as a dependency but only imports types from it, so no executor code lands in core bundles. And @kysera/audit, @kysera/rls, and @kysera/timestamps additionally declare @kysera/repository as a peer dependency for their repository extensions — @kysera/soft-delete does not need it.

Repository Factory Pattern

The factory pattern enables clean dependency injection:

// Factory function - creates repository with injected executor
export function createUserRepository(executor: Executor<Database>) {
return {
async findById(id: number): Promise<User | null> {
const row = await executor
.selectFrom('users')
.selectAll()
.where('id', '=', id)
.executeTakeFirst()
return row ?? null
},
async create(input: CreateUserInput): Promise<User> {
return executor.insertInto('users').values(input).returningAll().executeTakeFirstOrThrow()
}
}
}

// Factory of factories - creates all repositories
export function createRepositories(executor: Executor<Database>) {
return {
users: createUserRepository(executor),
posts: createPostRepository(executor)
} as const
}

Plugin Architecture

Plugins extend functionality through the @kysera/executor foundation layer:

Plugin Flow Through Executor

1. Query Interceptors (Work with Both Repository & DAL)

Modify queries before execution through the executor:

// Plugin definition
{
name: 'soft-delete',
version: '1.0.0',
interceptQuery(qb, context) {
if (context.operation === 'select') {
// Qualify with the alias when the table reference has one
// ('users as u' exposes only 'u' as correlation name)
return qb.where(`${context.alias ?? context.table}.deleted_at`, 'is', null)
}
return qb
}
}

// Automatic application through executor
const executor = await createExecutor(db, [softDeletePlugin()])
const users = await executor.selectFrom('users').selectAll().execute()
// -> SELECT * FROM users WHERE deleted_at IS NULL

2. Repository Extensions (Work with Repository Only)

Add new methods to repositories:

// Plugin definition
{
name: 'soft-delete',
version: '1.0.0',
extendRepository(repo) {
return {
...repo,
async softDelete(id: number) {
return repo.executor
.updateTable(repo.tableName)
.set({ deleted_at: new Date() })
.where('id', '=', id)
.execute()
},
async restore(id: number) { /* ... */ }
}
}
}

// Usage
const orm = await createORM(db, [softDeletePlugin()])
const userRepo = orm.createRepository(createUserRepository)
await userRepo.softDelete(1) // Extension method

Performance Characteristics

PackageSizeOverheadDependencies
@kysera/core~8KBMinimalexecutor
@kysera/executor~8KB<0.1ms (no interceptors)0
<0.2ms (with interceptors)(kysely peer)
@kysera/repository~22KB<0.3ms per queryexecutor, dal, core
@kysera/dal~4KB<0.2ms per queryexecutor, core
@kysera/infra~12KB<0.2ms per querycore
@kysera/debug~5KB<0.1ms per querycore
@kysera/testing~6KBDev-onlycore
Plugins4-12KB each<0.1ms per queryexecutor, core

Benchmarks

  • Cursor pagination: 72K queries/second
  • Debug plugin: 18K queries/second with memory management
  • Cursor encoding: 4-5M operations/second