Skip to main content

ADR-001: Repository Pattern for Database Access

Statusโ€‹

Accepted - Implemented across all database operations with comprehensive transaction safety and race condition prevention

Contextโ€‹

The application needed a consistent, testable, and transaction-safe approach to database operations. Direct database access throughout the codebase would lead to:

  • Inconsistent error handling
  • Difficult testing due to tight coupling
  • Lack of transaction safety
  • Code duplication
  • Race conditions in concurrent data access
  • Poor separation of concerns between business logic and data access

Decisionโ€‹

We will use the Repository Pattern for all database access with the following characteristics:

1. Dual Method Patternโ€‹

  • Public async methods that create transactions (deleteAll())
  • Internal sync methods for use within existing transactions (deleteAllInternal())

2. Enhanced Transaction Safetyโ€‹

  • All mutations wrapped in DatabaseService.executeTransaction()
  • All operations use withDatabaseOperation() for retry logic and event emission
  • Synchronous database operations (node-sqlite3-wasm) eliminate race conditions
  • Atomic cache updates prevent inconsistent state during concurrent access

3. Consistent Structureโ€‹


import { Database } from "node-sqlite3-wasm";
import { logger } from "@shared/utils/logger";
import { withDatabaseOperation } from "@electron/utils/operationalHooks";
import type { DatabaseService } from "@electron/services/database/DatabaseService";

/**
* Example repository demonstrating the standard repository pattern
* implementation.
*
* @remarks
* This repository follows the dual method pattern with public async methods
* that create transactions and internal sync methods for use within existing
* transaction contexts. All database operations are wrapped with proper error
* handling and event emission.
*
* @example
*
* ```typescript
* const repository = new ExampleRepository({ databaseService });
*
* // Public async method - creates its own transaction
* await repository.bulkInsert([data1, data2]);
*
* // Internal sync method - used within existing transaction
* await databaseService.executeTransaction((db) => {
* repository.bulkInsertInternal(db, [data1, data2]);
* // other operations...
* });
* ```
*
* @public
*/
export class ExampleRepository {
/**
* Database service instance for executing database operations.
*
* @private
*
* @readonly
*/
private readonly databaseService: DatabaseService;

/**
* Creates a new ExampleRepository instance.
*
* @example
*
* ```typescript
* const repository = new ExampleRepository({
* databaseService: serviceContainer.getDatabaseService(),
* });
* ```
*
* @param dependencies - Required dependencies for repository operations
* @param dependencies.databaseService - The {@link DatabaseService} instance
* for database access
*/
constructor(dependencies: ExampleRepositoryDependencies) {
this.databaseService = dependencies.databaseService;
}

/**
* Bulk inserts multiple records into the database.
*
* @remarks
* This is a public async method that creates its own database transaction.
* Uses {@link withDatabaseOperation} for retry logic and event emission. For
* use within existing transactions, use {@link bulkInsertInternal} instead.
*
* @example
*
* ```typescript
* // Bulk insert records with automatic transaction handling
* await repository.bulkInsert([record1, record2, record3]);
* ```
*
* @param records - Array of records to insert
*
* @returns Promise that resolves when all records are successfully inserted
*
* @throws {@link Error} When database operation fails or transaction cannot
* be completed
*
* @public
*
* @see {@link bulkInsertInternal} for transaction-internal operations
*/
public async bulkInsert(records: ExampleRow[]): Promise<void> {
if (records.length === 0) {
return;
}

return withDatabaseOperation(
async () =>
this.databaseService.executeTransaction((db) => {
this.bulkInsertInternal(db, records);
return Promise.resolve();
}),
"ExampleRepository.bulkInsert"
);
}

/**
* Bulk inserts multiple records within an existing transaction context.
*
* @remarks
* This is an internal sync method designed for use within existing database
* transactions. Uses prepared statements for optimal performance with large
* datasets. Does not create its own transaction - must be called within a
* transaction context.
*
* @example
*
* ```typescript
* // Use within an existing transaction
* await databaseService.executeTransaction((db) => {
* repository.bulkInsertInternal(db, records);
* // other transactional operations...
* });
* ```
*
* @param db - The active {@link Database} connection within a transaction
* @param records - Array of records to insert
*
* @throws {@link Error} When the SQL execution fails
*
* @public
*
* @see {@link bulkInsert} for standalone operation with automatic transaction
*/
public bulkInsertInternal(db: Database, records: ExampleRow[]): void {
const stmt = db.prepare(QUERIES.INSERT);

try {
for (const record of records) {
stmt.run([
record.id,
record.name,
record.createdAt,
]);
}
logger.debug(
`[ExampleRepository] Bulk inserted ${records.length} records (internal)`
);
} finally {
stmt.finalize();
}
}

/**
* Retrieves all records from the database.
*
* @remarks
* This is a read operation wrapped with {@link withDatabaseOperation} for
* consistent error handling and event emission.
*
* @example
*
* ```typescript
* const allRecords = await repository.findAll();
* ```
*
* @returns Promise resolving to array of all records
*
* @throws {@link Error} When database operation fails
*
* @public
*/
public async findAll(): Promise<ExampleRow[]> {
return withDatabaseOperation(() => {
const db = this.getDb();
return Promise.resolve(db.all(QUERIES.SELECT_ALL) as ExampleRow[]);
}, "ExampleRepository.findAll");
}

/**
* Deletes all records from the repository table.
*
* @remarks
* This is a public async method that creates its own database transaction.
* Uses {@link withDatabaseOperation} for retry logic and event emission. For
* use within existing transactions, use {@link deleteAllInternal} instead.
*
* @example
*
* ```typescript
* // Delete all records with automatic transaction handling
* await repository.deleteAll();
* ```
*
* @returns Promise that resolves when all records are successfully deleted
*
* @throws {@link Error} When database operation fails or transaction cannot
* be completed
*
* @public
*
* @see {@link deleteAllInternal} for transaction-internal operations
*/
public async deleteAll(): Promise<void> {
return withDatabaseOperation(
async () =>
this.databaseService.executeTransaction((db) => {
this.deleteAllInternal(db);
return Promise.resolve();
}),
"ExampleRepository.deleteAll"
);
}

/**
* Deletes all records from the repository table within an existing
* transaction context.
*
* @remarks
* This is an internal sync method designed for use within existing database
* transactions. Does not create its own transaction - must be called within
* a transaction context. Logs debug information about the operation
* completion.
*
* @example
*
* ```typescript
* // Use within an existing transaction
* await databaseService.executeTransaction((db) => {
* repository.deleteAllInternal(db);
* // other transactional operations...
* });
* ```
*
* @param db - The active {@link Database} connection within a transaction
*
* @throws {@link Error} When the SQL execution fails
*
* @public
*
* @see {@link deleteAll} for standalone operation with automatic transaction
*/
public deleteAllInternal(db: Database): void {
db.run(QUERIES.DELETE_ALL);
logger.debug("[ExampleRepository] All records deleted (internal)");
}

/**
* Gets the current database connection instance.
*
* @remarks
* Provides access to the underlying database connection through the
* database service. Should be used sparingly and only when the repository
* pattern methods are insufficient.
*
* @private
*
* @example
*
* ```typescript
* const db = repository.getDb();
* // Use for complex queries not covered by repository methods
* ```
*
* @returns The active {@link Database} connection instance
*/
private getDb(): Database {
return this.databaseService.getDatabase();
}
}

/**
* Interface representing a data row structure.
*/
interface ExampleRow {
/** Unique identifier for the record */
id: string;
/** Display name of the record */
name: string;
/** Creation timestamp */
createdAt: number;
}

/**
* Dependencies required for {@link ExampleRepository} instantiation.
*
* @remarks
* Defines the dependency injection contract for the repository, ensuring proper
* service availability and testability.
*
* @public
*/
interface ExampleRepositoryDependencies {
/**
* Database service instance for executing database operations.
*
* @remarks
* Must be a properly initialized {@link DatabaseService} with an active
* database connection.
*/
readonly databaseService: DatabaseService;
}

/**
* Centralized SQL query constants for the example repository.
*
* @remarks
* Prevents SQL duplication across repository methods and improves
* maintainability. All queries should be defined here rather than inline in
* methods.
*
* @private
*/
const QUERIES = {
/**
* SQL query to insert a single record.
*/
INSERT: "INSERT INTO example_table (id, name, createdAt) VALUES (?, ?, ?)",

/**
* SQL query to select all records.
*/
SELECT_ALL:
"SELECT id, name, createdAt FROM example_table ORDER BY createdAt DESC",

/**
* SQL query to delete all records from the repository table.
*
* @defaultValue "DELETE FROM example_table"
*/
DELETE_ALL: "DELETE FROM example_table",
} as const;

4. Query Constantsโ€‹

  • Centralized query strings in constants object
  • Prevents SQL duplication and improves maintainability

Consequencesโ€‹

Positiveโ€‹

  • Consistent error handling across all database operations
  • Easy testing through dependency injection and mocking
  • Enhanced transaction safety prevents data corruption and race conditions
  • Clear separation between transactional and non-transactional operations
  • Retry logic and event emission through operational hooks
  • Race condition prevention through synchronous database operations (node-sqlite3-wasm)
  • Atomic cache updates ensure data consistency during concurrent access
  • Comprehensive logging and monitoring of all database operations

Negativeโ€‹

  • Slight complexity increase with dual method pattern
  • Learning curve for developers unfamiliar with repository pattern
  • Additional abstraction layer may seem unnecessary for simple operations

Implementation Quality Assuranceโ€‹

Race Condition Preventionโ€‹

  • Synchronous Operations: node-sqlite3-wasm ensures all database operations complete synchronously
  • Atomic Transactions: BEGIN/COMMIT/ROLLBACK operations are atomic
  • Cache Consistency: Atomic cache replacement patterns prevent race conditions

Memory Managementโ€‹

  • Proper Cleanup: All repositories implement proper resource cleanup
  • Event Listener Management: Event emissions are handled asynchronously with proper error handling
  • Connection Pooling: Single database connection managed by DatabaseService singleton

Complianceโ€‹

All repository classes implement this pattern:

  • SiteRepository
  • MonitorRepository
  • HistoryRepository
  • SettingsRepository