Skip to main content
Skip to main content

Database Patterns

Common database design patterns and best practices used in Mifty applications.

Overview​

This guide covers essential database patterns that help you build scalable and maintainable applications with Mifty.

Repository Pattern​

The Repository Pattern provides a uniform interface for accessing data, regardless of the underlying storage mechanism.

Benefits​

  • Separation of Concerns: Isolates data access logic
  • Testability: Easy to mock for unit testing
  • Flexibility: Switch between different data sources
  • Consistency: Uniform data access patterns

Implementation​

export interface IUserRepository {
findById(id: string): Promise<User | null>;
create(user: CreateUserDto): Promise<User>;
update(id: string, data: UpdateUserDto): Promise<User>;
delete(id: string): Promise<void>;
}

export class UserRepository extends BaseRepository<User> implements IUserRepository {
// Implementation details
}

Unit of Work Pattern​

Manages transactions and ensures data consistency across multiple repository operations.

export class UserService {
constructor(
private userRepository: IUserRepository,
private profileRepository: IProfileRepository,
private unitOfWork: IUnitOfWork
) {}

async createUserWithProfile(userData: CreateUserDto, profileData: CreateProfileDto) {
return this.unitOfWork.transaction(async () => {
const user = await this.userRepository.create(userData);
const profile = await this.profileRepository.create({
...profileData,
userId: user.id
});
return { user, profile };
});
}
}

Query Object Pattern​

Encapsulates complex queries in reusable objects.

export class UserQueries {
static activeUsersWithProfiles(): QueryBuilder {
return QueryBuilder
.select(['users.*', 'profiles.displayName'])
.from('users')
.join('profiles', 'users.id', 'profiles.userId')
.where('users.isActive', true);
}

static usersByRole(role: string): QueryBuilder {
return QueryBuilder
.from('users')
.where('role', role)
.orderBy('createdAt', 'desc');
}
}

Data Mapper Pattern​

Separates the in-memory objects from the database schema.

export class UserMapper {
static toDomain(raw: UserRow): User {
return new User({
id: raw.id,
email: raw.email_address,
name: raw.full_name,
createdAt: new Date(raw.created_at)
});
}

static toPersistence(user: User): UserRow {
return {
id: user.id,
email_address: user.email,
full_name: user.name,
created_at: user.createdAt.toISOString()
};
}
}

Connection Pool Pattern​

Manages database connections efficiently.

export class DatabaseConfig {
static getPoolConfig(): PoolConfig {
return {
min: 2,
max: 10,
acquireTimeoutMillis: 30000,
createTimeoutMillis: 30000,
destroyTimeoutMillis: 5000,
idleTimeoutMillis: 30000,
reapIntervalMillis: 1000,
createRetryIntervalMillis: 200
};
}
}

Migration Pattern​

Manages database schema changes over time.

export class CreateUsersTable extends Migration {
async up(): Promise<void> {
await this.schema.createTable('users', (table) => {
table.uuid('id').primary();
table.string('email').unique().notNullable();
table.string('name').notNullable();
table.boolean('isActive').defaultTo(true);
table.timestamps(true, true);
});
}

async down(): Promise<void> {
await this.schema.dropTable('users');
}
}

Best Practices​

// Good
await db.transaction(async (trx) => {
await userRepository.create(userData, trx);
await profileRepository.create(profileData, trx);
});

// Avoid
await userRepository.create(userData);
await profileRepository.create(profileData); // Could fail leaving orphaned user

2. Implement Proper Error Handling​

export class UserRepository extends BaseRepository<User> {
async findById(id: string): Promise<User | null> {
try {
const result = await this.query().where('id', id).first();
return result ? this.mapToDomain(result) : null;
} catch (error) {
this.logger.error('Failed to find user by ID', { id, error });
throw new RepositoryError('User lookup failed', error);
}
}
}

3. Use Connection Pooling​

const db = knex({
client: 'postgresql',
connection: process.env.DATABASE_URL,
pool: {
min: 2,
max: 10
}
});

4. Implement Proper Indexing​

-- Index frequently queried columns
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);

-- Composite indexes for complex queries
CREATE INDEX idx_users_active_role ON users(is_active, role) WHERE is_active = true;