Leaky abstraction — when implementation detail bleeds through the interface
Intermediate5 min read·eng-16-010
interview
Concept
Leaky abstraction — an abstraction where implementation details "leak through" the interface that was supposed to hide them. Callers need to know about the internals to use the abstraction correctly.
Coined by Joel Spolsky (2002): "All non-trivial abstractions, to some degree, are leaky." The Law of Leaky Abstractions.
What "leaking" means: The abstraction breaks down in edge cases, and to understand why or fix it, you must look at the underlying implementation. The hidden details become visible because the behavior depends on them.
Classic examples:
- SQL and ORMs: Eloquent is supposed to abstract SQL. But to understand N+1, performance problems, and locking behavior, you need to know SQL.
- TCP over network: TCP abstracts reliable delivery. But network latency is still visible. You can't ignore the physical network when building distributed systems.
- Lazy loading in ORM:
$user->orderslooks like a simple property access. But it fires a database query — the DB implementation leaks through. - LIKE '%query%' in SQL: SQL abstracts index access, but a leading-wildcard LIKE cannot use a B-tree index. Performance depends on knowing the internals.
In PHP/Laravel:
- ORM transactions: The Laravel
DB::transaction()abstraction leaks when you need to handledeadlockretry — you need to know about DB locking internals. - Facades:
Cache::remember()looks simple. But whether Redis returns stale data on failure depends on the underlying driver.
How to handle leaky abstractions: Know your stack deeply enough to understand where abstractions break. Don't pretend the underlying system doesn't exist.
Code Example
php
<?php
// LEAKY ABSTRACTION — ORM hides SQL but SQL knowledge is required
// Looks like a simple in-memory collection:
$users = User::where('active', true)->get();
// But: timing, memory, and locking behavior depend on SQL internals
// Leak 1: N+1 — the SQL leaks through the ORM abstraction
foreach ($users as $user) {
echo $user->orders->count(); // fires a query per user — you MUST know SQL to understand this!
}
// Leak 2: Index ignorance
User::where('email', 'LIKE', '%alice%')->get(); // leading wildcard — no index possible!
// Eloquent doesn't warn you — you need SQL index knowledge
// Leak 3: Transaction behavior leaks
\DB::transaction(function () {
$user = User::lockForUpdate()->find(1); // leaks: you need to know about SELECT FOR UPDATE in MySQL
// If you don't know what lockForUpdate does at the SQL level, you'll misuse this
});
// ANOTHER LEAK: Date filtering
User::whereDate('created_at', '2024-01-15')->get();
// Generates: WHERE DATE(created_at) = '2024-01-15'
// DATE() function wraps the column — MySQL CANNOT use an index on created_at!
// Correct (index-friendly):
User::whereBetween('created_at', ['2024-01-15 00:00:00', '2024-01-15 23:59:59'])->get();
// Knowing the SQL leak helps you write the correct query
// EMBRACING THE LEAK — don't fight it, know it
// 1. Learn the underlying layer: SQL, Redis protocol, HTTP internals
// 2. Know when you're operating near abstraction boundaries
// 3. Drop to the lower level when the abstraction fails:
$users = \DB::select("
SELECT u.*, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid'
WHERE u.active = 1
GROUP BY u.id
HAVING order_count > 5
"); // sometimes raw SQL is the right answer — the ORM abstraction leaked
// FACADE LEAK — Cache::remember() is simple, but the lock behavior leaks
\Cache::remember('stats', 3600, function () {
return \DB::table('orders')->count(); // what if Redis is down?
// If driver is 'null' (test env), it never caches — behavior depends on driver
});