# ADR-002: Standardized Transaction Management and Unit of Work Pattern

**Date**: 2025-12-12

**Status**: Proposed

## Context

Following the standardization of error handling in ADR-001, the next most common source of architectural inconsistency and potential bugs is database transaction management. Currently, several repository methods (`createUser`, `createFlyerAndItems`, etc.) implement their own transaction logic by manually acquiring a client from the connection pool, and then managing `BEGIN`, `COMMIT`, and `ROLLBACK` states.

This manual approach has several drawbacks:
**Repetitive Boilerplate**: The `try/catch/finally` block for transaction management is duplicated across multiple files.
**Error-Prone**: It is easy to forget to `client.release()` in all code paths, which can lead to connection pool exhaustion and bring down the application. 3. **Poor Composability**: It is difficult to compose multiple repository methods into a single, atomic "Unit of Work". For example, a service function that needs to update a user's points and create a budget in a single transaction cannot easily do so if both underlying repository methods create their own transactions.

## Decision

We will implement a standardized "Unit of Work" pattern through a high-level `withTransaction` helper function. This function will abstract away the complexity of transaction management.

1. **`withTransaction` Helper**: A new helper function, `withTransaction<T>(callback: (client: PoolClient) => Promise<T>): Promise<T>`, will be created. This function will be responsible for:
   - Acquiring a client from the database pool.
   - Starting a transaction (`BEGIN`).
   - Executing the `callback` function, passing the transactional client to it.
   - If the callback succeeds, it will `COMMIT` the transaction.
   - If the callback throws an error, it will `ROLLBACK` the transaction and re-throw the error.
   - In all cases, it will `RELEASE` the client back to the pool.

2. **Repository Method Signature**: Repository methods that need to be part of a transaction will be updated to optionally accept a `PoolClient` in their constructor or as a method parameter. By default, they will use the global pool. When called from within a `withTransaction` block, they will be passed the transactional client.
3. **Service Layer Orchestration**: Service-layer functions that orchestrate multi-step operations will use `withTransaction` to ensure atomicity. They will instantiate or call repository methods, providing them with the transactional client from the callback.

### Example Usage

```typescript
// In a service file...
async function registerUserAndCreateDefaultList(userData) {
  return withTransaction(async (client) => {
    // Pass the transactional client to the repositories
    const userRepo = new UserRepository(client);
    const shoppingRepo = new ShoppingRepository(client);

    const newUser = await userRepo.createUser(userData);
    await shoppingRepo.createShoppingList(newUser.user_id, 'My First List');

    return newUser;
  });
}
```

## Consequences

### Positive

**DRY (Don't Repeat Yourself)**: Transaction logic is defined in one place, eliminating boilerplate from repository and service files.
**Safety and Reliability**: Guarantees that clients are always released, preventing connection leaks. Ensures proper rollback on any error.
**Clear Composability**: Provides a clear and explicit pattern for composing multiple database operations into a single atomic unit.
**Improved Readability**: Service logic becomes cleaner, focusing on the business operations rather than the mechanics of transaction management.

### Negative

**Learning Curve**: Developers will need to learn and adopt the `withTransaction` pattern for all transactional database work.
**Refactoring Effort**: Existing methods that manually manage transactions (`createUser`, `createBudget`, etc.) will need to be refactored to use the new pattern.
