Persistence Layer

The Persistence Layer manages all database interactions using Drizzle ORM with PostgreSQL. It implements the Repository pattern with Data Access Objects (DAO) and maintains clean separation between domain types and database types.
Layer Responsibility: The Persistence Layer handles all database operations, schema definitions, migrations, and data transformations. It never contains business logic or authorization checks.
Perfect for: Type-safe database operations, maintainable schema evolution, efficient queries, and clean data access patterns with proper type transformations.

Persistence Layer Architecture

The layer is organized with clear separation between models, repositories, and type definitions:
Persistence Layer Organization:
src/db/                         # 📁 Database Layer Root
├── models/                     # 📋 Schema Definitions
│   ├── db.ts                  # Drizzle instance & schema export
│   ├── user-model.ts          # User table schema
│   ├── subscription-model.ts   # Subscription table schema
│   ├── organization-model.ts   # Organization table schema
│   └── index.ts               # All models export
├── repositories/               # 🗄️ Data Access Objects (DAO)
│   ├── user-repository.ts     # User database operations
│   ├── subscription-repository.ts
│   └── organization-repository.ts
├── migrations/                 # 🔄 Database Migrations
│   ├── 0001_create_users.sql
│   ├── 0002_create_subscriptions.sql
│   └── meta/
├── helpers/                    # 🛠️ Database Utilities
│   ├── query-helper.ts        # Common query patterns
│   ├── transform-helper.ts    # Type transformations
│   └── transaction-helper.ts   # Transaction utilities
└── types/                      # 📝 Database Types
    ├── models.ts              # Inferred Drizzle types
    └── common.ts              # Common database types
Key Patterns:
  • Repository Pattern - DAO for each domain entity
  • Type Separation - Domain types vs Drizzle types
  • Schema Evolution - Migration-based changes
  • Connection Pooling - Optimized database connections

Repository Pattern (DAO)

Repositories encapsulate all database operations for specific entities:
User Repository Example:
// src/db/repositories/user-repository.ts
import { eq, and, desc, count, sql } from 'drizzle-orm'
import db from '../models/db'
import { users, type UserModel, type NewUserModel } from '../models/user-model'
import { organizations } from '../models/organization-model'

// ===== CREATE OPERATIONS =====

export async function createUserDao(userData: NewUserModel): Promise<UserModel> {
  const [newUser] = await db
    .insert(users)
    .values({
      ...userData,
      createdAt: new Date(),
      updatedAt: new Date(),
    })
    .returning()

  return newUser
}

export async function createUsersDao(userDataArray: NewUserModel[]): Promise<UserModel[]> {
  return await db
    .insert(users)
    .values(userDataArray.map(userData => ({
      ...userData,
      createdAt: new Date(),
      updatedAt: new Date(),
    })))
    .returning()
}

// ===== READ OPERATIONS =====

export async function getUserByIdDao(id: string): Promise<UserModel | null> {
  const user = await db.query.users.findFirst({
    where: eq(users.id, id),
    with: {
      organization: true,
      subscriptions: {
        where: eq(subscriptions.status, 'active'),
        orderBy: desc(subscriptions.createdAt),
      },
    },
  })

  return user || null
}

export async function getUserByEmailDao(email: string): Promise<UserModel | null> {
  const user = await db.query.users.findFirst({
    where: eq(users.email, email.toLowerCase()),
  })

  return user || null
}

export async function getUsersDao(
  filters: {
    organizationId?: string
    role?: string
    isActive?: boolean
    limit?: number
    offset?: number
  } = {}
): Promise<{
  users: UserModel[]
  totalCount: number
}> {
  const conditions = []

  if (filters.organizationId) {
    conditions.push(eq(users.organizationId, filters.organizationId))
  }

  if (filters.role) {
    conditions.push(eq(users.role, filters.role))
  }

  if (filters.isActive !== undefined) {
    conditions.push(eq(users.isActive, filters.isActive))
  }

  const whereClause = conditions.length > 0 ? and(...conditions) : undefined

  // Get users with pagination
  const userResults = await db.query.users.findMany({
    where: whereClause,
    limit: filters.limit || 50,
    offset: filters.offset || 0,
    orderBy: desc(users.createdAt),
    with: {
      organization: true,
    },
  })

  // Get total count
  const [{ count: totalCount }] = await db
    .select({ count: count() })
    .from(users)
    .where(whereClause)

  return {
    users: userResults,
    totalCount,
  }
}

// ===== UPDATE OPERATIONS =====

export async function updateUserDao(
  id: string,
  updates: Partial<NewUserModel>
): Promise<UserModel> {
  const [updatedUser] = await db
    .update(users)
    .set({
      ...updates,
      updatedAt: new Date(),
    })
    .where(eq(users.id, id))
    .returning()

  return updatedUser
}

export async function updateUserMetadataDao(
  id: string,
  metadata: Record<string, any>
): Promise<UserModel> {
  const [updatedUser] = await db
    .update(users)
    .set({
      metadata: JSON.stringify(metadata),
      updatedAt: new Date(),
    })
    .where(eq(users.id, id))
    .returning()

  return updatedUser
}

// ===== DELETE OPERATIONS =====

export async function deleteUserDao(id: string): Promise<UserModel> {
  const [deletedUser] = await db
    .delete(users)
    .where(eq(users.id, id))
    .returning()

  return deletedUser
}

export async function softDeleteUserDao(id: string): Promise<UserModel> {
  return await updateUserDao(id, {
    isActive: false,
    email: `deleted_${Date.now()}_${id}@deleted.com` // Unique constraint handling
  })
}

Type Transformations

Clean separation between database types and domain types:
Domain Types vs Database Types:
// src/services/types/domain/user-types.ts
// Domain types used by services and presentation layer
export interface User {
  id: string
  name: string
  email: string
  role: UserRole
  isActive: boolean
  createdAt: Date
  updatedAt: Date

  // Computed/transformed fields
  displayName: string
  avatarUrl?: string
  permissions: Permission[]

  // Related data (when needed)
  organization?: Organization
  activeSubscription?: Subscription
}

export interface CreateUserInput {
  name: string
  email: string
  password: string
  role?: UserRole
  organizationId?: string
}

export interface UpdateUserInput {
  id: string
  name?: string
  email?: string
  role?: UserRole
  isActive?: boolean
}

// Enums for domain logic
export enum UserRole {
  GUEST = 'GUEST',
  USER = 'USER',
  MODERATOR = 'MODERATOR',
  ADMIN = 'ADMIN',
  SUPER_ADMIN = 'SUPER_ADMIN',
}
Database Types (Drizzle Generated):
// src/db/types/models.ts
// Auto-generated from Drizzle schemas
import type { users, subscriptions } from '../models'

export type UserModel = typeof users.$inferSelect
export type NewUserModel = typeof users.$inferInsert
export type SubscriptionModel = typeof subscriptions.$inferSelect

// Database-specific types
export interface UserWithRelations extends UserModel {
  organization?: OrganizationModel
  subscriptions: SubscriptionModel[]
}

Database Migrations

Critical Setup: Before running migrations, enable UUID extension in your PostgreSQL database:
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
Drizzle migrations provide schema evolution with version control:
Drizzle Configuration:
// drizzle.config.ts
import type { Config } from 'drizzle-kit'
import { env } from './src/env'

export default {
  schema: './src/db/models/*-model.ts',
  out: './src/db/migrations',
  driver: 'pg',
  dbCredentials: {
    connectionString: env.DATABASE_URL,
  },
  verbose: true,
  strict: true,
} satisfies Config
Package.json Scripts:
{
  "scripts": {
    "db:generate": "drizzle-kit generate:pg",
    "db:migrate": "drizzle-kit push:pg",
    "db:studio": "drizzle-kit studio",
    "db:drop": "drizzle-kit drop",
    "db:check": "drizzle-kit check:pg",
    "db:up": "drizzle-kit up:pg"
  }
}
Migration Workflow:
# 1. Modify schema in model files
# 2. Generate migration files
npm run db:generate

# 3. Review generated SQL
# 4. Apply migrations to database
npm run db:migrate

# 5. Verify changes in Drizzle Studio
npm run db:studio

Performance Optimization

Database performance considerations and optimization patterns:
🚀

Query Optimization

Selective loading with Drizzle queries
Proper indexing on frequently queried columns
Batch operations for bulk data changes
Query analysis with EXPLAIN ANALYZE
Connection pooling for scalability
💾

Data Access Patterns

Repository pattern for consistent access
Type transformations for clean interfaces
Pagination for large datasets
Eager loading vs lazy loading strategies
Transaction optimization for consistency
🔍

Monitoring & Analytics

Query performance tracking
Slow query identification
Database metrics collection
Connection pool monitoring
Error rate tracking
🛡️

Data Security

SQL injection prevention with prepared statements
Data encryption for sensitive fields
Access logging for audit trails
Row-level security where appropriate
Connection security with TLS

Best Practices

Repository Design:
// ✅ Good: Focused, single-responsibility repositories
export class UserRepository {
  async findById(id: string): Promise<User | null> { }
  async findByEmail(email: string): Promise<User | null> { }
  async create(user: CreateUserInput): Promise<User> { }
  async update(id: string, updates: UpdateUserInput): Promise<User> { }
  async delete(id: string): Promise<void> { }
}

// ✅ Good: Use appropriate return types
async function getUserDao(id: string): Promise<UserModel | null> {
  // Return null for not found, not undefined
  return user || null
}

// ✅ Good: Consistent error handling
async function updateUserDao(id: string, data: UpdateUserInput): Promise<UserModel> {
  const [updated] = await db.update(users)
    .set(data)
    .where(eq(users.id, id))
    .returning()

  if (!updated) {
    throw new Error(`User ${id} not found`)
  }

  return updated
}

// ❌ Bad: Generic repositories lose type safety
export class GenericRepository<T> {
  async findById(id: string): Promise<T | null> { } // Too generic
}
Persistence layer ready! Your database operations are type-safe, performant, and maintainable with proper separation between domain and database concerns using Drizzle ORM.
    Persistence Layer | ShipSaaS Documentation | ShipSaaS - Launch your SaaS with AI in days