# Bad Example: Leaky Abstraction > An abstraction that leaks its implementation details, violating Atelier principles. Each violation is cited. ## The Code ```typescript // UserRepository — "abstracts" the database class UserRepository { async findAll(): Promise { // Leaks: returns the raw DB row type, not a domain User return db.query('SELECT id, email, password_hash, created_at, deleted_at FROM users'); } async findByEmail(email: string): Promise { // Leaks: the caller must know to filter deleted_at const rows = await db.query('SELECT * FROM users WHERE email = $1', [email]); return rows[0] || null; } async save(user: UserRow): Promise { // Leaks: the caller must know the column names and the SQL await db.query( 'UPDATE users SET email = $1, password_hash = $2, updated_at = now() WHERE id = $3', [user.email, user.password_hash, user.id] ); } } // Usage — the leak is visible const repo = new UserRepository(); const user = await repo.findByEmail('jane@example.com'); if (user && !user.deleted_at) { // caller must know about soft delete user.password_hash = await hash(newPassword); // caller must know the column await repo.save(user); // caller must know it's an UPDATE } ``` ## Violations ### C6 Composability (Core) - The abstraction is supposed to hide the database. It does not. - The caller must know: the row type (`UserRow`), the soft-delete column (`deleted_at`), the password column (`password_hash`), the SQL operation (`UPDATE`). - The abstraction is a thin wrapper. It composes nothing; it leaks everything. - **Fix:** Return a domain `User` (no `password_hash`, no `deleted_at`). Hide soft delete (the repo filters it). Hide persistence (the caller calls `save`, not `UPDATE`). ### C2 Clarity (Core) - The caller's code is unclear: `if (user && !user.deleted_at)` — what is `deleted_at`? Why does the caller check it? - The abstraction was supposed to clarify. It muddied. - **Fix:** `repo.findByEmail()` returns `User | null` (already filtered). The caller does not know soft delete exists. ### API P2 Clarity (API, by analogy) - The repo's API exposes the DB schema in its return types. `UserRow` is a DB concept, not a domain concept. - The public contract (return type) leaks the private implementation (the table). - **Fix:** The return type is `User`, a domain type. `UserRow` is internal. ### Data P8 Lifecycle Awareness (Data) - The soft-delete lifecycle (`deleted_at`) is the repo's concern. The caller should not manage it. - By exposing `deleted_at`, the repo forces every caller to remember the filter. A forgotten filter is a soft-delete leak. - **Fix:** The repo filters `deleted_at IS NULL` in every query. The caller never sees `deleted_at`. ### Security P9 Secret Hygiene (Security) - `password_hash` is in the returned `UserRow`. The caller now has access to the password hash. - A caller that logs `user` logs the hash. A caller that serializes `user` serializes the hash. - **Fix:** `User` does not include `password_hash`. Only `UserRepository` and `AuthService` (internal) see it. ### C4 Locality (Core) - The SQL is in the repo, but the column knowledge (`password_hash`, `deleted_at`) is in the caller. - A column rename touches the repo AND every caller. Locality is violated. - **Fix:** Column names are local to the repo. The caller knows only the domain `User`. ### C5 Reversibility (Core) - Changing the database (e.g., from SQL to NoSQL, or renaming a column) requires touching every caller. - The abstraction was supposed to make the change local. It does not. - **Fix:** The repo's interface (`findByEmail`, `save`) is stable. The implementation changes; the callers do not. ## What This Example Reveals The leaky abstraction is the false promise of encapsulation. The class is named `UserRepository` (suggesting it abstracts persistence), but it returns raw DB rows, exposes lifecycle columns, and leaks secret fields. The abstraction exists in name only. The cost: - Every caller must know the DB schema (C6 violated). - A schema change touches every caller (C5 violated, C4 violated). - Secret fields leak to callers (Security P9 violated). - The lifecycle is the caller's burden (Data P8 violated). The fix is always the same: **the abstraction's public type is the domain type, not the implementation type**. `UserRepository.findByEmail()` returns `User | null`, where `User` has `id`, `email`, `name` — and nothing else. `password_hash`, `deleted_at`, `UserRow` are internal. The caller knows nothing about the database.